planetsim/src/render/Colors.cpp
Jonas Reith 91f1b90a93 Civ Step 4: culture, beliefs & governments
Give the world peoples and faiths on top of Step 3's realms. All derived
deterministically from the (saved) settlement set + geography (pure hashes,
no RNG, recomputed each sim year -> no save-version bump, step-back free).

- PlanetCulture.{hpp,cpp}: computeCultures() groups living settlements into one
  culture per inhabited continent, each with a generated people name, a dominant
  ethos picked from its cells' environment (Seafaring/Highland/Nomadic/Agrarian,
  else a hashed Mercantile/Warlike), and a religion (a Faith focus biased by the
  dominant biome + a generated faith name). Runs after computeTerritory().
- Governments: each realm (Nation) gets a GovType from its tier + a deterministic
  pick, folded into its name ("Republic of X", "Duchy of X", "X Theocracy", "X
  Confederation", "X Dominion", ...) -- culture-driven renaming.
- Culture-driven grouping: realm vassalage is now restricted to the same
  continent, so every kingdom/empire is mono-cultural.
- Render: a Culture colour mode (cultureColor, cultural blocs) + pale
  buildCultureBorders lines + a Cultures tab + a cell-info culture/faith line +
  government-aware realm labels, all under key X. Cultures recompute with
  territory in rebuildTerritory().
- test_culture.cpp: one culture per continent, valid ethos/faith, governments per
  tier reflected in names, mono-cultural realms, determinism, RNG isolation,
  save->load->recompute parity. All 12 suites green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 07:58:58 +02:00

239 lines
11 KiB
C++

#include "Colors.hpp"
#include <algorithm>
#include <cmath>
Color elevationColor(double e, double seaLevel) {
if (e < seaLevel) {
// Water: deep -> shallow blue.
double t = std::clamp((e + 11000.0) / (seaLevel + 11000.0), 0.0, 1.0);
return Color{ (unsigned char)(10 + 20 * t),
(unsigned char)(30 + 90 * t),
(unsigned char)(80 + 120 * t), 255 };
}
// Land: green -> brown -> white by height.
double t = std::clamp(e / 9000.0, 0.0, 1.0);
if (t < 0.4) { double u = t / 0.4;
return Color{ (unsigned char)(60 + 80 * u), (unsigned char)(140 - 30 * u),
(unsigned char)(50), 255 }; }
if (t < 0.75) { double u = (t - 0.4) / 0.35;
return Color{ (unsigned char)(140 - 30 * u), (unsigned char)(110 - 40 * u),
(unsigned char)(50 + 10 * u), 255 }; }
double u = (t - 0.75) / 0.25;
return Color{ (unsigned char)(110 + 145 * u), (unsigned char)(70 + 185 * u),
(unsigned char)(60 + 195 * u), 255 };
}
Color plateColor(int id) {
float h = std::fmod(id * 0.61803398875f, 1.0f) * 360.0f;
return ColorFromHSV(h, 0.65f, 0.85f);
}
Color ageColor(double age, double maxAge) {
double t = std::clamp(age / std::max(1.0, maxAge), 0.0, 1.0);
return Color{ (unsigned char)(40 + 200 * t), (unsigned char)(40),
(unsigned char)(200 - 160 * t), 255 };
}
Color crustColor(bool oceanic) {
return oceanic ? Color{ 30, 60, 160, 255 } // oceanic: deep blue
: Color{ 160, 130, 70, 255 }; // continental: warm brown
}
Color lakeColor() { return Color{ 40, 200, 210, 255 }; } // bright turquoise (vs ocean blue)
Color biomeColor(Biome b) {
switch (b) {
case Biome::Ocean: return Color{ 20, 60, 120, 255 }; // deep blue
case Biome::Ice: return Color{ 235, 240, 250, 255 }; // white (polar caps / snow)
case Biome::Lake: return Color{ 40, 200, 210, 255 }; // bright turquoise
case Biome::Beach: return Color{ 222, 210, 150, 255 }; // pale sand
case Biome::Wetland: return Color{ 70, 115, 95, 255 }; // dark teal-green (swamp/bayou)
case Biome::Grassland: return Color{ 130, 185, 80, 255 }; // light green
case Biome::Savanna: return Color{ 185, 180, 85, 255 }; // yellow-green
case Biome::Desert: return Color{ 214, 184, 120, 255 }; // tan
case Biome::Forest: return Color{ 40, 110, 50, 255 }; // dark green
case Biome::Taiga: return Color{ 55, 105, 85, 255 }; // blue-green (boreal)
case Biome::Tundra: return Color{ 155, 165, 150, 255 }; // pale grey-green
case Biome::Hills: return Color{ 120, 135, 70, 255 }; // olive
case Biome::Mountains: return Color{ 135, 125, 115, 255 }; // grey-brown
}
return Color{ 255, 0, 255, 255 }; // unreachable; flags an unmapped biome
}
const char* biomeName(Biome b) {
switch (b) {
case Biome::Ocean: return "Ocean";
case Biome::Ice: return "Ice cap";
case Biome::Lake: return "Lake";
case Biome::Beach: return "Beach";
case Biome::Wetland: return "Wetland";
case Biome::Grassland: return "Grassland";
case Biome::Savanna: return "Savanna";
case Biome::Desert: return "Desert";
case Biome::Forest: return "Forest";
case Biome::Taiga: return "Taiga";
case Biome::Tundra: return "Tundra";
case Biome::Hills: return "Hills";
case Biome::Mountains: return "Mountains";
}
return "?";
}
const char* colorModeName(ColorMode m) {
switch (m) {
case ColorMode::Elevation: return "Elevation";
case ColorMode::Plate: return "Plates";
case ColorMode::Age: return "Crust age";
case ColorMode::Crust: return "Crust type";
case ColorMode::Biome: return "Biome";
case ColorMode::Temperature: return "Temperature";
case ColorMode::Precip: return "Precipitation";
case ColorMode::FloraDensity: return "Flora density";
case ColorMode::FaunaDensity: return "Fauna density";
case ColorMode::FungaDensity: return "Funga density";
case ColorMode::Ecoregion: return "Ecoregions";
case ColorMode::Habitability: return "Habitability";
case ColorMode::Territory: return "Territory / realms";
case ColorMode::Culture: return "Culture / faiths";
case ColorMode::TempSummer: return "Temperature (summer)";
case ColorMode::TempWinter: return "Temperature (winter)";
case ColorMode::Seasonality: return "Seasonality (summer-winter)";
}
return "?";
}
// Two-colour density ramp helper: barren -> rich.
static Color ramp2(double d01, const unsigned char lo[3], const unsigned char hi[3]) {
double t = std::clamp(d01, 0.0, 1.0);
auto L = [&](int c) { return (unsigned char)(lo[c] + (hi[c] - lo[c]) * t); };
return Color{ L(0), L(1), L(2), 255 };
}
Color floraColor(double d01) { // barren tan -> lush green
static const unsigned char lo[3] = { 200, 190, 150 }, hi[3] = { 25, 120, 35 };
return ramp2(d01, lo, hi);
}
Color faunaColor(double d01) { // pale -> amber -> red
double t = std::clamp(d01, 0.0, 1.0);
static const unsigned char key[3][3] = {
{ 225, 220, 195 }, // 0.0 pale
{ 220, 160, 60 }, // 0.5 amber
{ 180, 55, 40 }, // 1.0 red
};
double s = t * 2.0; int k = std::min(1, (int)s); double f = s - k;
auto L = [&](int c) { return (unsigned char)(key[k][c] + (key[k + 1][c] - key[k][c]) * f); };
return Color{ L(0), L(1), L(2), 255 };
}
Color fungaColor(double d01) { // pale -> violet/brown
static const unsigned char lo[3] = { 215, 205, 210 }, hi[3] = { 110, 55, 120 };
return ramp2(d01, lo, hi);
}
Color marineFloraColor(double d01) { // deep ocean blue -> bright teal/green bloom
static const unsigned char lo[3] = { 18, 45, 80 }, hi[3] = { 60, 215, 160 };
return ramp2(d01, lo, hi);
}
Color marineFaunaColor(double d01) { // deep blue -> cyan -> warm (rich shelves)
double t = std::clamp(d01, 0.0, 1.0);
static const unsigned char key[3][3] = {
{ 18, 45, 80 }, // 0.0 deep blue
{ 50, 175, 200 }, // 0.5 cyan
{ 235, 195, 90 }, // 1.0 warm/gold
};
double s = t * 2.0; int k = std::min(1, (int)s); double f = s - k;
auto L = [&](int c) { return (unsigned char)(key[k][c] + (key[k + 1][c] - key[k][c]) * f); };
return Color{ L(0), L(1), L(2), 255 };
}
Color nationColor(int id) { // distinct per-realm tint (offset hue/sat vs plateColor)
if (id < 0) return Color{ 40, 44, 50, 255 }; // wilderness: dim grey
float h = std::fmod((id + 4) * 0.61803398875f + 0.13f, 1.0f) * 360.0f;
return ColorFromHSV(h, 0.58f, 0.92f);
}
Color cultureColor(int id) { // per-culture tint (different phase so blocs read vs realms)
if (id < 0) return Color{ 40, 44, 50, 255 }; // no culture: dim grey
float h = std::fmod((id + 2) * 0.61803398875f + 0.47f, 1.0f) * 360.0f;
return ColorFromHSV(h, 0.50f, 0.86f); // slightly softer/paler than realm tints
}
Color habitabilityColor(double h01) { // barren grey -> green -> fertile gold
double t = std::clamp(h01, 0.0, 1.0);
static const unsigned char key[3][3] = {
{ 70, 74, 82 }, // 0.0 barren grey
{ 70, 150, 80 }, // 0.5 green
{ 230, 205, 90 }, // 1.0 fertile gold
};
double s = t * 2.0; int k = std::min(1, (int)s); double f = s - k;
auto L = [&](int c) { return (unsigned char)(key[k][c] + (key[k + 1][c] - key[k][c]) * f); };
return Color{ L(0), L(1), L(2), 255 };
}
Color ecoregionColor(int id, Biome b, double productivity) {
if (id < 0) return Color{ 48, 52, 58, 255 };
Color base = ColorFromHSV(std::fmod((id + 11) * 0.61803398875f, 1.0f) * 360.0f, 0.55f, 0.82f);
Color bio = biomeColor(b);
double p = std::clamp(productivity, 0.0, 1.0);
auto mix = [&](unsigned char a, unsigned char c, double t) {
return (unsigned char)(a * (1.0 - t) + c * t);
};
double biomeWeight = 0.35;
Color out{ mix(base.r, bio.r, biomeWeight), mix(base.g, bio.g, biomeWeight),
mix(base.b, bio.b, biomeWeight), 255 };
double brighten = 0.72 + 0.28 * p;
out.r = (unsigned char)std::clamp(out.r * brighten, 0.0, 255.0);
out.g = (unsigned char)std::clamp(out.g * brighten, 0.0, 255.0);
out.b = (unsigned char)std::clamp(out.b * brighten, 0.0, 255.0);
return out;
}
// Temperature ramp over ~[-40, 40] C: deep blue -> cyan -> green -> yellow -> red.
Color tempColor(double celsius) {
double t = std::clamp((celsius + 40.0) / 80.0, 0.0, 1.0); // 0 cold .. 1 hot
// 4 segments between 5 control colors.
static const unsigned char key[5][3] = {
{ 30, 40, 130 }, // -40 C deep blue
{ 60, 160, 210 }, // -20 C cyan
{ 90, 190, 90 }, // 0 C green
{ 225, 200, 70 }, // +20 C yellow
{ 210, 70, 50 }, // +40 C red
};
double s = t * 4.0; int k = std::min(3, (int)s); double f = s - k;
auto L = [&](int c){ return (unsigned char)(key[k][c] + (key[k + 1][c] - key[k][c]) * f); };
return Color{ L(0), L(1), L(2), 255 };
}
// Seasonality ramp: summer-winter range in deg C, ~[0, 45]: calm grey -> warm orange.
Color seasonColor(double rangeC) {
static const unsigned char lo[3] = { 95, 100, 110 }, hi[3] = { 235, 130, 40 };
double t = std::clamp(rangeC / 45.0, 0.0, 1.0);
auto L = [&](int c){ return (unsigned char)(lo[c] + (hi[c] - lo[c]) * t); };
return Color{ L(0), L(1), L(2), 255 };
}
// Tide: diverging around mid-tide. Low (negative) -> amber, high (positive) -> cyan.
Color tideColor(double level, double range) {
static const unsigned char loC[3] = { 235, 175, 80 }; // low tide (amber)
static const unsigned char midC[3] = { 150, 175, 185 }; // mid tide (pale)
static const unsigned char hiC[3] = { 55, 165, 235 }; // high tide (cyan)
double t = std::clamp(level / std::max(1e-6, range), -1.0, 1.0);
const unsigned char* a = t < 0.0 ? loC : midC;
const unsigned char* b = t < 0.0 ? midC : hiC;
double f = std::fabs(t);
auto L = [&](int c){ return (unsigned char)(a[c] + (b[c] - a[c]) * f); };
return Color{ L(0), L(1), L(2), 255 };
}
// Precipitation ramp over normalized [0,1]: tan (dry) -> green -> teal/blue (wet).
Color precipColor(double moist01) {
double t = std::clamp(moist01, 0.0, 1.0);
static const unsigned char key[4][3] = {
{ 205, 180, 120 }, // 0.00 dry tan
{ 170, 185, 90 }, // 0.33 scrub
{ 70, 160, 90 }, // 0.66 green
{ 40, 120, 190 }, // 1.00 wet blue
};
double s = t * 3.0; int k = std::min(2, (int)s); double f = s - k;
auto L = [&](int c){ return (unsigned char)(key[k][c] + (key[k + 1][c] - key[k][c]) * f); };
return Color{ L(0), L(1), L(2), 255 };
}