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>
991 lines
59 KiB
C++
991 lines
59 KiB
C++
#include "Viewer.hpp"
|
|
#include "Overlays.hpp"
|
|
#include "Map2D.hpp"
|
|
#include "Panels.hpp"
|
|
#include "Picking.hpp" // rotateZ (axial-tilt transform for labels)
|
|
#include "rlgl.h"
|
|
#include "Projection.hpp" // dirToLonLat (plate labels)
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <vector>
|
|
|
|
// Tint a settlement marker by its live environmental condition (1 = thriving, <1 = hardship/drought):
|
|
// blend toward a dull withered brown-red and darken as conditions worsen.
|
|
static Color witherColor(Color base, double cond) {
|
|
double h = std::clamp((0.8 - cond) / 0.6, 0.0, 1.0); // 0 above 0.8 .. 1 at/below 0.2
|
|
if (h <= 0.0) return base;
|
|
const unsigned char w[3] = { 130, 80, 70 }; // withered brown-red
|
|
double dim = 1.0 - 0.35 * h;
|
|
auto L = [&](unsigned char b, unsigned char wc) { return (unsigned char)std::clamp((b * (1.0 - h) + wc * h) * dim, 0.0, 255.0); };
|
|
return Color{ L(base.r, w[0]), L(base.g, w[1]), L(base.b, w[2]), base.a };
|
|
}
|
|
|
|
// Label style for a geographic feature: font size + colour, returns true if it's a "minor" feature
|
|
// (peaks/rivers/lakes/seas/small islands) -- those are drawn only when zoomed in, to declutter.
|
|
static bool labelStyle(const GeoFeature& f, int& font, Color& col) {
|
|
const Color land{215, 220, 235, 255}, water{120, 195, 230, 255},
|
|
river{110, 175, 235, 255}, mtn{220, 195, 150, 255};
|
|
switch (f.kind) {
|
|
case FeatureKind::Continent: font = 20; col = land; return false;
|
|
case FeatureKind::Ocean: font = 18; col = water; return false;
|
|
case FeatureKind::Sea: font = 15; col = water; return true;
|
|
case FeatureKind::Island: font = f.size >= 8 ? 15 : 13; col = land; return f.size < 8;
|
|
case FeatureKind::MountainRange: font = 15; col = mtn; return false;
|
|
case FeatureKind::Peak: font = 13; col = mtn; return true;
|
|
case FeatureKind::River: font = 13; col = river; return true;
|
|
case FeatureKind::Lake: font = 13; col = water; return true;
|
|
}
|
|
font = 13; col = land; return true;
|
|
}
|
|
|
|
// Render the 3D globe into its own RenderTexture (its viewport != the screen).
|
|
void Viewer::renderGlobe3D() {
|
|
BeginTextureMode(rt3d);
|
|
ClearBackground(Color{8, 10, 16, 255});
|
|
BeginMode3D(cam);
|
|
// Axial tilt: lean the whole globe (and everything drawn over it) by the
|
|
// obliquity about the world Z axis. Picking + plate labels rotate to match
|
|
// (see ViewerInput / renderFrame). The picking sphere is rotation-invariant.
|
|
rlPushMatrix();
|
|
rlRotatef((float)planet.cfg.axialTilt, 0.0f, 0.0f, 1.0f);
|
|
const std::vector<int>& tri = planet.triIndices();
|
|
const std::vector<Color>& dc = displayColors(); // live overlay (day/night + snow) or plain
|
|
rlBegin(RL_TRIANGLES);
|
|
for (size_t k = 0; k + 2 < tri.size(); k += 3) {
|
|
int idx[3] = { tri[k], tri[k + 1], tri[k + 2] };
|
|
for (int j = 0; j < 3; ++j) {
|
|
const Cell& cc = planet.cells[idx[j]];
|
|
const Vec3& u = cc.unit;
|
|
float r = visBase + (float)cc.elevation * elevExagg;
|
|
const Color& col = dc[idx[j]];
|
|
rlColor4ub(col.r, col.g, col.b, 255);
|
|
rlVertex3f((float)(u.x * r), (float)(u.y * r), (float)(u.z * r));
|
|
}
|
|
}
|
|
rlEnd();
|
|
|
|
// (The clicked tile's high-res subgrid is shown in the right-side detail panel,
|
|
// not overlaid on the globe -- the overlay was a low-res, always-elevation-coloured
|
|
// patch that clashed with the active colour mode and read as a "strange pattern".)
|
|
|
|
if (showBorders && (!borders.empty() || !ridgeBorders.empty())) {
|
|
rlSetLineWidth(2.0f); rlBegin(RL_LINES);
|
|
rlColor4ub(255, 235, 90, 255); // real plate borders: yellow
|
|
for (size_t i = 0; i + 1 < borders.size(); i += 2) {
|
|
rlVertex3f(borders[i].x, borders[i].y, borders[i].z);
|
|
rlVertex3f(borders[i + 1].x, borders[i + 1].y, borders[i + 1].z);
|
|
}
|
|
rlColor4ub(220, 70, 60, 255); // young spreading ridges: red
|
|
for (size_t i = 0; i + 1 < ridgeBorders.size(); i += 2) {
|
|
rlVertex3f(ridgeBorders[i].x, ridgeBorders[i].y, ridgeBorders[i].z);
|
|
rlVertex3f(ridgeBorders[i + 1].x, ridgeBorders[i + 1].y, ridgeBorders[i + 1].z);
|
|
}
|
|
rlEnd(); rlSetLineWidth(1.0f);
|
|
}
|
|
if (showNationBorders && !nationBorders.empty()) { // political / realm borders (dark, over the tint)
|
|
rlSetLineWidth(2.5f); rlBegin(RL_LINES); rlColor4ub(18, 18, 26, 235);
|
|
for (size_t i = 0; i + 1 < nationBorders.size(); i += 2) {
|
|
rlVertex3f(nationBorders[i].x, nationBorders[i].y, nationBorders[i].z);
|
|
rlVertex3f(nationBorders[i + 1].x, nationBorders[i + 1].y, nationBorders[i + 1].z);
|
|
}
|
|
rlEnd(); rlSetLineWidth(1.0f);
|
|
}
|
|
if (showCultureBorders && !cultureBorders.empty()) { // cultural-region borders (pale, distinct from political)
|
|
rlSetLineWidth(3.0f); rlBegin(RL_LINES); rlColor4ub(245, 240, 220, 220);
|
|
for (size_t i = 0; i + 1 < cultureBorders.size(); i += 2) {
|
|
rlVertex3f(cultureBorders[i].x, cultureBorders[i].y, cultureBorders[i].z);
|
|
rlVertex3f(cultureBorders[i + 1].x, cultureBorders[i + 1].y, cultureBorders[i + 1].z);
|
|
}
|
|
rlEnd(); rlSetLineWidth(1.0f);
|
|
}
|
|
if (showDrift && !driftArrows.empty()) {
|
|
rlSetLineWidth(2.5f); rlBegin(RL_LINES); rlColor4ub(90, 230, 255, 255);
|
|
for (size_t i = 0; i + 1 < driftArrows.size(); i += 2) {
|
|
rlVertex3f(driftArrows[i].x, driftArrows[i].y, driftArrows[i].z);
|
|
rlVertex3f(driftArrows[i + 1].x, driftArrows[i + 1].y, driftArrows[i + 1].z);
|
|
}
|
|
rlEnd(); rlSetLineWidth(1.0f);
|
|
}
|
|
if (phase3 && showRivers) { // Phase-3 river network
|
|
auto drawRiv = [&](const std::vector<Vector3>& segs, float w) {
|
|
if (segs.empty()) return;
|
|
rlSetLineWidth(w); rlBegin(RL_LINES); rlColor4ub(80, 170, 235, 255);
|
|
for (size_t i = 0; i + 1 < segs.size(); i += 2) {
|
|
rlVertex3f(segs[i].x, segs[i].y, segs[i].z);
|
|
rlVertex3f(segs[i + 1].x, segs[i + 1].y, segs[i + 1].z);
|
|
}
|
|
rlEnd(); rlSetLineWidth(1.0f);
|
|
};
|
|
drawRiv(rivers, 1.5f); drawRiv(bigRivers, 3.0f);
|
|
}
|
|
// Live World tide: colour the coastline by the local tide level (per-segment colour cached
|
|
// in coastCols so the 2D map reuses it). Auto-scaled to the current tide extent.
|
|
coastCols.clear();
|
|
if (liveWorld && showTides && !coast.empty()) {
|
|
const std::vector<double>& td = planet.tide();
|
|
double range = 1e-6;
|
|
for (int oc : coastOcean) if (oc >= 0 && oc < (int)td.size()) range = std::max(range, std::fabs(td[oc]));
|
|
coastCols.reserve(coastOcean.size());
|
|
for (int oc : coastOcean)
|
|
coastCols.push_back((oc >= 0 && oc < (int)td.size()) ? tideColor(td[oc], range) : Color{150,175,185,255});
|
|
rlSetLineWidth(3.0f); rlBegin(RL_LINES);
|
|
for (size_t i = 0, c = 0; i + 1 < coast.size(); i += 2, ++c) {
|
|
const Color& col = coastCols[c];
|
|
rlColor4ub(col.r, col.g, col.b, 255);
|
|
rlVertex3f(coast[i].x, coast[i].y, coast[i].z);
|
|
rlVertex3f(coast[i + 1].x, coast[i + 1].y, coast[i + 1].z);
|
|
}
|
|
rlEnd(); rlSetLineWidth(1.0f);
|
|
}
|
|
// Ocean currents: warm/cold arrows over the sea (per-segment colour).
|
|
if (showCurrents && !currentSegs.empty()) {
|
|
rlSetLineWidth(2.0f); rlBegin(RL_LINES);
|
|
for (size_t i = 0, c = 0; i + 1 < currentSegs.size(); i += 2, ++c) {
|
|
const Color& col = currentCols[c];
|
|
rlColor4ub(col.r, col.g, col.b, 255);
|
|
rlVertex3f(currentSegs[i].x, currentSegs[i].y, currentSegs[i].z);
|
|
rlVertex3f(currentSegs[i + 1].x, currentSegs[i + 1].y, currentSegs[i + 1].z);
|
|
}
|
|
rlEnd(); rlSetLineWidth(1.0f);
|
|
}
|
|
// Live World weather: a translucent cloud shell over the globe (white -> dark storm where it
|
|
// rains), alpha = cloud cover. Drawn as a second triangle layer just above the terrain.
|
|
if (liveWorld && showClouds && !planet.cloud().empty()) {
|
|
const std::vector<double>& cl = planet.cloud();
|
|
const std::vector<double>& rn = planet.rain();
|
|
double maxR = 1e-6; for (double r : rn) maxR = std::max(maxR, r);
|
|
const std::vector<int>& ctri = planet.triIndices();
|
|
const float cr = visBase + 0.03f;
|
|
rlBegin(RL_TRIANGLES);
|
|
for (size_t k = 0; k + 2 < ctri.size(); k += 3) {
|
|
for (int j = 0; j < 3; ++j) {
|
|
int idx = ctri[k + j];
|
|
double c = std::clamp(cl[idx], 0.0, 1.0);
|
|
double rain01 = std::clamp(rn[idx] / maxR, 0.0, 1.0);
|
|
unsigned char R = (unsigned char)(245 - 150 * rain01); // white -> slate
|
|
unsigned char G = (unsigned char)(245 - 130 * rain01);
|
|
unsigned char B = (unsigned char)(250 - 95 * rain01);
|
|
unsigned char A = (unsigned char)(std::clamp(c, 0.0, 1.0) * 205.0);
|
|
const Vec3& u = planet.cells[idx].unit;
|
|
rlColor4ub(R, G, B, A);
|
|
rlVertex3f((float)(u.x * cr), (float)(u.y * cr), (float)(u.z * cr));
|
|
}
|
|
}
|
|
rlEnd();
|
|
}
|
|
// Live World storm markers: an animated cyclonic spiral per weather system (hurricanes red
|
|
// with an eye; lows blue), spinning with the live clock by the system's hemisphere sense.
|
|
if (liveWorld && showClouds && !planet.storms().empty()) {
|
|
const float SR = visBase + 0.05f;
|
|
for (const auto& ws : planet.storms()) {
|
|
Vec3 p{ ws.pos.x, ws.pos.y, ws.pos.z };
|
|
Vec3 u = p.cross(Vec3{0, 1, 0}); if (u.length() < 1e-6) u = p.cross(Vec3{1, 0, 0});
|
|
u = u.normalized(); Vec3 v = p.cross(u).normalized();
|
|
bool hur = ws.tropical && ws.strength >= planet.cfg.weatherHurricaneStr;
|
|
unsigned char cR = hur ? 240 : 150, cG = hur ? 60 : 200, cB = hur ? 60 : 235;
|
|
unsigned char A = (unsigned char)(110 + 140 * std::clamp(ws.strength, 0.0, 1.0));
|
|
double rmax = 0.04 + 0.10 * ws.strength;
|
|
double phase = liveTime * ws.spin * 0.4;
|
|
rlSetLineWidth(2.0f); rlBegin(RL_LINES); rlColor4ub(cR, cG, cB, A);
|
|
const int N = 36; const double turns = 2.2;
|
|
for (int arm = 0; arm < 2; ++arm) {
|
|
double a0 = phase + arm * M_PI; Vec3 prev{};
|
|
for (int k = 0; k <= N; ++k) {
|
|
double t = (double)k / N;
|
|
double a = a0 + t * turns * 2.0 * M_PI * ws.spin;
|
|
Vec3 dir = u * std::cos(a) + v * std::sin(a);
|
|
Vec3 wp = (p + dir * (rmax * t)).normalized() * (double)SR;
|
|
if (k > 0) { rlVertex3f((float)prev.x, (float)prev.y, (float)prev.z);
|
|
rlVertex3f((float)wp.x, (float)wp.y, (float)wp.z); }
|
|
prev = wp;
|
|
}
|
|
}
|
|
rlEnd(); rlSetLineWidth(1.0f);
|
|
if (hur) { Vec3 e = p * (double)SR; DrawSphere(Vector3{(float)e.x,(float)e.y,(float)e.z}, 0.02f, Color{255,240,200,255}); }
|
|
}
|
|
}
|
|
// Live World volcano markers: growing vents glow red/orange, dormant vents go quiet/grey,
|
|
// and post-explosion ash vents flare bright. Inside the tilted matrix, so it tracks the globe.
|
|
if (liveWorld && showVolcanoes && !planet.volcanoes.empty()) {
|
|
const double maxH = std::max(1.0, planet.cfg.volcanoMaxHeight);
|
|
for (const Volcano& vc : planet.volcanoes) {
|
|
if (vc.cell < 0 || vc.cell >= (int)planet.cells.size()) continue;
|
|
const Cell& c = planet.cells[vc.cell];
|
|
Vec3 u = c.unit;
|
|
float r = visBase + (float)c.elevation * elevExagg;
|
|
double bf = std::clamp(planet.volcanoBuilt(vc) / maxH, 0.0, 1.0);
|
|
double er = planet.volcanoErupting(vc);
|
|
float coneH = 0.022f + 0.045f * (float)bf;
|
|
float coneR = 0.015f + 0.018f * (float)bf;
|
|
Vector3 b { (float)(u.x * r), (float)(u.y * r), (float)(u.z * r) };
|
|
Vector3 apex{ (float)(u.x * (r + coneH)), (float)(u.y * (r + coneH)), (float)(u.z * (r + coneH)) };
|
|
bool dormant = vc.phase == 1;
|
|
Color cone = dormant ? Color{105, 100, 95, 255}
|
|
: Color{ (unsigned char)(115 + 95 * er), (unsigned char)(65 + 20 * bf), 45, 255 };
|
|
DrawCylinderEx(b, apex, coneR, coneR * 0.25f, 8, cone);
|
|
if (er > 0.12 && !dormant) {
|
|
unsigned char a = (unsigned char)std::clamp(60.0 + 195.0 * er, 0.0, 255.0);
|
|
Color glow = vc.ashTimer > 0.0 ? Color{255, 210, 95, a} : Color{255, 140, 40, a};
|
|
DrawSphere(apex, 0.02f + 0.05f * (float)er, glow);
|
|
float ph = coneH + (vc.ashTimer > 0.0 ? 0.20f : 0.12f) * (float)er;
|
|
Vector3 top{ (float)(u.x * (r + ph)), (float)(u.y * (r + ph)), (float)(u.z * (r + ph)) };
|
|
rlSetLineWidth(2.0f); rlBegin(RL_LINES); rlColor4ub(255, 180, 80, a);
|
|
rlVertex3f(apex.x, apex.y, apex.z); rlVertex3f(top.x, top.y, top.z);
|
|
rlEnd(); rlSetLineWidth(1.0f);
|
|
}
|
|
}
|
|
}
|
|
// Settlement markers (civilization): a dot per settlement, sized + coloured by tier; dim for ruins.
|
|
if (showSettlements && !planet.settlements.empty()) {
|
|
const double townP = planet.cfg.civTownPop, cityP = planet.cfg.civCityPop, abP = planet.cfg.civAbandonPop;
|
|
const auto& cond = planet.settlementCondition();
|
|
for (size_t k = 0; k < planet.settlements.size(); ++k) {
|
|
const Settlement& s = planet.settlements[k];
|
|
if (s.cell < 0 || s.cell >= (int)planet.cells.size()) continue;
|
|
const Cell& c = planet.cells[s.cell];
|
|
float r = visBase + (float)c.elevation * elevExagg + 0.006f;
|
|
Vector3 p{ (float)(c.unit.x * r), (float)(c.unit.y * r), (float)(c.unit.z * r) };
|
|
bool alive = s.population >= abP;
|
|
SettleTier t = settleTierOf(s.population, townP, cityP);
|
|
float rad = t == SettleTier::City ? 0.026f : t == SettleTier::Town ? 0.018f : 0.012f;
|
|
Color col = !alive ? Color{110, 110, 116, 255}
|
|
: t == SettleTier::City ? Color{250, 220, 110, 255}
|
|
: t == SettleTier::Town ? Color{225, 170, 90, 255}
|
|
: Color{210, 130, 85, 255};
|
|
col = witherColor(col, k < cond.size() ? cond[k] : 1.0); // hardship -> withered tint
|
|
DrawSphere(p, rad, col);
|
|
if (alive && t != SettleTier::Village) { // a ring marks notable settlements
|
|
float rr = visBase + (float)c.elevation * elevExagg + 0.01f;
|
|
Vector3 e = { (float)(c.unit.x * rr), (float)(c.unit.y * rr), (float)(c.unit.z * rr) };
|
|
DrawSphereWires(e, rad + 0.008f, 6, 6, Color{255, 245, 210, 150});
|
|
}
|
|
}
|
|
}
|
|
if (showGrat) drawGraticule3D(graticule, gratR);
|
|
// Markers: selected (orange), hovered cell (yellow), hovered subcell (white).
|
|
if (selectedCell >= 0) {
|
|
Vec3 u = planet.cells[selectedCell].unit * (double)(visBase + 0.012f);
|
|
DrawSphere(Vector3{(float)u.x, (float)u.y, (float)u.z}, 0.03f, ORANGE);
|
|
}
|
|
if (hovered >= 0 && !hasHoverSub) {
|
|
Vec3 u = planet.cells[hovered].unit * (double)(visBase + 0.012f);
|
|
DrawSphere(Vector3{(float)u.x, (float)u.y, (float)u.z}, 0.022f, YELLOW);
|
|
}
|
|
if (hasHoverSub) {
|
|
Vec3 u = hoverSub.unit * (double)(visBase + 0.02f);
|
|
DrawSphere(Vector3{(float)u.x, (float)u.y, (float)u.z}, 0.012f, WHITE);
|
|
}
|
|
// Spin axis: a rod through the poles, extended beyond the surface (tilts with
|
|
// the globe since it's inside the rotated matrix). Pole caps mark N (red)/S (blue).
|
|
{
|
|
float ax = visBase + 0.6f;
|
|
rlSetLineWidth(2.5f);
|
|
rlBegin(RL_LINES); rlColor4ub(210, 220, 235, 255);
|
|
rlVertex3f(0.0f, -ax, 0.0f); rlVertex3f(0.0f, ax, 0.0f);
|
|
rlEnd(); rlSetLineWidth(1.0f);
|
|
DrawSphere(Vector3{0.0f, ax, 0.0f}, 0.05f, Color{230, 90, 80, 255}); // north
|
|
DrawSphere(Vector3{0.0f, -ax, 0.0f}, 0.05f, Color{80, 140, 230, 255}); // south
|
|
}
|
|
// Live World sky: a small, distant sun (bright core + faint halo) and the orbiting moons
|
|
// (sun-lit phase + orbit ring; dimmed reddish during a lunar eclipse). All inside the tilted
|
|
// matrix so they stay consistent with the model-space lit pattern.
|
|
if (liveWorld) {
|
|
// Sun: far away + small, with a couple of translucent halo shells so it still reads.
|
|
const float sunDist = 9.0f;
|
|
Vector3 sp{ sunDir.x * sunDist, sunDir.y * sunDist, sunDir.z * sunDist };
|
|
DrawSphere(sp, 0.60f, Color{255, 240, 180, 26});
|
|
DrawSphere(sp, 0.34f, Color{255, 238, 170, 55});
|
|
DrawSphere(sp, 0.17f, Color{255, 246, 205, 255});
|
|
|
|
const auto& mns = planet.getMoons();
|
|
for (size_t m = 0; m < mns.size() && m < moonDirs.size(); ++m) {
|
|
const Vector3& dir = moonDirs[m];
|
|
float dist = visBase + 0.8f + (float)(mns[m].orbitRadius / 30.0) * 4.0f; // visible band
|
|
Vector3 mp{ dir.x * dist, dir.y * dist, dir.z * dist };
|
|
float rr = (float)mns[m].dispRadius;
|
|
|
|
// Faint orbit ring: the great circle perpendicular to the orbit-plane normal.
|
|
if (m < moonNormals.size()) {
|
|
Vec3 nrm = Vec3{moonNormals[m].x, moonNormals[m].y, moonNormals[m].z}.normalized();
|
|
Vec3 u = std::fabs(nrm.y) < 0.9 ? nrm.cross(Vec3{0,1,0}).normalized()
|
|
: nrm.cross(Vec3{1,0,0}).normalized();
|
|
Vec3 v = nrm.cross(u);
|
|
rlBegin(RL_LINES); rlColor4ub(120, 130, 160, 90);
|
|
const int seg = 64;
|
|
for (int k = 0; k < seg; ++k) {
|
|
double a0 = 2.0 * M_PI * k / seg, a1 = 2.0 * M_PI * (k + 1) / seg;
|
|
Vec3 p0 = (u * std::cos(a0) + v * std::sin(a0)) * dist;
|
|
Vec3 p1 = (u * std::cos(a1) + v * std::sin(a1)) * dist;
|
|
rlVertex3f((float)p0.x, (float)p0.y, (float)p0.z);
|
|
rlVertex3f((float)p1.x, (float)p1.y, (float)p1.z);
|
|
}
|
|
rlEnd();
|
|
}
|
|
|
|
// Lunar eclipse: moon near the anti-solar point (in the planet's shadow) -> dim red.
|
|
double antiAlign = -(dir.x*sunDir.x + dir.y*sunDir.y + dir.z*sunDir.z); // dot(dir,-sun)
|
|
bool eclipsed = antiAlign > std::cos(0.13);
|
|
Color lit = eclipsed ? Color{90, 35, 30, 255} : Color{210, 210, 215, 255};
|
|
DrawSphere(mp, rr, lit);
|
|
// Phase via the offset-dark-sphere trick: lit fraction k = (1 - cos(phase))/2, with
|
|
// cos(phase)=dot(moonDir,sunDir) (new moon when aligned with the sun). Shift a dark
|
|
// sphere toward the unlit (anti-sun) side to occlude it; offset 0 = new, ~2r = full.
|
|
double cosPhase = dir.x*sunDir.x + dir.y*sunDir.y + dir.z*sunDir.z;
|
|
double k = (1.0 - cosPhase) * 0.5; // 0 = new, 1 = full
|
|
float off = (float)(k * 2.2 * rr);
|
|
Vector3 dp{ mp.x - sunDir.x * off, mp.y - sunDir.y * off, mp.z - sunDir.z * off };
|
|
DrawSphere(dp, rr * 1.02f, Color{12, 12, 16, 255});
|
|
}
|
|
}
|
|
rlPopMatrix();
|
|
EndMode3D();
|
|
EndTextureMode();
|
|
}
|
|
|
|
// 2D Equal Earth map + its overlays (borders/drift/rivers/labels/markers).
|
|
void Viewer::renderMap2D() {
|
|
DrawRectangleRec(mapRect, Color{6, 8, 14, 255});
|
|
Rectangle vr = mapViewRect(); // projection rect (zoom/pan); scissor stays mapRect
|
|
BeginScissorMode((int)mapRect.x, (int)mapRect.y, (int)mapRect.width, (int)mapRect.height);
|
|
drawMap2D(planet, displayColors(), map2D, vr, mapLon);
|
|
if (showGrat) { drawGraticule2D(graticule, vr, mapLon); drawGraticuleLabels2D(vr, mapLon); }
|
|
if (showBorders && !borders.empty()) drawSegments2D(borders, Color{255, 235, 90, 255}, 2.0f, vr, mapLon);
|
|
if (showBorders && !ridgeBorders.empty()) drawSegments2D(ridgeBorders, Color{220, 70, 60, 255}, 2.0f, vr, mapLon);
|
|
if (showNationBorders && !nationBorders.empty()) drawSegments2D(nationBorders, Color{18, 18, 26, 235}, 2.0f, vr, mapLon);
|
|
if (showCultureBorders && !cultureBorders.empty()) drawSegments2D(cultureBorders, Color{245, 240, 220, 230}, 2.5f, vr, mapLon);
|
|
if (showDrift && !driftArrows.empty()) drawSegments2D(driftArrows, Color{90, 230, 255, 255}, 2.0f, vr, mapLon);
|
|
if (liveWorld && showTides && !coastCols.empty()) drawColoredSegments2D(coast, coastCols, 2.0f, vr, mapLon);
|
|
if (showCurrents && !currentCols.empty()) drawColoredSegments2D(currentSegs, currentCols, 1.6f, vr, mapLon);
|
|
if (liveWorld && showClouds && !planet.cloud().empty()) drawWeather2D(planet, planet.cloud(), planet.rain(), map2D, vr, mapLon);
|
|
if (liveWorld && showClouds && !planet.storms().empty()) {
|
|
for (const auto& ws : planet.storms()) {
|
|
double lon, lat; dirToLonLat(Vec3{ws.pos.x, ws.pos.y, ws.pos.z}, lon, lat);
|
|
Vector2 sp = projLonLat(lon, lat, mapLon, vr);
|
|
bool hur = ws.tropical && ws.strength >= planet.cfg.weatherHurricaneStr;
|
|
Color c = hur ? Color{240, 60, 60, 255} : Color{150, 200, 235, 255};
|
|
float rad = (5.0f + 10.0f * (float)ws.strength) * (float)std::min(2.0, mapZoom);
|
|
DrawCircleLines((int)sp.x, (int)sp.y, rad, c);
|
|
if (hur) DrawCircleLines((int)sp.x, (int)sp.y, rad * 0.55f, c);
|
|
DrawCircleV(sp, 2.0f, c);
|
|
}
|
|
}
|
|
if (liveWorld && showVolcanoes && !planet.volcanoes.empty()) {
|
|
for (const Volcano& vc : planet.volcanoes) {
|
|
if (vc.cell < 0 || vc.cell >= (int)planet.cells.size()) continue;
|
|
double er = planet.volcanoErupting(vc);
|
|
double lon, lat; dirToLonLat(planet.cells[vc.cell].unit, lon, lat);
|
|
Vector2 sp = projLonLat(lon, lat, mapLon, vr);
|
|
float s = (5.0f + 3.0f * (float)er) * (float)std::min(2.0, mapZoom);
|
|
Color tri = vc.phase == 1 ? Color{125, 120, 115, 255}
|
|
: vc.ashTimer > 0.0 ? Color{245, 170, 55, 255}
|
|
: Color{170, 75, 50, 255};
|
|
DrawPoly(sp, 3, s, -90.0f, tri); // filled up-pointing triangle (cone)
|
|
if (er > 0.12 && vc.phase != 1)
|
|
DrawCircleLines((int)sp.x, (int)sp.y, s + 3.0f,
|
|
Color{255, 170, 70, (unsigned char)std::clamp(90.0 + 150.0 * er, 0.0, 255.0)});
|
|
}
|
|
}
|
|
if (showSettlements && !planet.settlements.empty()) {
|
|
const double townP = planet.cfg.civTownPop, cityP = planet.cfg.civCityPop, abP = planet.cfg.civAbandonPop;
|
|
const auto& cond = planet.settlementCondition();
|
|
float zf = (float)std::min(2.0, mapZoom);
|
|
for (size_t k = 0; k < planet.settlements.size(); ++k) {
|
|
const Settlement& s = planet.settlements[k];
|
|
if (s.cell < 0 || s.cell >= (int)planet.cells.size()) continue;
|
|
double lon, lat; dirToLonLat(planet.cells[s.cell].unit, lon, lat);
|
|
Vector2 sp = projLonLat(lon, lat, mapLon, vr);
|
|
bool alive = s.population >= abP;
|
|
SettleTier t = settleTierOf(s.population, townP, cityP);
|
|
float rad = (t == SettleTier::City ? 4.5f : t == SettleTier::Town ? 3.2f : 2.2f) * zf;
|
|
Color col = !alive ? Color{120, 120, 126, 255}
|
|
: t == SettleTier::City ? Color{250, 220, 110, 255}
|
|
: t == SettleTier::Town ? Color{225, 170, 90, 255}
|
|
: Color{210, 130, 85, 255};
|
|
col = witherColor(col, k < cond.size() ? cond[k] : 1.0);
|
|
DrawCircleV(sp, rad, col);
|
|
if (alive && t != SettleTier::Village) DrawCircleLines((int)sp.x, (int)sp.y, rad + 2.0f, Color{255, 245, 210, 180});
|
|
}
|
|
}
|
|
if (phase3 && showRivers) {
|
|
drawSegments2D(rivers, Color{80, 170, 235, 255}, 1.5f, vr, mapLon);
|
|
drawSegments2D(bigRivers, Color{80, 170, 235, 255}, 3.0f, vr, mapLon);
|
|
}
|
|
if (showDrift && !plateLabels.empty()) {
|
|
for (const auto& lbl : plateLabels) {
|
|
Vec3 u = Vec3{lbl.pos.x, lbl.pos.y, lbl.pos.z}.normalized();
|
|
double lon, lat; dirToLonLat(u, lon, lat);
|
|
Vector2 lp = projLonLat(lon, lat, mapLon, vr);
|
|
const char* txt = TextFormat("P%d", lbl.id);
|
|
DrawText(txt, (int)lp.x + 4, (int)lp.y - 8, 12, RAYWHITE);
|
|
}
|
|
}
|
|
// Place-name labels (the atlas). Minor features only when the map is zoomed in.
|
|
if (showNames && planet.geographyBuilt()) {
|
|
bool zoomed = mapZoom > 1.5;
|
|
for (const GeoFeature& f : planet.geography()) {
|
|
if (f.anchorCell < 0 || f.anchorCell >= (int)planet.cells.size()) continue;
|
|
int font; Color col; bool minor = labelStyle(f, font, col);
|
|
if (font <= 0 || (minor && !zoomed)) continue;
|
|
double lon, lat; dirToLonLat(planet.cells[f.anchorCell].unit, lon, lat);
|
|
Vector2 lp = projLonLat(lon, lat, mapLon, vr);
|
|
if (!CheckCollisionPointRec(lp, mapRect)) continue;
|
|
int w = MeasureText(f.name.c_str(), font);
|
|
DrawText(f.name.c_str(), (int)lp.x - w / 2 + 1, (int)lp.y - font / 2 + 1, font, Color{0, 0, 0, 180});
|
|
DrawText(f.name.c_str(), (int)lp.x - w / 2, (int)lp.y - font / 2, font, col);
|
|
}
|
|
}
|
|
if (selectedCell >= 0) DrawCircleV(mapScreen(map2D, selectedCell, vr, mapLon), 5, ORANGE);
|
|
if (hovered >= 0) DrawCircleV(mapScreen(map2D, hovered, vr, mapLon), 4, YELLOW);
|
|
EndScissorMode();
|
|
DrawRectangleLinesEx(mapRect, 1, Color{90, 90, 110, 255});
|
|
DrawText(mapZoom > 1.0 ? TextFormat("2D Equal Earth (zoom %.1fx, drag to pan, wheel to zoom)", mapZoom)
|
|
: "2D Equal Earth (hover, drag to pan, wheel to zoom)",
|
|
(int)mapRect.x + 6, (int)mapRect.y + 4, 14, Color{200, 200, 210, 255});
|
|
}
|
|
|
|
// Live World tabbed info panel in the freed space right of the (left-aligned) 2D map.
|
|
void Viewer::renderLiveInfo() {
|
|
liveInfoTabRects.clear(); eventRowRects.clear(); eventRowIndices.clear(); atlasRowCells.clear();
|
|
if (!liveWorld) return;
|
|
Rectangle r = liveInfoRect;
|
|
DrawRectangleRec(r, Color{10, 12, 20, 235});
|
|
DrawRectangleLinesEx(r, 1, Color{90, 90, 110, 255});
|
|
int x = (int)r.x + 14, y = (int)r.y + 10;
|
|
DrawText("Live info", x, y, 20, RAYWHITE);
|
|
const char* tabs[9] = { "Sky", "Tides", "Weather", "Events", "Atlas", "Eco", "Civ", "Realms", "Culture" };
|
|
float tx = r.x + 10.0f, ty = r.y + 38.0f;
|
|
for (int i = 0; i < 9; ++i) {
|
|
float tw = (r.width - 20.0f) / 9.0f;
|
|
Rectangle tr{ tx + i * tw, ty, tw - 4.0f, 24.0f };
|
|
liveInfoTabRects.push_back(tr);
|
|
bool on = liveInfoTab == i;
|
|
DrawRectangleRec(tr, on ? Color{42, 48, 68, 255} : Color{18, 22, 34, 255});
|
|
DrawRectangleLinesEx(tr, 1, on ? Color{125, 145, 190, 255} : Color{65, 70, 90, 255});
|
|
int tfs = 11; // smaller font: 9 tabs are narrow
|
|
int w = MeasureText(tabs[i], tfs);
|
|
DrawText(tabs[i], (int)(tr.x + (tr.width - w) * 0.5f), (int)tr.y + 6, tfs,
|
|
on ? RAYWHITE : Color{155, 165, 185, 255});
|
|
}
|
|
y = (int)r.y + 72;
|
|
|
|
// Sky geometry at the current clock (recomputed here so the panel is self-contained).
|
|
const double dayH = planet.cfg.dayLengthHours, yrD = planet.cfg.yearLengthDays;
|
|
double days = liveTime / dayH;
|
|
double doy = days / yrD; doy -= std::floor(doy);
|
|
double tod = days - std::floor(days);
|
|
const double dStep = 0.03; // ~ for waxing/waning + rising/falling
|
|
double days2 = days + dStep, doy2 = days2 / yrD - std::floor(days2 / yrD), tod2 = days2 - std::floor(days2);
|
|
Vec3 sun = planet.sunDirection(doy, tod);
|
|
Vec3 sun2 = planet.sunDirection(doy2, tod2);
|
|
auto illumFrac = [](const Vec3& moon, const Vec3& s) { return (1.0 - moon.dot(s)) * 0.5; };
|
|
auto phaseName = [](double f, bool wax) -> const char* {
|
|
if (f < 0.04) return "New";
|
|
if (f > 0.96) return "Full";
|
|
if (f > 0.46 && f < 0.54) return wax ? "First quarter" : "Last quarter";
|
|
if (f < 0.5) return wax ? "Waxing crescent" : "Waning crescent";
|
|
return wax ? "Waxing gibbous" : "Waning gibbous";
|
|
};
|
|
// A small 2D phase disc: dark circle with the lit fraction filled (terminator ellipse).
|
|
auto drawPhase = [](float cx, float cy, float rad, double f, bool wax) {
|
|
DrawCircle((int)cx, (int)cy, rad, Color{26, 28, 36, 255});
|
|
double cosphi = 1.0 - 2.0 * f; // terminator x = w * cosphi
|
|
for (int dy = -(int)rad; dy <= (int)rad; ++dy) {
|
|
double w = std::sqrt(std::max(0.0, (double)rad * rad - (double)dy * dy));
|
|
double xt = w * cosphi, xa, xb;
|
|
if (wax) { xa = xt; xb = w; } else { xa = -w; xb = -xt; }
|
|
if (xb > xa) DrawLine((int)(cx + xa), (int)(cy + dy), (int)(cx + xb), (int)(cy + dy), Color{226, 226, 232, 255});
|
|
}
|
|
DrawCircleLines((int)cx, (int)cy, rad, Color{120, 124, 145, 255});
|
|
};
|
|
|
|
const auto& mns = planet.getMoons();
|
|
if (liveInfoTab == 0) {
|
|
for (size_t m = 0; m < mns.size(); ++m) {
|
|
Vec3 md = planet.moonDirection((int)m, tod, days);
|
|
Vec3 md2 = planet.moonDirection((int)m, tod2, days2);
|
|
double f = illumFrac(md, sun);
|
|
bool wax = illumFrac(md2, sun2) >= f;
|
|
float cy = (float)y + 20.0f;
|
|
drawPhase((float)x + 22.0f, cy, 20.0f, f, wax);
|
|
DrawText(TextFormat("Moon %d: %s", (int)m + 1, phaseName(f, wax)), x + 52, y + 6, 17, Color{210, 215, 225, 255});
|
|
DrawText(TextFormat("%.0f%% lit period %.0f d", f * 100.0, mns[m].periodDays), x + 52, y + 27, 15, Color{150, 160, 175, 255});
|
|
y += 50;
|
|
}
|
|
if (mns.empty()) DrawText("(no moons)", x, y, 16, Color{150, 155, 170, 255});
|
|
} else if (liveInfoTab == 1) {
|
|
DrawText("Tidal phase", x, y, 18, Color{200, 205, 220, 255}); y += 28;
|
|
if (selectedCell >= 0 && selectedCell < (int)planet.cells.size()) {
|
|
const Cell& c = planet.cells[selectedCell];
|
|
const double sea = planet.cfg.seaLevel;
|
|
bool selLand = c.elevation > sea, coastal = false;
|
|
for (int nb : c.neighbors) if ((planet.cells[nb].elevation > sea) != selLand) { coastal = true; break; }
|
|
if (coastal) {
|
|
auto cellTide = [&](double dy, double td, double dd) {
|
|
double h = 0.0;
|
|
for (int mm = 0; mm < (int)mns.size(); ++mm) {
|
|
double cc = c.unit.dot(planet.moonDirection(mm, td, dd));
|
|
h += mns[mm].tideWeight * (cc * cc - 1.0 / 3.0);
|
|
}
|
|
double cs = c.unit.dot(planet.sunDirection(dy, td));
|
|
h += planet.cfg.tideSunFactor * (cs * cs - 1.0 / 3.0);
|
|
return h * planet.cfg.tideAmplitude;
|
|
};
|
|
bool rising = cellTide(doy2, tod2, days2) >= cellTide(doy, tod, days);
|
|
double lvl = ((int)planet.tide().size() == (int)planet.cells.size()) ? planet.tide()[selectedCell]
|
|
: cellTide(doy, tod, days);
|
|
DrawText(TextFormat("coastal cell #%d", selectedCell), x, y, 15, Color{160, 170, 185, 255}); y += 22;
|
|
DrawText(TextFormat("%+.2f m %s, %s", lvl, lvl >= 0.0 ? "high" : "low", rising ? "rising" : "falling"),
|
|
x, y, 17, tideColor(lvl, std::max(0.05, std::fabs(lvl)))); y += 25;
|
|
DrawText("(equilibrium model - placeholder)", x, y, 13, Color{120, 125, 140, 255});
|
|
} else DrawText("selected tile is inland", x, y, 15, Color{150, 155, 170, 255});
|
|
} else DrawText("click a coastal tile", x, y, 15, Color{150, 155, 170, 255});
|
|
} else if (liveInfoTab == 2) {
|
|
DrawText("Weather systems", x, y, 18, Color{200, 205, 220, 255}); y += 28;
|
|
const auto& storms = planet.storms();
|
|
if (storms.empty()) DrawText("(calm - none active)", x, y, 15, Color{150, 155, 170, 255});
|
|
int shown = 0;
|
|
for (const auto& ws : storms) {
|
|
if (shown >= 10 || y > (int)(r.y + r.height) - 22) break;
|
|
double lon, lat; dirToLonLat(Vec3{ws.pos.x, ws.pos.y, ws.pos.z}, lon, lat);
|
|
bool hur = ws.tropical && ws.strength >= planet.cfg.weatherHurricaneStr;
|
|
const char* kind = hur ? (lon > -0.5 && lon < 2.4 ? "Typhoon" : "Hurricane")
|
|
: ws.tropical ? "Tropical low" : "Low";
|
|
Color c = hur ? Color{240, 90, 80, 255} : Color{170, 200, 230, 255};
|
|
DrawText(TextFormat("%s %.0f%% @ %+.0f,%+.0f", kind, ws.strength * 100.0,
|
|
lat * 180.0 / M_PI, lon * 180.0 / M_PI), x, y, 15, c);
|
|
y += 21; ++shown;
|
|
}
|
|
} else if (liveInfoTab == 3) {
|
|
DrawText("World events", x, y, 18, Color{200, 205, 220, 255});
|
|
DrawText(TextFormat("%d saved", (int)events.size()), (int)(r.x + r.width) - 74, y + 2, 13, Color{145, 155, 175, 255});
|
|
y += 28;
|
|
if (events.empty()) {
|
|
DrawText("(no events yet)", x, y, 15, Color{150, 155, 170, 255});
|
|
} else {
|
|
for (int ei = (int)events.size() - 1; ei >= 0; --ei) {
|
|
if (y > (int)(r.y + r.height) - 42) break;
|
|
const WorldEvent& e = events[ei];
|
|
Rectangle row{ r.x + 8.0f, (float)y - 3.0f, r.width - 16.0f, 40.0f };
|
|
eventRowRects.push_back(row); eventRowIndices.push_back(ei);
|
|
Color bg = e.severity >= 2 ? Color{58, 30, 34, 210}
|
|
: e.severity == 1 ? Color{42, 42, 34, 205}
|
|
: Color{18, 22, 34, 205};
|
|
Color fg = e.severity >= 2 ? Color{250, 130, 95, 255}
|
|
: e.severity == 1 ? Color{230, 190, 95, 255}
|
|
: Color{175, 205, 235, 255};
|
|
DrawRectangleRec(row, bg);
|
|
DrawRectangleLinesEx(row, 1, Color{70, 75, 92, 255});
|
|
const char* icon = e.kind == 2 ? "^" : e.kind == 3 ? "*" : e.kind == 4 ? "#" : "~";
|
|
DrawText(icon, (int)row.x + 7, (int)row.y + 6, 18, fg);
|
|
double d = e.timeHours / std::max(0.1, planet.cfg.dayLengthHours);
|
|
DrawText(TextFormat("D%.1f", d), (int)row.x + 24, (int)row.y + 5, 12, Color{145, 155, 175, 255});
|
|
DrawText(e.title.c_str(), (int)row.x + 68, (int)row.y + 4, 14, fg);
|
|
DrawText(e.detail.c_str(), (int)row.x + 68, (int)row.y + 21, 12, Color{165, 170, 185, 255});
|
|
y += 43;
|
|
}
|
|
}
|
|
} else if (liveInfoTab == 4) { // Atlas: named geographic features, grouped by kind; click a row to fly there
|
|
const auto& F = planet.geography();
|
|
DrawText("Atlas", x, y, 18, Color{200, 205, 220, 255});
|
|
DrawText(TextFormat("%d named", (int)F.size()), (int)(r.x + r.width) - 78, y + 2, 13, Color{145, 155, 175, 255});
|
|
y += 26;
|
|
if (F.empty()) {
|
|
DrawText(planet.geographyBuilt() ? "(none)" : "press M to name the world", x, y, 14, Color{150, 155, 170, 255});
|
|
} else {
|
|
// Order kinds for a readable list; within a kind, largest first.
|
|
const FeatureKind order[8] = { FeatureKind::Continent, FeatureKind::Island, FeatureKind::Ocean,
|
|
FeatureKind::Sea, FeatureKind::MountainRange, FeatureKind::Peak, FeatureKind::River, FeatureKind::Lake };
|
|
auto kindColor = [](FeatureKind k) -> Color {
|
|
switch (k) {
|
|
case FeatureKind::Ocean: case FeatureKind::Sea: case FeatureKind::Lake: return Color{120, 195, 230, 255};
|
|
case FeatureKind::River: return Color{110, 175, 235, 255};
|
|
case FeatureKind::MountainRange: case FeatureKind::Peak: return Color{220, 195, 150, 255};
|
|
default: return Color{215, 220, 235, 255};
|
|
}
|
|
};
|
|
for (FeatureKind k : order) {
|
|
std::vector<int> idx;
|
|
for (int i = 0; i < (int)F.size(); ++i) if (F[i].kind == k) idx.push_back(i);
|
|
if (idx.empty()) continue;
|
|
std::sort(idx.begin(), idx.end(), [&](int a, int b){ return F[a].size > F[b].size; });
|
|
if (y > (int)(r.y + r.height) - 22) break;
|
|
DrawText(featureKindName(k), x, y, 13, Color{150, 158, 178, 255});
|
|
y += 18;
|
|
for (int i : idx) {
|
|
if (y > (int)(r.y + r.height) - 18) break;
|
|
Rectangle row{ r.x + 10.0f, (float)y - 2.0f, r.width - 20.0f, 18.0f };
|
|
eventRowRects.push_back(row); atlasRowCells.push_back(F[i].anchorCell);
|
|
DrawText(F[i].name.c_str(), (int)row.x + 8, (int)row.y + 1, 14, kindColor(k));
|
|
y += 19;
|
|
}
|
|
y += 4;
|
|
}
|
|
}
|
|
} else if (liveInfoTab == 5) { // Eco: named ecoregions, richest first; click a row to fly there
|
|
const auto& E = planet.ecoregions();
|
|
DrawText("Ecoregions", x, y, 18, Color{200, 205, 220, 255});
|
|
DrawText(TextFormat("%d named", (int)E.size()), (int)(r.x + r.width) - 78, y + 2, 13, Color{145, 155, 175, 255});
|
|
y += 26;
|
|
if (E.empty()) {
|
|
DrawText(planet.ecoregionsBuilt() ? "(none)" : "press E to name ecology", x, y, 14, Color{150, 155, 170, 255});
|
|
} else {
|
|
std::vector<int> idx(E.size());
|
|
for (size_t i = 0; i < E.size(); ++i) idx[i] = (int)i;
|
|
std::sort(idx.begin(), idx.end(), [&](int a, int b) {
|
|
double pa = std::max({ E[a].floraProductivity, E[a].faunaProductivity, E[a].fungaProductivity });
|
|
double pb = std::max({ E[b].floraProductivity, E[b].faunaProductivity, E[b].fungaProductivity });
|
|
if (std::fabs(pa - pb) > 1e-9) return pa > pb;
|
|
return E[a].size > E[b].size;
|
|
});
|
|
for (int ei : idx) {
|
|
if (y > (int)(r.y + r.height) - 34) break;
|
|
const Ecoregion& e = E[ei];
|
|
Rectangle row{ r.x + 10.0f, (float)y - 2.0f, r.width - 20.0f, 32.0f };
|
|
eventRowRects.push_back(row); atlasRowCells.push_back(e.anchorCell);
|
|
double prod = std::max({ e.floraProductivity, e.faunaProductivity, e.fungaProductivity });
|
|
DrawRectangleRec(row, Color{18, 22, 34, 205});
|
|
DrawRectangleLinesEx(row, 1, Color{70, 75, 92, 255});
|
|
DrawText(e.name.c_str(), (int)row.x + 8, (int)row.y + 2, 14, ecoregionColor(ei, e.biome, prod));
|
|
DrawText(TextFormat("%s %.0f%% life %d cells", biomeName(e.biome), prod * 100.0, e.size),
|
|
(int)row.x + 8, (int)row.y + 18, 11, Color{150, 158, 178, 255});
|
|
y += 35;
|
|
}
|
|
}
|
|
} else if (liveInfoTab == 6) { // Civ: settlements by population (largest first); click a row to fly there
|
|
const auto& S = planet.settlements;
|
|
const double townP = planet.cfg.civTownPop, cityP = planet.cfg.civCityPop, abP = planet.cfg.civAbandonPop;
|
|
DrawText("Settlements", x, y, 18, Color{200, 205, 220, 255});
|
|
DrawText(TextFormat("%d", (int)S.size()), (int)(r.x + r.width) - 40, y + 2, 13, Color{145, 155, 175, 255});
|
|
y += 26;
|
|
if (S.empty()) {
|
|
DrawText(planet.settlementsPlaced() ? "(none)" : "press U for the dawn of civilization", x, y, 13, Color{150, 155, 170, 255});
|
|
} else {
|
|
std::vector<int> idx(S.size());
|
|
for (size_t i = 0; i < S.size(); ++i) idx[i] = (int)i;
|
|
std::sort(idx.begin(), idx.end(), [&](int a, int b) { return S[a].population > S[b].population; });
|
|
for (int si : idx) {
|
|
if (y > (int)(r.y + r.height) - 22) break;
|
|
const Settlement& s = S[si];
|
|
bool alive = s.population >= abP;
|
|
SettleTier t = settleTierOf(s.population, townP, cityP);
|
|
Color fg = !alive ? Color{120, 120, 128, 255}
|
|
: t == SettleTier::City ? Color{245, 215, 110, 255}
|
|
: t == SettleTier::Town ? Color{210, 200, 150, 255}
|
|
: Color{185, 195, 175, 255};
|
|
Rectangle row{ r.x + 10.0f, (float)y - 2.0f, r.width - 20.0f, 19.0f };
|
|
eventRowRects.push_back(row); atlasRowCells.push_back(s.cell);
|
|
const char* pop = s.population >= 1.0e6 ? TextFormat("%.1fM", s.population / 1.0e6)
|
|
: s.population >= 1.0e3 ? TextFormat("%.0fk", s.population / 1.0e3)
|
|
: TextFormat("%.0f", s.population);
|
|
DrawText(s.name.c_str(), (int)row.x + 6, (int)row.y + 2, 14, fg);
|
|
const char* tag = !alive ? "ruins" : settleTierName(t);
|
|
DrawText(TextFormat("%s %s", tag, pop), (int)(r.x + r.width) - 92, (int)row.y + 3, 11, Color{150, 158, 178, 255});
|
|
y += 20;
|
|
}
|
|
}
|
|
} else if (liveInfoTab == 7) { // Realms: nations by population (largest first); click a row to fly to the capital
|
|
const auto& N = planet.nationList();
|
|
DrawText("Realms", x, y, 18, Color{200, 205, 220, 255});
|
|
DrawText(TextFormat("%d", (int)N.size()), (int)(r.x + r.width) - 40, y + 2, 13, Color{145, 155, 175, 255});
|
|
y += 26;
|
|
if (N.empty()) {
|
|
DrawText(planet.settlementsPlaced() ? "press P for the territory view" : "press U then P", x, y, 13, Color{150, 155, 170, 255});
|
|
} else {
|
|
std::vector<int> idx(N.size());
|
|
for (size_t i = 0; i < N.size(); ++i) idx[i] = (int)i;
|
|
std::sort(idx.begin(), idx.end(), [&](int a, int b) { return N[a].totalPop > N[b].totalPop; });
|
|
for (int ni : idx) {
|
|
if (y > (int)(r.y + r.height) - 22) break;
|
|
const Nation& nat = N[ni];
|
|
int cap = (nat.capital >= 0 && nat.capital < (int)planet.settlements.size()) ? planet.settlements[nat.capital].cell : -1;
|
|
Rectangle row{ r.x + 10.0f, (float)y - 2.0f, r.width - 20.0f, 19.0f };
|
|
eventRowRects.push_back(row); atlasRowCells.push_back(cap);
|
|
Color fg = nat.tier == NationTier::Empire ? Color{250, 215, 130, 255}
|
|
: nat.tier == NationTier::Kingdom ? Color{215, 210, 175, 255}
|
|
: Color{180, 190, 175, 255};
|
|
const char* tp = nat.totalPop >= 1.0e6 ? TextFormat("%.1fM", nat.totalPop / 1.0e6)
|
|
: nat.totalPop >= 1.0e3 ? TextFormat("%.0fk", nat.totalPop / 1.0e3)
|
|
: TextFormat("%.0f", nat.totalPop);
|
|
DrawText(nat.name.c_str(), (int)row.x + 6, (int)row.y + 2, 14, fg);
|
|
DrawText(TextFormat("%dx %s", nat.members, tp), (int)(r.x + r.width) - 92, (int)row.y + 3, 11, Color{150, 158, 178, 255});
|
|
y += 20;
|
|
}
|
|
}
|
|
} else { // Cultures: peoples by population (largest first); click a row to fly to their largest city
|
|
const auto& C = planet.cultureList();
|
|
DrawText("Cultures", x, y, 18, Color{200, 205, 220, 255});
|
|
DrawText(TextFormat("%d", (int)C.size()), (int)(r.x + r.width) - 40, y + 2, 13, Color{145, 155, 175, 255});
|
|
y += 26;
|
|
if (C.empty()) {
|
|
DrawText(planet.settlementsPlaced() ? "press X for the culture view" : "press U then X", x, y, 13, Color{150, 155, 170, 255});
|
|
} else {
|
|
const auto& sc = planet.settleCulture();
|
|
std::vector<int> idx(C.size());
|
|
for (size_t i = 0; i < C.size(); ++i) idx[i] = (int)i;
|
|
std::sort(idx.begin(), idx.end(), [&](int a, int b) { return C[a].totalPop > C[b].totalPop; });
|
|
for (int ci : idx) {
|
|
if (y > (int)(r.y + r.height) - 22) break;
|
|
const Culture& cu = C[ci];
|
|
// Representative cell: the largest living settlement of this culture (to fly to).
|
|
int repCell = -1; double repPop = -1.0;
|
|
for (size_t s = 0; s < planet.settlements.size(); ++s)
|
|
if (s < sc.size() && sc[s] == ci && planet.settlements[s].population > repPop)
|
|
{ repPop = planet.settlements[s].population; repCell = planet.settlements[s].cell; }
|
|
Rectangle row{ r.x + 10.0f, (float)y - 2.0f, r.width - 20.0f, 19.0f };
|
|
eventRowRects.push_back(row); atlasRowCells.push_back(repCell);
|
|
DrawText(cu.name.c_str(), (int)row.x + 6, (int)row.y + 2, 14, cultureColor(ci));
|
|
DrawText(TextFormat("%s %s", cultureEthosName(cu.ethos), faithFocusName(cu.faith)),
|
|
(int)(r.x + r.width) - 118, (int)row.y + 3, 11, Color{150, 158, 178, 255});
|
|
y += 20;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Right column: hover/selection info (top) + detail panel or world stats (bottom).
|
|
void Viewer::renderPanels() {
|
|
drawHoverPanel(planet, hoverRect, hovered, selectedCell);
|
|
if (selectedCell >= 0 && !subgrids.empty())
|
|
drawDetailPanel(planet, subgrids[0], selectedCell,
|
|
planet.cells[selectedCell].elevation, planet.cells[selectedCell].geoAge,
|
|
panelRect, gridRect, hoveredSubIdx);
|
|
else
|
|
drawStats(planet, panelRect, elapsedMy, settled, liveWorld, liveTime);
|
|
}
|
|
|
|
// Top-left HUD text + the clickable pause button.
|
|
void Viewer::renderHUD() {
|
|
// Active view-mode label, centered at the top of the 3D viewport.
|
|
{
|
|
const char* vm = TextFormat("%s view", colorModeName(mode));
|
|
int vw = MeasureText(vm, 22);
|
|
DrawText(vm, view3DW / 2 - vw / 2, 10, 22, Color{235, 225, 140, 255});
|
|
}
|
|
|
|
int y = 10;
|
|
auto line = [&](const std::string& s){ DrawText(s.c_str(), 12, y, 18, RAYWHITE); y += 22; };
|
|
double fastest = 0.0; for (const auto& pl : planet.plates) fastest = std::max(fastest, pl.speedCmYr);
|
|
line(liveWorld ? "Planet Sim - Live World"
|
|
: !settled ? "Planet Sim - World Creation: forming"
|
|
: phase3 ? "Planet Sim - World Creation: hydrology"
|
|
: "Planet Sim - World Creation: drift & erosion");
|
|
line(TextFormat("Cells: %d Subdiv: %d CellWidth: %.0f km",
|
|
(int)planet.cells.size(), cfg.subdivisions, planet.cellWidthMeters() / 1000.0));
|
|
line(TextFormat("Elevation: %.0f .. %.0f m", minE, maxE));
|
|
if (!settled)
|
|
line(TextFormat("Forming terrain tick %lld max change %.1f m/tick%s",
|
|
stepCount, maxChange, paused ? " [PAUSED]" : ""));
|
|
else if (liveWorld) {
|
|
const double dayH = planet.cfg.dayLengthHours, yrD = planet.cfg.yearLengthDays;
|
|
double days = liveTime / dayH;
|
|
long year = (long)std::floor(days / yrD) + 1;
|
|
long doy = (long)std::floor(days - std::floor(days / yrD) * yrD) + 1;
|
|
double hod = liveTime - std::floor(days) * dayH; // hours into the current day
|
|
int hh = (int)hod, mm = (int)((hod - hh) * 60.0);
|
|
line(TextFormat("Live World Year %ld Day %ld %02d:%02d%s",
|
|
year, doy, hh, mm, paused ? " [PAUSED]" : ""));
|
|
const double weekH = 7.0 * dayH, monthH = 30.0 * dayH;
|
|
const char* rl; double rv;
|
|
if (liveRate >= monthH) { rl = "mo/s"; rv = liveRate / monthH; }
|
|
else if (liveRate >= weekH) { rl = "wk/s"; rv = liveRate / weekH; }
|
|
else if (liveRate >= dayH) { rl = "d/s"; rv = liveRate / dayH; }
|
|
else { rl = "h/s"; rv = liveRate; }
|
|
line(TextFormat("rate %.1f %s day/night %s ([ / ] speed, N toggle, W exit)",
|
|
rv, rl, dayNightOn ? "on" : "off"));
|
|
int nStorm = 0, nHur = 0;
|
|
for (const auto& ws : planet.storms()) { ++nStorm; if (ws.tropical && ws.strength >= planet.cfg.weatherHurricaneStr) ++nHur; }
|
|
line(TextFormat("weather systems: %d tropical cyclones: %d%s", nStorm, nHur,
|
|
followId ? " [following]" : ""));
|
|
line("Y follow storm · . / , step clock +/- · wheel-on-map zoom");
|
|
}
|
|
else {
|
|
line(TextFormat("%s %.1f My elapsed %.1f My/s%s",
|
|
phase3 ? "Hydrology - drift, rivers & erosion" : "Drift & erosion",
|
|
elapsedMy, driftRate, paused ? " [PAUSED]" : ""));
|
|
line(TextFormat("dt %.2f My/step fastest plate %.1f cm/yr [ / ] speed", dtMy, fastest));
|
|
if (phase3) {
|
|
int riverCells = 0, lakeCells = 0; const auto& dq = planet.discharge(); const auto& lk = planet.lakeDepth();
|
|
for (size_t i = 0; i < planet.cells.size(); ++i) {
|
|
if (!dq.empty() && dq[i] > planet.cfg.riverThreshold) ++riverCells;
|
|
if (!lk.empty() && lk[i] > 20.0 && planet.cells[i].elevation > planet.cfg.seaLevel) ++lakeCells;
|
|
}
|
|
line(TextFormat("rivers: %d cells lakes: %d cells", riverCells, lakeCells));
|
|
}
|
|
}
|
|
y += 8;
|
|
line("hover: cell info | click tile: open detail panel | C close");
|
|
line("1 elev 2 plates 3 age 4 crust 5 biome 6 temp* 7 precip 8 flora 9 fauna 0 funga E eco (*6 cycles mean/summer/winter/season)");
|
|
line(TextFormat("B borders [%s] | D vectors [%s] | G grid [%s] | J rivers [%s] | N day/night [%s] | T tides [%s] | O currents [%s]",
|
|
showBorders ? "on" : "off", showDrift ? "on" : "off", showGrat ? "on" : "off", showRivers ? "on" : "off", dayNightOn ? "on" : "off", showTides ? "on" : "off", showCurrents ? "on" : "off"));
|
|
line(TextFormat("K clouds [%s] | V volcanoes [%s] | M names [%s] | E eco | I habitability | U settlements [%s] | P territory [%s] | X culture [%s]",
|
|
showClouds ? "on" : "off", showVolcanoes ? "on" : "off", showNames ? "on" : "off",
|
|
!planet.settlementsPlaced() ? "seed" : showSettlements ? "on" : "off",
|
|
showNationBorders ? "on" : "off", showCultureBorders ? "on" : "off"));
|
|
line(TextFormat("SPACE pause | [ / ] speed | S step | F fast-fwd | H hydrology [%s] | L biota [%s] | W live [%s] | R reseed | +/-",
|
|
phase3 ? "on" : "off", planet.biotaPopulated() ? "on" : "off", liveWorld ? "on" : "off"));
|
|
line("F5 save | F9 load | F12 screenshot | F2 reload planet.cfg");
|
|
if (!statusMsg.empty() && GetTime() < statusUntil) {
|
|
y += 4; DrawText(statusMsg.c_str(), 12, y, 18, Color{120, 230, 140, 255}); y += 22;
|
|
}
|
|
|
|
// Clickable pause button (bottom-left of the 3D quadrant).
|
|
DrawRectangleRec(pauseBtn, onPause ? Color{60, 70, 92, 255} : Color{28, 34, 46, 235});
|
|
DrawRectangleLinesEx(pauseBtn, 1, Color{120, 120, 150, 255});
|
|
const char* plbl = paused ? "> RESUME" : "|| PAUSE";
|
|
Color plcol = paused ? Color{120, 230, 140, 255} : RAYWHITE;
|
|
int plw = MeasureText(plbl, 18);
|
|
DrawText(plbl, (int)(pauseBtn.x + (pauseBtn.width - plw) / 2), (int)pauseBtn.y + 7, 18, plcol);
|
|
}
|
|
|
|
// Phase-3 transition prompt (modal overlay over the 3D viewport).
|
|
void Viewer::renderPrompt() {
|
|
if (!phase3Prompt) return;
|
|
DrawRectangle(0, 0, (int)view3DW, (int)view3DH, Color{0, 0, 0, 150});
|
|
const char* q = TextFormat("Reached %.0f My of drift. Begin hydrology (rivers, lakes & erosion)?", elapsedMy);
|
|
int qw = MeasureText(q, 22);
|
|
DrawText(q, (int)(pbCx - qw / 2.0f), (int)(pbCy - 40.0f), 22, RAYWHITE);
|
|
auto drawBtn = [&](Rectangle b, const char* lbl, Color fill) {
|
|
bool hot = CheckCollisionPointRec(mp, b);
|
|
DrawRectangleRec(b, hot ? Color{70, 90, 120, 255} : fill);
|
|
DrawRectangleLinesEx(b, 1, Color{150, 150, 180, 255});
|
|
int w = MeasureText(lbl, 18);
|
|
DrawText(lbl, (int)(b.x + (b.width - w) / 2.0f), (int)(b.y + 11.0f), 18, RAYWHITE);
|
|
};
|
|
drawBtn(p3ContinueBtn, "Keep building", Color{40, 46, 60, 255});
|
|
drawBtn(p3StartBtn, "Start hydrology", Color{30, 72, 60, 255});
|
|
}
|
|
|
|
// One full frame: globe texture, then composite + 3D labels + map + panels +
|
|
// HUD + prompt onto the screen.
|
|
void Viewer::renderFrame() {
|
|
renderGlobe3D();
|
|
|
|
BeginDrawing();
|
|
ClearBackground(Color{8, 10, 16, 255});
|
|
DrawTextureRec(rt3d.texture, Rectangle{0, 0, (float)view3DW, -(float)view3DH},
|
|
Vector2{0, 0}, WHITE);
|
|
|
|
// 3D plate labels (manually projected to match BeginMode3D's viewport exactly).
|
|
if (showDrift && !plateLabels.empty()) {
|
|
Vec3 camPos{cam.position.x, cam.position.y, cam.position.z};
|
|
Vec3 camTgt{cam.target.x, cam.target.y, cam.target.z};
|
|
Vec3 camUp {cam.up.x, cam.up.y, cam.up.z};
|
|
Vec3 forward = (camTgt - camPos).normalized();
|
|
Vec3 right = forward.cross(camUp).normalized();
|
|
Vec3 up = right.cross(forward);
|
|
|
|
double fovRad = cam.fovy * M_PI / 180.0;
|
|
double aspect = (double)view3DW / view3DH;
|
|
double projH = std::tan(fovRad * 0.5); // half-height of the view frustum (NDC)
|
|
double projW = projH * aspect;
|
|
|
|
for (const auto& lbl : plateLabels) {
|
|
Vec3 lp = rotateZ(Vec3{lbl.pos.x, lbl.pos.y, lbl.pos.z}, planet.cfg.axialTilt); // tilt to match globe
|
|
if (lp.dot(camPos) <= 0.0) continue; // far hemisphere -> hidden by globe
|
|
Vec3 rel = lp - camPos;
|
|
double z = rel.dot(forward);
|
|
if (z <= 0.0) continue;
|
|
double xndc = rel.dot(right) / (projW * z);
|
|
double yndc = rel.dot(up) / (projH * z);
|
|
float sx = (float)((xndc * 0.5 + 0.5) * view3DW);
|
|
float sy = (float)((0.5 - yndc * 0.5) * view3DH);
|
|
DrawText(TextFormat("P%d", lbl.id), (int)sx + 6, (int)sy - 6, 16, RAYWHITE);
|
|
}
|
|
}
|
|
|
|
// 3D place-name labels (the atlas), same manual projection. Minor features (peaks/rivers/lakes/
|
|
// small islands) only show when zoomed in, to keep the default view readable.
|
|
if (showNames && planet.geographyBuilt()) {
|
|
Vec3 camPos{cam.position.x, cam.position.y, cam.position.z};
|
|
Vec3 camTgt{cam.target.x, cam.target.y, cam.target.z};
|
|
Vec3 camUp {cam.up.x, cam.up.y, cam.up.z};
|
|
Vec3 forward = (camTgt - camPos).normalized();
|
|
Vec3 right = forward.cross(camUp).normalized();
|
|
Vec3 up = right.cross(forward);
|
|
double fovRad = cam.fovy * M_PI / 180.0;
|
|
double aspect = (double)view3DW / view3DH;
|
|
double projH = std::tan(fovRad * 0.5), projW = projH * aspect;
|
|
bool zoomed = camDist < 5.5;
|
|
for (const GeoFeature& f : planet.geography()) {
|
|
if (f.anchorCell < 0 || f.anchorCell >= (int)planet.cells.size()) continue;
|
|
int font; Color col; bool minor = labelStyle(f, font, col);
|
|
if (font <= 0 || (minor && !zoomed)) continue;
|
|
const Cell& c = planet.cells[f.anchorCell];
|
|
double sr = visBase + (double)c.elevation * elevExagg + 0.01;
|
|
Vec3 lp = rotateZ(c.unit, planet.cfg.axialTilt) * sr;
|
|
if (lp.dot(camPos) <= 0.0) continue; // far hemisphere
|
|
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 sy = (float)((0.5 - rel.dot(up) / (projH * z) * 0.5) * view3DH);
|
|
int w = MeasureText(f.name.c_str(), font);
|
|
DrawText(f.name.c_str(), (int)sx - w / 2 + 1, (int)sy - font / 2 + 1, font, Color{0, 0, 0, 180});
|
|
DrawText(f.name.c_str(), (int)sx - w / 2, (int)sy - font / 2, font, col);
|
|
}
|
|
}
|
|
|
|
// 3D settlement labels: name towns + cities (villages only when zoomed in), same manual projection.
|
|
if (showSettlements && !planet.settlements.empty()) {
|
|
Vec3 camPos{cam.position.x, cam.position.y, cam.position.z};
|
|
Vec3 camTgt{cam.target.x, cam.target.y, cam.target.z};
|
|
Vec3 forward = (camTgt - camPos).normalized();
|
|
Vec3 right = forward.cross(Vec3{cam.up.x, cam.up.y, cam.up.z}).normalized();
|
|
Vec3 up = right.cross(forward);
|
|
double fovRad = cam.fovy * M_PI / 180.0, aspect = (double)view3DW / view3DH;
|
|
double projH = std::tan(fovRad * 0.5), projW = projH * aspect;
|
|
bool zoomed = camDist < 5.0;
|
|
const double townP = planet.cfg.civTownPop, cityP = planet.cfg.civCityPop, abP = planet.cfg.civAbandonPop;
|
|
for (const Settlement& s : planet.settlements) {
|
|
if (s.cell < 0 || s.cell >= (int)planet.cells.size() || s.population < abP) continue;
|
|
SettleTier t = settleTierOf(s.population, townP, cityP);
|
|
if (t == SettleTier::Village && !zoomed) continue; // declutter
|
|
int font = t == SettleTier::City ? 15 : t == SettleTier::Town ? 13 : 12;
|
|
const Cell& c = planet.cells[s.cell];
|
|
double sr = visBase + (double)c.elevation * elevExagg + 0.02;
|
|
Vec3 lp = rotateZ(c.unit, planet.cfg.axialTilt) * sr;
|
|
if (lp.dot(camPos) <= 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 sy = (float)((0.5 - rel.dot(up) / (projH * z) * 0.5) * view3DH);
|
|
int w = MeasureText(s.name.c_str(), font);
|
|
DrawText(s.name.c_str(), (int)sx - w / 2 + 1, (int)sy + 6 + 1, font, Color{0, 0, 0, 190});
|
|
DrawText(s.name.c_str(), (int)sx - w / 2, (int)sy + 6, font,
|
|
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.
|
|
if (showNationBorders && !planet.nationList().empty()) {
|
|
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 right = forward.cross(Vec3{cam.up.x, cam.up.y, cam.up.z}).normalized();
|
|
Vec3 up = right.cross(forward);
|
|
double fovRad = cam.fovy * M_PI / 180.0, aspect = (double)view3DW / view3DH;
|
|
double projH = std::tan(fovRad * 0.5), projW = projH * aspect;
|
|
for (const Nation& nat : planet.nationList()) {
|
|
if (nat.tier == NationTier::CityState) continue; // declutter: only multi-settlement realms
|
|
if (nat.capital < 0 || nat.capital >= (int)planet.settlements.size()) continue;
|
|
int cell = planet.settlements[nat.capital].cell;
|
|
if (cell < 0 || cell >= (int)planet.cells.size()) continue;
|
|
int font = nat.tier == NationTier::Empire ? 16 : 14;
|
|
Vec3 lp = rotateZ(planet.cells[cell].unit, planet.cfg.axialTilt)
|
|
* (visBase + (double)planet.cells[cell].elevation * elevExagg + 0.035);
|
|
if (lp.dot(camPos) <= 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 sy = (float)((0.5 - rel.dot(up) / (projH * z) * 0.5) * view3DH);
|
|
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, (int)sy - font - 8, font, Color{245, 235, 210, 255});
|
|
}
|
|
}
|
|
|
|
renderMap2D();
|
|
renderLiveInfo();
|
|
renderPanels();
|
|
renderHUD();
|
|
renderPrompt();
|
|
|
|
EndDrawing();
|
|
}
|