Cultures stop being static one-per-continent blocs. Culture identities are now stateful: the list is append-only (seeded once at the dawn, schism children appended later, records frozen after creation) and the per-settlement culture is mutable state. - stepCulture(year) in the yearly tick (pure hashes, no RNG): assimilation (a conquered settlement adopts its ruler's culture, which drops the Step-5 revolt cultBonus -> assimilation pacifies provinces), border conversion (population x trade-prestige pressure; realm capitals exempt), schism (a far-flung coherent cluster -- typically overseas colonies -- breaks away as a new people with a local-bank NameGen name and ethos/faith re-derived from its own lands). - computeCultures() became a pure derived refresh (seeds only when the list is empty); seedCultures() reproduces the old per-continent peoples byte-identically for the dawn and pre-v23 loads. - Colonies inherit the founder's culture at founding; the Step-3 mono-cultural vassalage rule is culture-matched (regionId fallback) so schism clusters found their own realms -> colonial independence wars. - Per-cell culture view colours by the owning settlement's culture, so a conquered city keeps its colour until it assimilates. - Save v23: culture identities + sSettleCulture + next-id counter, also in the step-back frames; snapshots rewind via truncate-and-replay. Pre-v23 saves re-seed on the next refresh. - Kind-7 world events; Cultures tab hides extinct peoples and shows a schism child's founding year. Knobs cult* in planet.cfg. - New test_cultevo.cpp (30 checks); all 15 existing suites still pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
443 lines
23 KiB
C++
443 lines
23 KiB
C++
#include "Planet.hpp"
|
|
#include "NameGen.hpp"
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <cmath>
|
|
#include <unordered_map>
|
|
|
|
// --- 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);
|
|
// each realm gets a GOVERNMENT type folded into its name. Since Step 8 the culture layer is split:
|
|
// - seedCultures() one-time seeding at the dawn (one culture per inhabited continent) -- also runs
|
|
// for pre-v23 loads. Identities (name/ethos/faith) are FROZEN at creation.
|
|
// - computeCultures() the DERIVED refresh (tallies, governments, per-cell view). Runs on every
|
|
// territory rebuild / load / step-back, so it must never mutate identity state.
|
|
// - stepCulture() the once-per-sim-year mutation pass: assimilation (conquered settlements adopt
|
|
// their ruler's culture), border conversion (dominant foreign cultural pressure)
|
|
// and schism (a far-flung cluster becomes a new people). Pure hashes of
|
|
// (stable id, year, seed) -- no RNG touched, so step-back replays it exactly.
|
|
// The culture list is append-only and records are immutable after creation (indices/colors stay
|
|
// stable; extinct cultures keep their slot); the list + sSettleCulture are saved (v23) + snapshotted.
|
|
|
|
namespace {
|
|
// A pure hash for deterministic picks -- never touches rngState (mirrors civHash in PlanetCiv.cpp).
|
|
inline uint32_t cultHash(uint32_t a) { a ^= a << 13; a ^= a >> 17; a ^= a << 5; return a ? a : 1u; }
|
|
inline double cultHashf(uint32_t a) { return (cultHash(a) & 0xFFFFFFu) / double(0x1000000); } // [0,1)
|
|
|
|
// Environmental tally over a set of settlement cells -> the signals that pick an ethos + faith.
|
|
// Factored out of the old computeCultures() so schism children re-derive theirs the same way.
|
|
struct CultEnv { int cnt = 0, coast = 0, high = 0, arid = 0, fert = 0; int biome[13] = {0}; };
|
|
|
|
void envAdd(CultEnv& a, const std::vector<Cell>& cells, const std::vector<double>& moist,
|
|
double sea, int c) {
|
|
if (c < 0 || c >= (int)cells.size()) return;
|
|
a.cnt++;
|
|
bool coast = false;
|
|
for (int j : cells[c].neighbors) if (cells[j].elevation <= sea) { coast = true; break; }
|
|
if (coast) a.coast++;
|
|
Biome b = cells[c].biome;
|
|
if ((int)b >= 0 && (int)b < 13) a.biome[(int)b]++;
|
|
if (b == Biome::Mountains || b == Biome::Hills) a.high++;
|
|
if (b == Biome::Desert || (moist.size() == cells.size() && moist[c] < 0.25)) a.arid++;
|
|
if (b == Biome::Grassland || b == Biome::Forest || b == Biome::Wetland || b == Biome::Savanna) a.fert++;
|
|
}
|
|
|
|
// Ethos: the dominant environmental signal; if none is strong, a deterministic social pick.
|
|
// Faith: coastal peoples worship the Sea, else the dominant biome sets it, with a rare variation.
|
|
void envPick(const CultEnv& a, uint32_t seed, uint32_t rk, int members,
|
|
CultureEthos& ethos, Faith& faith) {
|
|
double cnt = std::max(1, a.cnt);
|
|
double fCoast = a.coast / cnt, fHigh = a.high / cnt, fArid = a.arid / cnt, fFert = a.fert / cnt;
|
|
|
|
CultureEthos best = CultureEthos::Agrarian; double bestF = fFert;
|
|
if (fCoast > bestF) { bestF = fCoast; best = CultureEthos::Seafaring; }
|
|
if (fHigh > bestF) { bestF = fHigh; best = CultureEthos::Highland; }
|
|
if (fArid > bestF) { bestF = fArid; best = CultureEthos::Nomadic; }
|
|
if (bestF >= 0.34) ethos = best;
|
|
else {
|
|
uint32_t h = cultHash(seed ^ (rk * 2654435761u) ^ 0xC0FFEEu);
|
|
ethos = (members >= 3 && h % 3u == 0u) ? CultureEthos::Mercantile
|
|
: (h % 2u ? CultureEthos::Warlike : CultureEthos::Agrarian);
|
|
}
|
|
|
|
Faith f;
|
|
if (fCoast >= 0.5) f = Faith::Sea;
|
|
else {
|
|
int dom = 0; for (int bi = 1; bi < 13; ++bi) if (a.biome[bi] > a.biome[dom]) dom = bi;
|
|
switch ((Biome)dom) {
|
|
case Biome::Mountains: case Biome::Hills: f = Faith::Sky; break;
|
|
case Biome::Desert: case Biome::Savanna: f = Faith::Sun; break;
|
|
case Biome::Tundra: case Biome::Taiga: case Biome::Ice: f = Faith::Ancestors; break;
|
|
case Biome::Forest: case Biome::Wetland: f = Faith::Harvest; break;
|
|
case Biome::Beach: case Biome::Lake: case Biome::Ocean:f = Faith::Sea; break;
|
|
default: f = Faith::Earth; break;
|
|
}
|
|
uint32_t h = cultHash(seed ^ (rk * 40503u) ^ 0xFA17u);
|
|
if (h % 7u == 0u) f = Faith::Moon;
|
|
else if (h % 13u == 0u) f = Faith::War;
|
|
}
|
|
faith = f;
|
|
}
|
|
|
|
// The faith's proper name from a seed + language bank ("the X Faith" / "Cult of X" / "the X Path").
|
|
std::string makeFaithName(uint32_t faithSeed, int bank) {
|
|
std::string root = namegen::makeName(faithSeed, bank);
|
|
switch (cultHash(faithSeed) % 3u) {
|
|
case 0: return "the " + root + " Faith";
|
|
case 1: return "Cult of " + root;
|
|
default: return "the " + root + " Path";
|
|
}
|
|
}
|
|
}
|
|
|
|
const char* cultureEthosName(CultureEthos e) {
|
|
switch (e) {
|
|
case CultureEthos::Agrarian: return "Agrarian";
|
|
case CultureEthos::Seafaring: return "Seafaring";
|
|
case CultureEthos::Nomadic: return "Nomadic";
|
|
case CultureEthos::Highland: return "Highland";
|
|
case CultureEthos::Mercantile: return "Mercantile";
|
|
case CultureEthos::Warlike: return "Warlike";
|
|
}
|
|
return "Agrarian";
|
|
}
|
|
|
|
const char* faithFocusName(Faith f) {
|
|
switch (f) {
|
|
case Faith::Sun: return "Sun";
|
|
case Faith::Moon: return "Moon";
|
|
case Faith::Sea: return "Sea";
|
|
case Faith::Sky: return "Sky";
|
|
case Faith::Earth: return "Earth";
|
|
case Faith::Ancestors: return "Ancestors";
|
|
case Faith::War: return "War";
|
|
case Faith::Harvest: return "Harvest";
|
|
}
|
|
return "Earth";
|
|
}
|
|
|
|
const char* govTypeName(GovType g) {
|
|
switch (g) {
|
|
case GovType::Tribe: return "Chiefdom";
|
|
case GovType::CityRepublic: return "Republic";
|
|
case GovType::Duchy: return "Duchy";
|
|
case GovType::Kingdom: return "Kingdom";
|
|
case GovType::Theocracy: return "Theocracy";
|
|
case GovType::Confederation: return "Confederation";
|
|
case GovType::Empire: return "Empire";
|
|
case GovType::Autocracy: return "Autocracy";
|
|
}
|
|
return "Kingdom";
|
|
}
|
|
|
|
// One-time seeding: group living settlements into cultures, one per inhabited continent (regionId;
|
|
// settlements with no geography fall back to a per-language-bank culture), then freeze each culture's
|
|
// ethos/faith/names. Byte-identical to the pre-Step-8 derivation, so a pre-v23 load or the dawn
|
|
// produces the same peoples as before.
|
|
void Planet::seedCultures() {
|
|
cultures.clear();
|
|
sSettleCulture.assign(settlements.size(), -1);
|
|
sCultureNextId = 1;
|
|
if (settlements.empty()) return;
|
|
|
|
const int n = (int)cells.size();
|
|
const double sea = cfg.seaLevel, abP = cfg.civAbandonPop;
|
|
const uint32_t seed = cfg.seed ? cfg.seed : 1u;
|
|
|
|
// 1) Group living settlements into cultures.
|
|
std::unordered_map<int, int> keyToCulture;
|
|
auto living = [&](const Settlement& s) {
|
|
return s.cell >= 0 && s.cell < n && s.population >= abP;
|
|
};
|
|
for (size_t k = 0; k < settlements.size(); ++k) {
|
|
const Settlement& s = settlements[k];
|
|
if (!living(s)) continue;
|
|
int key = (s.regionId >= 0) ? s.regionId : (-1 - s.bank);
|
|
auto it = keyToCulture.find(key);
|
|
int ci;
|
|
if (it == keyToCulture.end()) {
|
|
ci = (int)cultures.size();
|
|
Culture cu; cu.id = sCultureNextId++; cu.regionId = s.regionId; cu.bank = s.bank;
|
|
cultures.push_back(cu);
|
|
keyToCulture[key] = ci;
|
|
} else ci = it->second;
|
|
sSettleCulture[k] = ci;
|
|
cultures[ci].members++;
|
|
cultures[ci].totalPop += s.population;
|
|
}
|
|
if (cultures.empty()) return;
|
|
|
|
// 2) Environmental tally over each culture's settlement cells.
|
|
std::vector<CultEnv> acc(cultures.size());
|
|
for (size_t k = 0; k < settlements.size(); ++k) {
|
|
int ci = sSettleCulture[k]; if (ci < 0) continue;
|
|
envAdd(acc[ci], cells, sMoist, sea, settlements[k].cell);
|
|
}
|
|
|
|
// 3) Per-culture ethos, faith and names (all deterministic hashes of the region + seed).
|
|
for (size_t ci = 0; ci < cultures.size(); ++ci) {
|
|
Culture& cu = cultures[ci];
|
|
uint32_t rk = (uint32_t)(cu.regionId >= 0 ? cu.regionId : (1000 - cu.bank)) + 1u;
|
|
envPick(acc[ci], seed, rk, cu.members, cu.ethos, cu.faith);
|
|
uint32_t nameSeed = seed ^ cultHash(rk * 2654435761u + 0x50C1A1u);
|
|
uint32_t faithSeed = seed ^ cultHash(rk * 40503u + 0xFA17Fu);
|
|
cu.name = "the " + namegen::makeName(nameSeed, cu.bank);
|
|
cu.faithName = makeFaithName(faithSeed, cu.bank);
|
|
}
|
|
}
|
|
|
|
// The DERIVED refresh: seeds once when no cultures exist, then only recomputes tallies, governments
|
|
// and the per-cell culture view from the (stateful) per-settlement culture. Safe to call on every
|
|
// territory rebuild / load / step-back -- it never reassigns a settlement's culture (only sanitizes
|
|
// out-of-range entries and backfills unset living ones from the nearest cultured neighbour).
|
|
void Planet::computeCultures() {
|
|
const int n = (int)cells.size();
|
|
sCellCulture.assign(n, -1);
|
|
if (settlements.empty()) { sSettleCulture.clear(); return; }
|
|
if (cultures.empty()) seedCultures();
|
|
if (cultures.empty()) return; // no living settlement yet -> seeded on a later refresh
|
|
|
|
const double abP = cfg.civAbandonPop;
|
|
const uint32_t seed = cfg.seed ? cfg.seed : 1u;
|
|
auto living = [&](size_t k) {
|
|
const Settlement& s = settlements[k];
|
|
return s.cell >= 0 && s.cell < n && s.population >= abP;
|
|
};
|
|
|
|
// Per-settlement culture is STATE (Step 8): resize/sanitize + backfill only, never reassign.
|
|
if (sSettleCulture.size() != settlements.size()) sSettleCulture.resize(settlements.size(), -1);
|
|
for (int& c : sSettleCulture) if (c < -1 || c >= (int)cultures.size()) c = -1;
|
|
for (size_t k = 0; k < settlements.size(); ++k) {
|
|
if (!living(k) || sSettleCulture[k] >= 0) continue;
|
|
int bestS = -1; double bestD = 1e9; // nearest living cultured settlement (deterministic)
|
|
for (size_t j = 0; j < settlements.size(); ++j) {
|
|
if (j == k || !living(j) || sSettleCulture[j] < 0) continue;
|
|
double d = std::acos(std::clamp(
|
|
cells[settlements[k].cell].unit.dot(cells[settlements[j].cell].unit), -1.0, 1.0));
|
|
if (d < bestD) { bestD = d; bestS = (int)j; }
|
|
}
|
|
if (bestS >= 0) sSettleCulture[k] = sSettleCulture[bestS];
|
|
}
|
|
|
|
// Derived tallies (living settlements only; extinct cultures keep their slot with members = 0).
|
|
for (Culture& cu : cultures) { cu.members = 0; cu.totalPop = 0.0; }
|
|
for (size_t k = 0; k < settlements.size(); ++k) {
|
|
int ci = sSettleCulture[k];
|
|
if (ci < 0 || !living(k)) continue;
|
|
cultures[ci].members++;
|
|
cultures[ci].totalPop += settlements[k].population;
|
|
}
|
|
|
|
// Government per realm (from tier + a deterministic pick) + fold it into the realm's name, and
|
|
// tag the realm with its capital's culture.
|
|
for (Nation& nat : nations) {
|
|
nat.cultureId = (nat.capital >= 0 && nat.capital < (int)sSettleCulture.size())
|
|
? sSettleCulture[nat.capital] : -1;
|
|
uint32_t gh = cultHash((nat.id * 2654435761u) ^ seed ^ 0x604Fu) % 3u;
|
|
switch (nat.tier) {
|
|
case NationTier::CityState: nat.gov = (gh == 0) ? GovType::CityRepublic : (gh == 1) ? GovType::Tribe : GovType::Theocracy; break;
|
|
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;
|
|
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;
|
|
}
|
|
}
|
|
|
|
// Per-cell culture: prefer the owning SETTLEMENT's culture (Step 8 -- a conquered city keeps its
|
|
// people's colour inside the conqueror's realm until it assimilates); fall back to the realm's.
|
|
const bool haveOwner = (int)sCellSettleOwner.size() == n;
|
|
const bool haveNation = (int)sCellNation.size() == n;
|
|
if (!haveNation) return;
|
|
for (int i = 0; i < n; ++i) {
|
|
int ni = sCellNation[i];
|
|
if (ni < 0 || ni >= (int)nations.size()) continue;
|
|
int cu = -1;
|
|
if (haveOwner) {
|
|
int ow = sCellSettleOwner[i];
|
|
if (ow >= 0 && ow < (int)sSettleCulture.size()) cu = sSettleCulture[ow];
|
|
}
|
|
sCellCulture[i] = (cu >= 0) ? cu : nations[ni].cultureId;
|
|
}
|
|
}
|
|
|
|
// --- Civilization Step 8: the yearly cultural-evolution pass ----------------------------------------
|
|
// Three passes, all pure hashes of (stable id, year, seed) so a step-back replays them exactly:
|
|
// A) assimilation -- a settlement held by a foreign-culture overlord adopts the ruler's culture
|
|
// (the Step-5 revolt cultBonus then drops on its own: assimilation pacifies provinces);
|
|
// B) border conversion -- a settlement dwarfed by a nearby foreign culture's weight (population +
|
|
// trade prestige) converts toward it (realm capitals are exempt: they anchor identity);
|
|
// C) schism -- a large culture's far-flung coherent cluster (overseas colonies) breaks away as a
|
|
// NEW people: a fresh name from the local language bank, ethos/faith re-derived from its own
|
|
// lands (appended to `cultures`; step-back truncates the list and replay re-creates it).
|
|
std::vector<WarEvent> Planet::stepCulture(long year) {
|
|
std::vector<WarEvent> ev;
|
|
const int n = (int)cells.size();
|
|
if (settlements.empty() || cultures.empty()) return ev;
|
|
if (sSettleCulture.size() != settlements.size()) return ev; // refresh hasn't run yet
|
|
const double abP = cfg.civAbandonPop;
|
|
const uint32_t seed = cfg.seed ? cfg.seed : 1u;
|
|
const size_t ns = settlements.size();
|
|
auto living = [&](size_t k) {
|
|
const Settlement& s = settlements[k];
|
|
return s.cell >= 0 && s.cell < n && s.population >= abP;
|
|
};
|
|
auto ang = [&](const Vec3& a, const Vec3& b) {
|
|
return std::acos(std::clamp(a.dot(b), -1.0, 1.0));
|
|
};
|
|
auto unitOf = [&](size_t k) -> const Vec3& { return cells[settlements[k].cell].unit; };
|
|
std::vector<char> converted(ns, 0); // at most one culture change per settlement per year
|
|
|
|
// A) Assimilation under foreign rule.
|
|
if (sSettleAllegiance.size() == ns && cfg.cultAssimRate > 0.0) {
|
|
for (size_t s = 0; s < ns; ++s) {
|
|
if (!living(s)) continue;
|
|
int ov = sSettleAllegiance[s];
|
|
if (ov < 0 || ov >= (int)ns || ov == (int)s || !living((size_t)ov)) continue;
|
|
int myCult = sSettleCulture[s], ovCult = sSettleCulture[ov];
|
|
if (ovCult < 0 || ovCult >= (int)cultures.size() || myCult == ovCult) continue;
|
|
if (cultHashf(settlements[s].id * 2654435761u ^ (uint32_t)year * 40503u ^ seed ^ 0xA5513Au)
|
|
>= cfg.cultAssimRate) continue;
|
|
sSettleCulture[s] = ovCult; converted[s] = 1;
|
|
ev.push_back(WarEvent{ 1, settlements[s].cell,
|
|
settlements[s].name + " adopts the culture of " + cultures[ovCult].name,
|
|
"Generations under foreign rule erode the old ways.", 7 });
|
|
}
|
|
}
|
|
|
|
// B) Border conversion under dominant foreign cultural pressure.
|
|
if (cfg.cultConvertRate > 0.0) {
|
|
std::vector<char> isCapital(ns, 0);
|
|
for (const Nation& nat : nations)
|
|
if (nat.capital >= 0 && nat.capital < (int)ns) isCapital[nat.capital] = 1;
|
|
const bool haveProsp = sProsperity.size() == ns;
|
|
std::vector<double> w(ns, 0.0); // cultural weight: population x (1 + prestige from trade)
|
|
for (size_t k = 0; k < ns; ++k)
|
|
if (living(k))
|
|
w[k] = settlements[k].population
|
|
* (1.0 + cfg.cultPrestigeWeight * (haveProsp ? sProsperity[k] : 0.0));
|
|
const double range = std::max(1e-6, cfg.cultSpreadRange);
|
|
std::vector<double> pressure(cultures.size(), 0.0);
|
|
for (size_t s = 0; s < ns; ++s) {
|
|
if (!living(s) || converted[s] || isCapital[s]) continue;
|
|
int myCult = sSettleCulture[s];
|
|
if (myCult < 0 || myCult >= (int)cultures.size()) continue;
|
|
std::fill(pressure.begin(), pressure.end(), 0.0);
|
|
for (size_t j = 0; j < ns; ++j) {
|
|
if (j == s || !living(j)) continue;
|
|
int cj = sSettleCulture[j]; if (cj < 0 || cj >= (int)cultures.size()) continue;
|
|
double d = ang(unitOf(s), unitOf(j));
|
|
if (d < range) pressure[cj] += w[j] * (1.0 - d / range);
|
|
}
|
|
double own = pressure[myCult] + w[s];
|
|
int cBest = -1; double pBest = 0.0;
|
|
for (size_t c = 0; c < cultures.size(); ++c)
|
|
if ((int)c != myCult && pressure[c] > pBest) { pBest = pressure[c]; cBest = (int)c; }
|
|
if (cBest < 0 || own <= 0.0 || pBest <= cfg.cultConvertDominance * own) continue;
|
|
double rate = cfg.cultConvertRate * std::min(3.0, pBest / (cfg.cultConvertDominance * own));
|
|
if (cultHashf(settlements[s].id * 2654435761u ^ (uint32_t)year * 19349663u ^ seed ^ 0xB07DE4u)
|
|
>= rate) continue;
|
|
sSettleCulture[s] = cBest; converted[s] = 1;
|
|
ev.push_back(WarEvent{ 1, settlements[s].cell,
|
|
settlements[s].name + " embraces the ways of " + cultures[cBest].name,
|
|
"Kinship and trade draw the town into a foreign sphere.", 7 });
|
|
}
|
|
}
|
|
|
|
// C) Schism: a distant coherent cluster of a large culture becomes a new people.
|
|
const size_t ncult = cultures.size(); // iterate the pre-pass list (children append)
|
|
if (cfg.cultSchismRate > 0.0 && (int)ncult < 64) {
|
|
std::vector<int> memCount(ncult, 0);
|
|
std::vector<Vec3> cSum(ncult);
|
|
for (size_t s = 0; s < ns; ++s) {
|
|
if (!living(s)) continue;
|
|
int ci = sSettleCulture[s]; if (ci < 0 || ci >= (int)ncult) continue;
|
|
memCount[ci]++;
|
|
cSum[ci] = cSum[ci] + unitOf(s) * std::max(1.0, settlements[s].population);
|
|
}
|
|
const double range = std::max(1e-6, cfg.cultSpreadRange);
|
|
for (size_t ci = 0; ci < ncult && (int)cultures.size() < 64; ++ci) {
|
|
if (memCount[ci] < std::max(1, cfg.cultSchismMinMembers)) continue;
|
|
if (cSum[ci].length() <= 1e-12) continue;
|
|
Vec3 centroid = cSum[ci].normalized(); // population-weighted cultural core
|
|
// One deterministic roll per culture-year (cheap early exit; pure hash, no state consumed).
|
|
if (cultHashf(cultures[ci].id * 2654435761u ^ (uint32_t)year * 40503u ^ seed ^ 0x5C1531u)
|
|
>= cfg.cultSchismRate) continue;
|
|
// Members far from the core; the breakaway cluster = the farthest one + distant members
|
|
// near it (a coherent region / overseas colony group, not scattered strays).
|
|
std::vector<int> distant; int far = -1; double farD = 0.0;
|
|
for (size_t s = 0; s < ns; ++s) {
|
|
if (!living(s) || sSettleCulture[s] != (int)ci) continue;
|
|
double d = ang(unitOf(s), centroid);
|
|
if (d > cfg.cultSchismRange) {
|
|
distant.push_back((int)s);
|
|
if (d > farD) { farD = d; far = (int)s; }
|
|
}
|
|
}
|
|
if (far < 0 || (int)distant.size() < std::max(1, cfg.cultSchismMinCluster)) continue;
|
|
std::vector<int> cluster;
|
|
for (int s : distant)
|
|
if (ang(unitOf((size_t)s), unitOf((size_t)far)) <= range) cluster.push_back(s);
|
|
if ((int)cluster.size() < std::max(1, cfg.cultSchismMinCluster)) continue;
|
|
if ((int)cluster.size() >= memCount[ci]) continue; // a schism splits, it never renames all
|
|
|
|
// Copy the parent fields we need BEFORE the push_back (it invalidates references).
|
|
const uint32_t parId = cultures[ci].id;
|
|
const std::string parName = cultures[ci].name;
|
|
const Faith parFaith = cultures[ci].faith;
|
|
const std::string parFaithName = cultures[ci].faithName;
|
|
const int parBank = cultures[ci].bank;
|
|
|
|
// Majority regionId of the cluster (ties resolve by cluster order -- deterministic).
|
|
std::unordered_map<int, int> regCount; int regBest = -1, regBestN = 0;
|
|
for (int s : cluster) {
|
|
int c = ++regCount[settlements[s].regionId];
|
|
if (c > regBestN) { regBestN = c; regBest = settlements[s].regionId; }
|
|
}
|
|
|
|
Culture child;
|
|
child.id = sCultureNextId++;
|
|
child.parentId = (int)parId;
|
|
child.foundedYear = year;
|
|
child.regionId = regBest;
|
|
child.bank = (regBest >= 0) ? namegen::bankForRegion(cfg.seed, regBest) : parBank;
|
|
|
|
// Ethos/faith re-derived from the cluster's own lands (hash-salted by the child id so the
|
|
// faith can mutate); keep the parent's faith name if the faith itself is unchanged.
|
|
CultEnv env;
|
|
for (int s : cluster) envAdd(env, cells, sMoist, cfg.seaLevel, settlements[s].cell);
|
|
uint32_t rk = child.id * 0x9E3779B9u + 0xC41Du;
|
|
envPick(env, seed, rk, (int)cluster.size(), child.ethos, child.faith);
|
|
|
|
uint32_t nameSeed = seed ^ cultHash(rk * 2654435761u + 0x50C1A1u);
|
|
std::string root = namegen::makeName(nameSeed, child.bank);
|
|
auto usedName = [&](const std::string& r) {
|
|
for (const Culture& c : cultures) if (c.name == "the " + r) return true; return false;
|
|
};
|
|
for (int g = 0; usedName(root) && g < 128; ++g)
|
|
root = namegen::makeName(nameSeed += 0x9E3779B9u, child.bank);
|
|
child.name = "the " + root;
|
|
child.faithName = (child.faith == parFaith)
|
|
? parFaithName
|
|
: makeFaithName(seed ^ cultHash(rk * 40503u + 0xFA17Fu), child.bank);
|
|
|
|
int childIdx = (int)cultures.size();
|
|
for (int s : cluster) { sSettleCulture[s] = childIdx; converted[s] = 1; }
|
|
std::string title = child.name + " break away from " + parName;
|
|
title[0] = (char)std::toupper((unsigned char)title[0]);
|
|
ev.push_back(WarEvent{ 2, settlements[far].cell, title,
|
|
"A distant people drifts apart and names itself anew.", 7 });
|
|
cultures.push_back(std::move(child));
|
|
}
|
|
}
|
|
return ev;
|
|
}
|