Realms stop coexisting peacefully and go to war. Unlike the derived Steps 3-4,
war is stateful & path-dependent, so it adds saved state (allegiance + wars +
a war RNG), extends the step-back snapshot, and bumps the save to v21.
- PlanetConflict.{hpp,cpp}: stepConflict(year) runs once per sim year. Neighbouring
realms (adjacency by settlement proximity) grow hostile from ambition (size gap)
+ ideology (culture/faith difference) + contested frontier + a yearly streak, and
declare wars (cap warMaxConcurrent). Each war-year runs a battle (strength =
totalPop x Warlike bonus x defender home advantage), inflicts casualties on
frontier cities, and the winner conquers a loser frontier city (allegiance flips
to the victor) or sacks it (ruins). Conquered foreign/distant cities revolt over
time; a realm that loses its capital collapses -> empires rise and fall. Separate
sWarRng keeps tectonics deterministic.
- computeTerritory() honours sSettleAllegiance (overriding the mono-cultural rule)
with a chain-resolving capital lookup, so borders move as cities change hands.
- Save v21: allegiance + wars + war RNG (new hasConflict readState param + a
per-frame block in the step-back history); WeatherSnapshot + capture/restoreWeather
extended, so ,/. rewind conquests + revolts.
- Render: red war-front lines + a red at-war marker & active-wars list in the Realms
tab over the Territory view (P), a cell-info AT WAR flag, a HUD war count, and
kind=5 events (declare / capture / sack / revolt / peace).
- war* config knobs; test_conflict.cpp covers wars erupting, conquest moving the
border, revolts, determinism, RNG isolation, save-v21 + snapshot round-trip.
All 13 suites green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
337 lines
16 KiB
C++
337 lines
16 KiB
C++
#include "Overlays.hpp"
|
|
#include "Map2D.hpp" // projLonLat (2D projection of overlays)
|
|
#include "Colors.hpp" // elevationColor (subgrid patch)
|
|
#include "rlgl.h"
|
|
#include "Projection.hpp" // dirToLonLat / lonLatToDir
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstdlib> // std::abs(int)
|
|
|
|
void buildBorders(const Planet& p, float radius,
|
|
std::vector<Vector3>& real, std::vector<Vector3>& ridge) {
|
|
real.clear(); ridge.clear();
|
|
auto midV = [&](int i, int j) -> Vector3 {
|
|
Vec3 m = ((p.cells[i].unit + p.cells[j].unit) * 0.5).normalized() * radius;
|
|
return Vector3{ (float)m.x, (float)m.y, (float)m.z };
|
|
};
|
|
auto isBaby = [&](int pid){ return pid >= 0 && pid < (int)p.plates.size() && p.plates[pid].baby; };
|
|
auto emit = [&](const Vector3& a, const Vector3& b, int pA, int pB) {
|
|
std::vector<Vector3>& out = (isBaby(pA) || isBaby(pB)) ? ridge : real;
|
|
out.push_back(a); out.push_back(b);
|
|
};
|
|
const std::vector<int>& tri = p.triIndices();
|
|
for (size_t k = 0; k + 2 < tri.size(); k += 3) {
|
|
int ia = tri[k], ib = tri[k + 1], ic = tri[k + 2];
|
|
int pa = p.cells[ia].plateId, pb = p.cells[ib].plateId, pc = p.cells[ic].plateId;
|
|
if (pa == pb && pb == pc) continue;
|
|
if (pa != pb && pb != pc && pa != pc) {
|
|
Vec3 c = ((p.cells[ia].unit + p.cells[ib].unit + p.cells[ic].unit)
|
|
* (1.0 / 3.0)).normalized() * radius;
|
|
Vector3 C{ (float)c.x, (float)c.y, (float)c.z };
|
|
emit(C, midV(ia, ib), pa, pb);
|
|
emit(C, midV(ib, ic), pb, pc);
|
|
emit(C, midV(ic, ia), pc, pa);
|
|
} else {
|
|
int lone, o1, o2;
|
|
if (pa == pb) { lone = ic; o1 = ia; o2 = ib; }
|
|
else if (pb == pc) { lone = ia; o1 = ib; o2 = ic; }
|
|
else { lone = ib; o1 = ia; o2 = ic; }
|
|
emit(midV(lone, o1), midV(lone, o2), p.cells[lone].plateId, p.cells[o1].plateId);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Trace boundaries where a per-cell integer label differs across a triangle's cells (the plate
|
|
// dual-contour, parameterised by the label array). Shared by nation + culture borders.
|
|
static void buildLabelBorders(const Planet& p, float radius, const std::vector<int>& cn,
|
|
std::vector<Vector3>& segs) {
|
|
segs.clear();
|
|
if ((int)cn.size() != (int)p.cells.size()) return;
|
|
auto midV = [&](int i, int j) -> Vector3 {
|
|
Vec3 m = ((p.cells[i].unit + p.cells[j].unit) * 0.5).normalized() * radius;
|
|
return Vector3{ (float)m.x, (float)m.y, (float)m.z };
|
|
};
|
|
auto emit = [&](const Vector3& a, const Vector3& b) { segs.push_back(a); segs.push_back(b); };
|
|
const std::vector<int>& tri = p.triIndices();
|
|
for (size_t k = 0; k + 2 < tri.size(); k += 3) {
|
|
int ia = tri[k], ib = tri[k + 1], ic = tri[k + 2];
|
|
int na = cn[ia], nb = cn[ib], nc = cn[ic];
|
|
if (na == nb && nb == nc) continue;
|
|
if (na != nb && nb != nc && na != nc) {
|
|
Vec3 c = ((p.cells[ia].unit + p.cells[ib].unit + p.cells[ic].unit) * (1.0 / 3.0)).normalized() * radius;
|
|
Vector3 C{ (float)c.x, (float)c.y, (float)c.z };
|
|
emit(C, midV(ia, ib)); emit(C, midV(ib, ic)); emit(C, midV(ic, ia));
|
|
} else {
|
|
int lone, o1, o2;
|
|
if (na == nb) { lone = ic; o1 = ia; o2 = ib; }
|
|
else if (nb == nc) { lone = ia; o1 = ib; o2 = ic; }
|
|
else { lone = ib; o1 = ia; o2 = ic; }
|
|
emit(midV(lone, o1), midV(lone, o2));
|
|
}
|
|
}
|
|
}
|
|
|
|
void buildNationBorders(const Planet& p, float radius, std::vector<Vector3>& segs) {
|
|
buildLabelBorders(p, radius, p.cellNation(), segs);
|
|
}
|
|
|
|
void buildCultureBorders(const Planet& p, float radius, std::vector<Vector3>& segs) {
|
|
buildLabelBorders(p, radius, p.cellCulture(), segs);
|
|
}
|
|
|
|
void buildWarFrontier(const Planet& p, float radius, std::vector<Vector3>& segs) {
|
|
segs.clear();
|
|
const std::vector<int>& cn = p.cellNation();
|
|
if (p.warList().empty() || (int)cn.size() != (int)p.cells.size()) return;
|
|
// A short tick across each cell edge whose two realms are currently at war (the war front).
|
|
for (int i = 0; i < (int)p.cells.size(); ++i) {
|
|
int a = cn[i]; if (a < 0) continue;
|
|
for (int j : p.cells[i].neighbors) {
|
|
if (j <= i) continue; // each undirected edge once
|
|
int b = (j < (int)cn.size()) ? cn[j] : -1;
|
|
if (b < 0 || b == a || !p.realmsAtWar(a, b)) continue;
|
|
Vec3 m = ((p.cells[i].unit + p.cells[j].unit) * 0.5).normalized() * radius;
|
|
Vec3 t = (p.cells[j].unit - p.cells[i].unit).normalized() * (radius * 0.03);
|
|
segs.push_back(Vector3{ (float)(m.x - t.x), (float)(m.y - t.y), (float)(m.z - t.z) });
|
|
segs.push_back(Vector3{ (float)(m.x + t.x), (float)(m.y + t.y), (float)(m.z + t.z) });
|
|
}
|
|
}
|
|
}
|
|
|
|
void buildDriftArrows(const Planet& p, float radius,
|
|
std::vector<Vector3>& out, std::vector<PlateLabel>& labels) {
|
|
out.clear(); labels.clear();
|
|
int np = (int)p.plates.size();
|
|
if (np == 0) return;
|
|
std::vector<Vec3> sum(np, Vec3{0, 0, 0});
|
|
std::vector<int> cnt(np, 0);
|
|
for (const auto& c : p.cells)
|
|
if (c.plateId >= 0 && c.plateId < np) { sum[c.plateId] = sum[c.plateId] + c.unit; ++cnt[c.plateId]; }
|
|
std::vector<Vec3> centroid(np), vel(np);
|
|
double vmax = 1e-30;
|
|
for (int i = 0; i < np; ++i) {
|
|
if (cnt[i] == 0) continue;
|
|
centroid[i] = sum[i].normalized();
|
|
Vec3 omega = p.plates[i].driftAxis * p.plates[i].driftSpeed;
|
|
vel[i] = omega.cross(centroid[i]);
|
|
vmax = std::max(vmax, vel[i].length());
|
|
}
|
|
const double shaftMin = 0.12, shaftMax = 0.45;
|
|
auto V = [](const Vec3& v) { return Vector3{ (float)v.x, (float)v.y, (float)v.z }; };
|
|
for (int i = 0; i < np; ++i) {
|
|
if (cnt[i] == 0 || vel[i].length() < 1e-12) continue;
|
|
Vec3 dir = vel[i].normalized();
|
|
double len = shaftMin + (shaftMax - shaftMin) * (vel[i].length() / vmax);
|
|
Vec3 base = centroid[i] * (double)radius;
|
|
Vec3 tip = base + dir * len;
|
|
Vec3 perp = dir.cross(centroid[i]).normalized();
|
|
double hl = len * 0.30;
|
|
Vec3 back = dir * -1.0;
|
|
Vec3 h1 = tip + (back * 0.8 + perp * 0.6) * hl;
|
|
Vec3 h2 = tip + (back * 0.8 - perp * 0.6) * hl;
|
|
out.push_back(V(base)); out.push_back(V(tip));
|
|
out.push_back(V(tip)); out.push_back(V(h1));
|
|
out.push_back(V(tip)); out.push_back(V(h2));
|
|
|
|
Vec3 lbl = centroid[i] * (radius + 0.015); // just above the planet surface at the plate centroid
|
|
labels.push_back({i, V(lbl)});
|
|
}
|
|
}
|
|
|
|
void buildRivers(const Planet& p, float radius,
|
|
std::vector<Vector3>& rivers, std::vector<Vector3>& bigRivers) {
|
|
rivers.clear(); bigRivers.clear();
|
|
const std::vector<double>& disc = p.discharge();
|
|
const std::vector<int>& flow = p.flowTo();
|
|
if (disc.empty() || flow.empty()) return;
|
|
const double thr = p.cfg.riverThreshold;
|
|
auto V = [&](int i) { Vec3 m = p.cells[i].unit * (double)radius; return Vector3{ (float)m.x, (float)m.y, (float)m.z }; };
|
|
for (int i = 0; i < (int)p.cells.size(); ++i) {
|
|
int d = flow[i];
|
|
if (d < 0 || disc[i] < thr) continue; // not a river / reached the sea
|
|
(disc[i] > thr * 6.0 ? bigRivers : rivers).push_back(V(i));
|
|
(disc[i] > thr * 6.0 ? bigRivers : rivers).push_back(V(d));
|
|
}
|
|
}
|
|
|
|
void buildCoastline(const Planet& p, float radius,
|
|
std::vector<Vector3>& segs, std::vector<int>& oceanCell) {
|
|
segs.clear(); oceanCell.clear();
|
|
const double sea = p.cfg.seaLevel;
|
|
auto isLand = [&](int i) { return p.cells[i].elevation > sea; };
|
|
auto midV = [&](int i, int j) -> Vector3 {
|
|
Vec3 m = ((p.cells[i].unit + p.cells[j].unit) * 0.5).normalized() * radius;
|
|
return Vector3{ (float)m.x, (float)m.y, (float)m.z };
|
|
};
|
|
// The ocean cell nearest a coast segment, for sampling tide. Prefer an ocean endpoint of
|
|
// the cut edge; fall back to the lone/other cell that is ocean.
|
|
auto emit = [&](const Vector3& a, const Vector3& b, int oc) {
|
|
segs.push_back(a); segs.push_back(b); oceanCell.push_back(oc);
|
|
};
|
|
const std::vector<int>& tri = p.triIndices();
|
|
for (size_t k = 0; k + 2 < tri.size(); k += 3) {
|
|
int ia = tri[k], ib = tri[k + 1], ic = tri[k + 2];
|
|
bool la = isLand(ia), lb = isLand(ib), lc = isLand(ic);
|
|
if (la == lb && lb == lc) continue; // all land or all ocean: no coast
|
|
// Exactly one vertex differs from the other two -> one cut separating it.
|
|
int lone, o1, o2;
|
|
if (la == lb) { lone = ic; o1 = ia; o2 = ib; }
|
|
else if (lb == lc) { lone = ia; o1 = ib; o2 = ic; }
|
|
else { lone = ib; o1 = ia; o2 = ic; }
|
|
int oc = isLand(lone) ? o1 : lone; // the ocean side of the cut
|
|
emit(midV(lone, o1), midV(lone, o2), oc);
|
|
}
|
|
}
|
|
|
|
void buildCurrents(const Planet& p, float radius,
|
|
std::vector<Vector3>& segs, std::vector<Color>& cols) {
|
|
segs.clear(); cols.clear();
|
|
const std::vector<Vec3>& cur = p.current();
|
|
if (cur.empty()) return;
|
|
const double sea = p.cfg.seaLevel;
|
|
double maxSp = 1e-9;
|
|
for (const Vec3& v : cur) maxSp = std::max(maxSp, v.length());
|
|
const Vec3 up{0, 1, 0};
|
|
const Color warm{235, 120, 90, 255}, cold{90, 160, 235, 255};
|
|
auto V = [](const Vec3& q) { return Vector3{ (float)q.x, (float)q.y, (float)q.z }; };
|
|
for (int i = 0; i < (int)p.cells.size(); i += 3) { // subsample for readability
|
|
if (p.cells[i].elevation > sea) continue;
|
|
const Vec3& vel = cur[i];
|
|
double sp = vel.length();
|
|
if (sp < 0.18 * maxSp) continue; // skip the slack water
|
|
const Vec3& nrm = p.cells[i].unit;
|
|
Vec3 dir = vel * (1.0 / sp);
|
|
double len = 0.02 + 0.03 * (sp / maxSp);
|
|
Vec3 base = nrm * (double)radius;
|
|
Vec3 tip = base + dir * len;
|
|
Vec3 perp = dir.cross(nrm).normalized();
|
|
Vec3 back = dir * -1.0; double hl = len * 0.35;
|
|
Vec3 h1 = tip + (back * 0.8 + perp * 0.6) * hl;
|
|
Vec3 h2 = tip + (back * 0.8 - perp * 0.6) * hl;
|
|
// Warm if flowing poleward (toward the nearer pole), cold if equatorward.
|
|
Vec3 northT = up - nrm * up.dot(nrm); double nl = northT.length();
|
|
double pw = 0.0;
|
|
if (nl > 1e-9) { northT = northT * (1.0 / nl); pw = dir.dot(northT) * (nrm.y >= 0 ? 1.0 : -1.0); }
|
|
Color c = pw >= 0.0 ? warm : cold;
|
|
segs.push_back(V(base)); segs.push_back(V(tip)); cols.push_back(c);
|
|
segs.push_back(V(tip)); segs.push_back(V(h1)); cols.push_back(c);
|
|
segs.push_back(V(tip)); segs.push_back(V(h2)); cols.push_back(c);
|
|
}
|
|
}
|
|
|
|
std::vector<std::vector<Vector2>> buildGraticule() {
|
|
std::vector<std::vector<Vector2>> g;
|
|
const double D = M_PI / 180.0;
|
|
for (int la = -60; la <= 60; la += 30) { // parallels
|
|
std::vector<Vector2> pl;
|
|
for (int lo = -180; lo <= 180; lo += 5) pl.push_back({ (float)(lo * D), (float)(la * D) });
|
|
g.push_back(pl);
|
|
}
|
|
for (int lo = -180; lo < 180; lo += 30) { // meridians
|
|
std::vector<Vector2> pl;
|
|
for (int la = -85; la <= 85; la += 5) pl.push_back({ (float)(lo * D), (float)(la * D) });
|
|
g.push_back(pl);
|
|
}
|
|
return g;
|
|
}
|
|
void drawGraticule3D(const std::vector<std::vector<Vector2>>& g, float radius) {
|
|
rlBegin(RL_LINES); rlColor4ub(110, 125, 150, 150);
|
|
for (const auto& pl : g)
|
|
for (size_t i = 0; i + 1 < pl.size(); ++i) {
|
|
Vec3 a = lonLatToDir(pl[i].x, pl[i].y) * (double)radius;
|
|
Vec3 b = lonLatToDir(pl[i + 1].x, pl[i + 1].y) * (double)radius;
|
|
rlVertex3f((float)a.x, (float)a.y, (float)a.z);
|
|
rlVertex3f((float)b.x, (float)b.y, (float)b.z);
|
|
}
|
|
rlEnd();
|
|
}
|
|
void drawGraticule2D(const std::vector<std::vector<Vector2>>& g, Rectangle r, double lonOffset) {
|
|
rlBegin(RL_LINES); rlColor4ub(110, 125, 150, 150);
|
|
for (const auto& pl : g)
|
|
for (size_t i = 0; i + 1 < pl.size(); ++i) {
|
|
Vector2 pa = projLonLat(pl[i].x, pl[i].y, lonOffset, r);
|
|
Vector2 pb = projLonLat(pl[i + 1].x, pl[i + 1].y, lonOffset, r);
|
|
if (fabsf(pa.x - pb.x) > r.width * 0.5f) continue;
|
|
rlVertex2f(pa.x, pa.y); rlVertex2f(pb.x, pb.y);
|
|
}
|
|
rlEnd();
|
|
}
|
|
|
|
void drawGraticuleLabels2D(Rectangle r, double lonOffset) {
|
|
const double D = M_PI / 180.0;
|
|
const Color col{ 170, 185, 205, 220 };
|
|
// Latitudes at the left edge (Equal Earth y depends only on lat).
|
|
const int lats[] = { 60, 30, 0, -30, -60 };
|
|
for (int la : lats) {
|
|
Vector2 p = projLonLat(0.0, la * D, lonOffset, r);
|
|
const char* t = (la == 0) ? "0" : TextFormat("%d%c", std::abs(la), la > 0 ? 'N' : 'S');
|
|
DrawText(t, (int)r.x + 3, (int)p.y - 6, 11, col);
|
|
}
|
|
// Longitudes along the bottom edge (skip any panned off the map).
|
|
const int lons[] = { -180, -120, -60, 0, 60, 120, 180 };
|
|
int by = (int)(r.y + r.height) - 14;
|
|
for (int lo : lons) {
|
|
Vector2 p = projLonLat(lo * D, 0.0, lonOffset, r);
|
|
if (p.x < r.x + 2 || p.x > r.x + r.width - 2) continue;
|
|
const char* t = (lo == 0) ? "0" : TextFormat("%d%c", std::abs(lo), lo > 0 ? 'E' : 'W');
|
|
DrawText(t, (int)p.x - 8, by, 11, col);
|
|
}
|
|
}
|
|
|
|
void drawSegments2D(const std::vector<Vector3>& segs, Color col, float width,
|
|
Rectangle r, double lonOffset) {
|
|
if (segs.empty()) return;
|
|
rlSetLineWidth(width); rlBegin(RL_LINES); rlColor4ub(col.r, col.g, col.b, 255);
|
|
for (size_t i = 0; i + 1 < segs.size(); i += 2) {
|
|
Vec3 a = Vec3{segs[i].x, segs[i].y, segs[i].z}.normalized();
|
|
Vec3 b = Vec3{segs[i + 1].x, segs[i + 1].y, segs[i + 1].z}.normalized();
|
|
double alo, ala, blo, bla; dirToLonLat(a, alo, ala); dirToLonLat(b, blo, bla);
|
|
Vector2 pa = projLonLat(alo, ala, lonOffset, r), pb = projLonLat(blo, bla, lonOffset, r);
|
|
if (fabsf(pa.x - pb.x) > r.width * 0.5f) continue;
|
|
rlVertex2f(pa.x, pa.y); rlVertex2f(pb.x, pb.y);
|
|
}
|
|
rlEnd(); rlSetLineWidth(1.0f);
|
|
}
|
|
|
|
void drawColoredSegments2D(const std::vector<Vector3>& segs, const std::vector<Color>& cols,
|
|
float width, Rectangle r, double lonOffset) {
|
|
if (segs.empty()) return;
|
|
rlSetLineWidth(width); rlBegin(RL_LINES);
|
|
for (size_t i = 0, c = 0; i + 1 < segs.size(); i += 2, ++c) {
|
|
Vec3 a = Vec3{segs[i].x, segs[i].y, segs[i].z}.normalized();
|
|
Vec3 b = Vec3{segs[i + 1].x, segs[i + 1].y, segs[i + 1].z}.normalized();
|
|
double alo, ala, blo, bla; dirToLonLat(a, alo, ala); dirToLonLat(b, blo, bla);
|
|
Vector2 pa = projLonLat(alo, ala, lonOffset, r), pb = projLonLat(blo, bla, lonOffset, r);
|
|
if (fabsf(pa.x - pb.x) > r.width * 0.5f) continue;
|
|
const Color& col = cols[c < cols.size() ? c : cols.size() - 1];
|
|
rlColor4ub(col.r, col.g, col.b, 255);
|
|
rlVertex2f(pa.x, pa.y); rlVertex2f(pb.x, pb.y);
|
|
}
|
|
rlEnd(); rlSetLineWidth(1.0f);
|
|
}
|
|
|
|
void drawSubgrids(const std::vector<std::shared_ptr<SubGrid>>& sgs,
|
|
float visBase, float elevExagg, double seaLevel, float eps) {
|
|
for (const auto& sg : sgs) {
|
|
if (!sg || sg->res < 2) continue;
|
|
int R = sg->res;
|
|
rlBegin(RL_TRIANGLES);
|
|
for (int j = 0; j < R - 1; ++j)
|
|
for (int i = 0; i < R - 1; ++i) {
|
|
const SubCell* q[4] = {
|
|
&sg->sub[(size_t)j * R + i], &sg->sub[(size_t)j * R + i + 1],
|
|
&sg->sub[(size_t)(j + 1) * R + i + 1], &sg->sub[(size_t)(j + 1) * R + i]
|
|
};
|
|
const int order[6] = { 0, 1, 2, 0, 2, 3 };
|
|
for (int o : order) {
|
|
const SubCell* s = q[o];
|
|
Color col = elevationColor(s->elevation, seaLevel);
|
|
float rr = visBase + (float)s->elevation * elevExagg + eps;
|
|
rlColor4ub(col.r, col.g, col.b, 255);
|
|
rlVertex3f((float)(s->unit.x * rr), (float)(s->unit.y * rr), (float)(s->unit.z * rr));
|
|
}
|
|
}
|
|
rlEnd();
|
|
}
|
|
}
|