using System;
using System.Collections.Generic;
using Godot;
using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
///
/// ⭐⭐ THE HYDROLOGY MAP (rivers/05) — a first-class reference map, not a diagnostic dump (→ D-056).
///
/// "Here is where the water lives and where it flows": the island's relief, its lakes as water, the
/// flow-direction field as a quiet streamline texture, and the flow-through rivers prominent —
/// chaining visibly through lakes and low ground to the sea, width ∝ √drainage on the fixed law,
/// sea-reaching and lake-terminal distinguished, confluences as merges, dropped rivers as a ghost of
/// their stem only. Presentation only — reads data, writes pixels.
///
/// The companion plate is the DATA map of the field on its own: hue by
/// heading, sinks black, walled basins darkened — the one placement / flooding / irrigation reference.
///
public static class HydrologyRenderer
{
private static readonly Color RiverSea = new(0.860f, 0.960f, 1.000f);
private static readonly Color RiverLake = new(1.000f, 0.840f, 0.520f);
private static readonly Color RiverEdge = new(0.050f, 0.110f, 0.240f);
private static readonly Color LakeWater = new(0.300f, 0.560f, 0.940f);
private static readonly Color PondWater = new(0.330f, 0.540f, 0.860f);
private static readonly Color Ghost = new(0.620f, 0.220f, 0.200f);
private static readonly Color Junction = new(1.000f, 1.000f, 1.000f);
private static readonly Color Stream = new(0.980f, 0.990f, 1.000f);
private static readonly Color Ink = new(0.941f, 0.949f, 0.961f);
private static readonly Color Ocean = new(0.055f, 0.110f, 0.235f);
private static readonly Color Sink = new(0.020f, 0.020f, 0.020f);
private static readonly int[] DX = { -1, -1, -1, 0, 0, 1, 1, 1 };
private static readonly int[] DY = { -1, 0, 1, -1, 1, -1, 0, 1 };
/// TinyFont carries only . - : / ( ) 0-9 A-Z; everything else would print as a gap. Map the punctuation the plate text uses.
public static string Txt(string s) => s
.Replace("%", " PCT").Replace("+", " AND ").Replace(";", " -").Replace(",", " -").Replace("'", "")
.Replace("→", "-").Replace("≤", "UNDER").Replace(">", "OVER").Replace("<", "UNDER").Replace("!", ".").Replace(" ", " ");
/// The shaded-relief base (the atlas look), quietened — desaturated and darkened a little so the water reads on top of it.
public static Image Base(float[,] height, int n, float sea)
{
var look = new LookConfig { SeaLevel = sea };
var img = ReliefRenderer.Render(height, n, look);
for (int x = 0; x < n; x++)
for (int y = 0; y < n; y++)
{
Color c = img.GetPixel(x, y);
float l = 0.299f * c.R + 0.587f * c.G + 0.114f * c.B;
Color q = c.Lerp(new Color(l, l, l), 0.38f);
img.SetPixel(x, y, new Color(q.R * 0.88f, q.G * 0.88f, q.B * 0.88f));
}
return img;
}
private static bool[] LakeIds(BasinGraph graph)
{
int max = 0;
foreach (var b in graph.Nodes) if (b.Id > max) max = b.Id;
var lake = new bool[max + 1];
foreach (var b in graph.Nodes) if (b.IsLake) lake[b.Id] = true;
return lake;
}
public static Image Hydrology(FlowThroughRouting.Result r, sbyte[] dir, DrainageAnalysis.Plan plan, BasinGraph graph,
Image img, int n, bool[] isOcean, bool[] isClassifyWater, string title, string subtitle, string third)
{
int total = n * n;
bool[] lakeId = LakeIds(graph);
bool IsLakeWater(int i) => isClassifyWater[i] && !isOcean[i] && plan.BasinId[i] != 0 && plan.BasinId[i] < lakeId.Length && lakeId[plan.BasinId[i]];
// 1. the lakes — every non-ocean classify body drawn as water; the significant ones (lake basins) a touch brighter.
for (int i = 0; i < total; i++)
{
if (!isClassifyWater[i] || isOcean[i]) continue;
img.SetPixel(i / n, i % n, IsLakeWater(i) ? LakeWater : PondWater);
}
// 2. the field as a streamline texture — quiet, present, not shouting.
Streamlines(img, dir, n, isOcean, isClassifyWater, n >= 4096 ? 56 : 28, 0.30f);
// 3. dropped rivers — a ghost of the upland stem only: "considered, dropped".
foreach (var fr in r.Rivers)
{
if (!fr.Dropped || fr.Routed.CellPath == null) continue;
int stem = Math.Min(fr.Routed.StemCells, fr.Routed.CellPath.Count);
for (int i = 0; i < stem; i++)
{
var (x, y) = fr.Routed.CellPath[i];
img.SetPixel(x, y, img.GetPixel(x, y).Lerp(Ghost, 0.55f));
}
}
// 4. the kept rivers — outlined, smallest first so the big ones finish on top; lake spans not drawn.
var kept = new List();
foreach (var fr in r.Rivers) if (!fr.Dropped && fr.Routed.CellPath != null && fr.Routed.CellPath.Count > 0) kept.Add(fr);
kept.Sort((a, b) => a.Candidate.DrainagePx.CompareTo(b.Candidate.DrainagePx));
foreach (var fr in kept) DrawRiver(img, fr, n, RiverEdge, +1, IsLakeWater);
foreach (var fr in kept) DrawRiver(img, fr, n, fr.ReachesSea ? RiverSea : RiverLake, 0, IsLakeWater);
// 5. markers — through the confluence root: a tributary's mouth is its trunk's mouth.
int mark = n >= 4096 ? 14 : 8;
foreach (var fr in kept)
{
if (fr.Routed.Joined)
{
DrainageRenderer.Disc(img, fr.Routed.JunctionCell.x, fr.Routed.JunctionCell.y, Math.Max(3, mark / 2), n, Junction);
continue;
}
if (fr.MouthCell < 0) continue;
int mx = fr.MouthCell / n, my = fr.MouthCell % n;
if (fr.Terminus == FlowThroughRouting.Terminus.Ocean)
{
DrainageRenderer.Square(img, mx, my, mark / 2, n, RiverSea);
DrainageRenderer.Ring(img, mx, my, mark, n, RiverEdge, 3);
}
else
{
DrainageRenderer.Disc(img, mx, my, mark / 2, n, RiverLake);
DrainageRenderer.Ring(img, mx, my, mark, n, RiverEdge, 3);
}
}
// 6. labels.
int ls = n >= 4096 ? 4 : 3;
var placer = new DrainageRenderer.LabelPlacer(n, ls, headerLines: 8);
int droppedLabels = 0;
var byArea = new List(kept);
byArea.Sort((a, b) => b.Candidate.DrainagePx.CompareTo(a.Candidate.DrainagePx));
foreach (var fr in byArea)
{
var c = fr.Candidate;
string tag = fr.Routed.Joined ? $"INTO R{fr.Routed.ConfluenceParentRank}"
: fr.Trunk ? "TRUNK" : fr.Terminus == FlowThroughRouting.Terminus.Ocean ? $"SEA VIA {fr.Chain.Count}" : $"LAKE {fr.TerminusBasinId}";
int lx = fr.Routed.Joined ? fr.Routed.JunctionCell.x : fr.MouthCell / n;
int ly = fr.Routed.Joined ? fr.Routed.JunctionCell.y : fr.MouthCell % n;
if (!placer.Place(img, Txt($"R{c.Rank} {DrainageRenderer.DrainageLabel(c.DrainagePx)} {tag}"), lx, ly, mark,
fr.Routed.Joined ? Junction : fr.ReachesSea ? RiverSea : RiverLake)) droppedLabels++;
}
// 7. the legend, on a dark bar so it reads on the relief.
int s = n >= 4096 ? 4 : 3; int lh = TinyFont.Height(s) + 6;
Bar(img, n, 12 + lh * 8 + 8);
TinyFont.Draw(img, Txt(title), 12, 12, s, Ink);
TinyFont.Draw(img, Txt(subtitle), 12, 12 + lh, s, Ink);
TinyFont.Draw(img, Txt(third), 12, 12 + lh * 2, s, Ink);
TinyFont.Draw(img, Txt($"PALE BLUE RIVER = REACHES THE SEA ({r.Trunks} NATURAL TRUNK + {r.FlowThrough} FLOW-THROUGH). SQUARE = MOUTH. AMBER RIVER = LAKE-TERMINAL ({r.LakeTerminal}), DISC = WHERE IT ENTERS ITS LAKE"), 12, 12 + lh * 3, s, RiverSea);
TinyFont.Draw(img, Txt($"WHITE DOT = CONFLUENCE ({r.Joined} JOINED). FAINT RED GHOST = A RIVER CONSIDERED AND DROPPED ({r.DroppedDry + r.DroppedClosed}) - ITS CHAIN WALLED AT A DRY SINK, SO IT IS NOT DRAWN"), 12, 12 + lh * 4, s, Junction);
TinyFont.Draw(img, Txt($"BLUE = EXISTING LAKES (CLASSIFY WATER). A RIVER'S SPAN ACROSS A LAKE IS WATER, NOT A DRAWN CHANNEL. STREAMLINES = THE FLOW-DIRECTION FIELD AT CAP {r.CapM:F0} M"), 12, 12 + lh * 5, s, LakeWater);
TinyFont.Draw(img, Txt($"WIDTH: {DrainageRenderer.StemWidthLaw()} - AS RIVERS/02B, 03, 03B, 03C"), 12, 12 + lh * 6, s, Ink);
TinyFont.Draw(img, "ROUTING AND DATA ONLY - NO HEIGHT MUTATED, NO WATER FILLED OR CREATED, NO BED CARVED." + (droppedLabels > 0 ? $" ({droppedLabels} LABEL(S) DROPPED)" : ""), 12, 12 + lh * 7, s, Ink);
return img;
}
/// Draw a river's OWN reach (up to its junction) as discs along its rasterised cells, skipping cells that are lake water.
private static void DrawRiver(Image img, FlowThroughRouting.FlowRiver fr, int n, Color c, int grow, Func isLakeWater)
{
int w = DrainageRenderer.StemWidthFixed(fr.Candidate.DrainagePx);
int rad = Math.Max(1, w / 2) + grow;
var cells = fr.Routed.CellPath;
int own = fr.Routed.Joined ? OwnLength(fr.Routed) : cells.Count;
for (int i = 0; i < own; i++)
{
var (x, y) = cells[i];
if (x < 0 || y < 0 || x >= n || y >= n) continue;
if (isLakeWater(x * n + y)) continue;
DrainageRenderer.Disc(img, x, y, rad, n, c);
}
}
private static int OwnLength(RiverRouting.RoutedRiver r)
{
for (int i = 0; i < r.CellPath.Count; i++)
if (r.CellPath[i].x == r.JunctionCell.x && r.CellPath[i].y == r.JunctionCell.y) return i + 1;
return r.CellPath.Count;
}
///
/// The field as a texture: from a grid of seed cells on land, follow the field for a short run and
/// draw it faint-to-stronger along the flow, with a dot at the downstream end. A quiet island-wide
/// "which way does water go here" that never competes with the rivers.
///
private static void Streamlines(Image img, sbyte[] dir, int n, bool[] isOcean, bool[] isClassifyWater, int step, float alpha)
{
int len = (int)(step * 0.7f);
for (int gx = step / 2; gx < n; gx += step)
for (int gy = step / 2; gy < n; gy += step)
{
int c = gx * n + gy;
if (isOcean[c] || isClassifyWater[c] || dir[c] < 0) continue;
for (int k = 0; k < len; k++)
{
sbyte d = dir[c];
if (d < 0) break;
int cx = c / n, cy = c % n;
int t = (cx + DX[d]) * n + (cy + DY[d]);
if (isOcean[t]) break;
float a = alpha * (0.35f + 0.65f * k / len);
img.SetPixel(t / n, t % n, img.GetPixel(t / n, t % n).Lerp(Stream, a));
c = t;
}
img.SetPixel(c / n, c % n, img.GetPixel(c / n, c % n).Lerp(Stream, alpha * 1.4f));
}
}
private static void Bar(Image img, int n, int height)
{
for (int y = 0; y < Math.Min(height, n); y++)
for (int x = 0; x < n; x++)
img.SetPixel(x, y, img.GetPixel(x, y).Lerp(new Color(0.04f, 0.05f, 0.07f), 0.72f));
}
///
/// ⭐ THE FLOW-DIRECTION DATA MAP — hue by heading (the wheel: N red, E yellow-green, S cyan, W violet),
/// sinks black, ocean dark, walled basins darkened, lakes as water at half strength so the field still
/// shows through, streamlines on top. Not pretty by design — legible.
///
public static Image FlowDirection(sbyte[] dir, int[] acc, DrainageAnalysis.Plan plan, BasinGraph graph, HashSet walled,
int n, bool[] isOcean, bool[] isClassifyWater, string title, string subtitle, string third)
{
var img = Image.CreateEmpty(n, n, false, Image.Format.Rgb8);
long maxAcc = 1;
for (int i = 0; i < acc.Length; i++) if (acc[i] > maxAcc) maxAcc = acc[i];
double lmax = Math.Log(1.0 + maxAcc);
var hue = new Color[8];
for (int k = 0; k < 8; k++)
{
// screen +y is SOUTH, so flip y to get a compass angle; hue 0 at north, clockwise.
float ang = MathF.Atan2(DX[k], -DY[k]); // 0 = north, +π/2 = east
float h = (ang / (2f * MathF.PI) + 1f) % 1f;
hue[k] = Color.FromHsv(h, 0.62f, 0.86f);
}
bool[] lakeId = LakeIds(graph);
for (int i = 0; i < n * n; i++)
{
int x = i / n, y = i % n;
if (isOcean[i]) { img.SetPixel(x, y, Ocean); continue; }
sbyte d = dir[i];
Color c;
if (d < 0) c = Sink;
else
{
// Hue = heading; brightness = log accumulation on the capped field, so the field's own drainage
// tree reads as bright channels on dark slopes and the per-cell heading noise stays quiet.
float v = (float)(Math.Log(1.0 + acc[i]) / lmax);
float b = 0.22f + 0.78f * v;
c = new Color(hue[d].R * b, hue[d].G * b, hue[d].B * b);
}
int id = plan.BasinId[i];
if (id != 0 && walled.Contains(id)) c = c.Lerp(new Color(0.55f, 0.08f, 0.08f), 0.35f);
if (isClassifyWater[i])
c = c.Lerp(id != 0 && id < lakeId.Length && lakeId[id] ? LakeWater : PondWater, 0.45f);
img.SetPixel(x, y, c);
}
int s = n >= 4096 ? 4 : 3; int lh = TinyFont.Height(s) + 6;
Bar(img, n, 12 + lh * 5 + 8);
TinyFont.Draw(img, Txt(title), 12, 12, s, Ink);
TinyFont.Draw(img, Txt(subtitle), 12, 12 + lh, s, Ink);
TinyFont.Draw(img, Txt(third), 12, 12 + lh * 2, s, Ink);
// the wheel, as swatches
string[] names = { "NW", "W", "SW", "N", "S", "NE", "E", "SE" };
int cx0 = 12, cy0 = 12 + lh * 3;
TinyFont.Draw(img, Txt("HUE = HEADING:"), cx0, cy0, s, Ink);
int cursor = cx0 + TinyFont.Width("HUE = HEADING: ", s);
int[] order = { 3, 5, 6, 7, 4, 2, 1, 0 }; // N NE E SE S SW W NW
foreach (int k in order)
{
DrainageRenderer.Square(img, cursor + 8, cy0 + TinyFont.Height(s) / 2, 7, n, hue[k]);
TinyFont.Draw(img, names[k], cursor + 20, cy0, s, Ink);
cursor += 20 + TinyFont.Width(names[k] + " ", s);
}
TinyFont.Draw(img, Txt("BRIGHTNESS = LOG FLOW ACCUMULATION ON THIS FIELD (CHANNELS BRIGHT). BLACK = SINK (A WALLED BASIN FLOOR). RED-TINTED = INSIDE A WALLED BASIN - FLOW ENTERING IT ENDS THERE. BLUE HAZE = CLASSIFY WATER."), 12, 12 + lh * 4, s, Ink);
return img;
}
}
}