Rebuild lake generation: bowls in valleys filled to their spill level (t96)

Lakes no longer pour water to a guessed lip and repair it afterwards. Each lake lowers the land
in the nearest valley with a smoothstep bowl, and water fills it to its spill level (the lower of
the highest ground on each side, Priority-Flood in one dimension), so it is flat and contained by
construction. Clay beds, no caves or loose pockets close under lakes, trees kept off the shore.
Removes the levelling pass, the lower-rim lip and the bank-slope cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cj2RPXdPykKMEz5BXXczsj
This commit is contained in:
2026-10-29 20:55:52 -04:00
co-authored by Claude Opus 5.5
parent e3e01a84b1
commit daeb3b2238
9 changed files with 165 additions and 206 deletions
+11 -68
View File
@@ -1,4 +1,4 @@
// World generation helpers: noise, hard rock specks and lake levelling.
// World generation helpers: noise, hard rock specks and lake spill levels.
// ---------- Perlin noise ----------
function makeNoise(rng) {
const perm = new Uint8Array(512), gx = new Float32Array(256), gy = new Float32Array(256);
@@ -60,72 +60,15 @@ function clearHardSpecks(live) {
return n;
}
// Grow each basin upward from its bottom: dry walls/rims are crossed only if filled to them.
function levelWorldWater() {
const seen = new Uint8Array(N), region = new Uint32Array(N), cells = new Int32Array(N), stack = new Int32Array(N);
let tag = 0;
for (let start = 0; start < N; start++) {
if (mat[start] !== WATER || seen[start]) continue;
let size = 0, sp = 1, bottom = start;
stack[0] = start; seen[start] = 1;
// Measure the original four-connected body before moving any of its water.
while (sp) {
const i = stack[--sp], x = i % W, y = (i / W) | 0;
cells[size++] = i;
if (i > bottom) bottom = i;
for (let d = 0; d < 4; d++) {
if ((d === 0 && !x) || (d === 1 && x === W - 1) || (d === 2 && !y) || (d === 3 && y === H - 1)) continue;
const j = i + (d === 0 ? -1 : d === 1 ? 1 : d === 2 ? -W : W);
if (mat[j] === WATER && !seen[j]) { seen[j] = 1; stack[sp++] = j; }
}
}
if (size < 50) continue;
let volume = size;
for (let k = 0; k < size; k++) { const i = cells[k]; mat[i] = EMPTY; flags[i] = 0; }
tag++; size = sp = 1; cells[0] = stack[0] = bottom; region[bottom] = tag;
for (let level = (bottom / W) | 0; level >= 0; level--) {
if (!sp && level < H - 1) {
for (let x = 0; x < W; x++) {
const i = level * W + x;
if (region[i + W] === tag && (mat[i] === EMPTY || mat[i] === WATER)) {
region[i] = tag; cells[size++] = i; stack[sp++] = i;
}
}
}
// At this height, fill every connected low space before rising another row.
while (sp) {
const i = stack[--sp], x = i % W, y = (i / W) | 0;
if (mat[i] === WATER) { volume++; seen[i] = 1; }
for (let d = 0; d < 4; d++) {
if ((d === 0 && !x) || (d === 1 && x === W - 1) || (d === 2 && y <= level) || (d === 3 && y === H - 1)) continue;
const j = i + (d === 0 ? -1 : d === 1 ? 1 : d === 2 ? -W : W);
if (region[j] === tag || (mat[j] !== EMPTY && mat[j] !== WATER)) continue;
region[j] = tag; cells[size++] = j; stack[sp++] = j;
}
}
if (size >= volume) break;
}
const body = cells.subarray(0, size);
body.sort(); // row order, with no objects allocated per grain
for (let k = 0; k < size; k++) { const i = cells[k]; mat[i] = EMPTY; flags[i] = 0; seen[i] = 1; }
for (let end = size; end > 0 && volume > 0;) {
const y = (cells[end - 1] / W) | 0;
let begin = end - 1;
while (begin > 0 && ((cells[begin - 1] / W) | 0) === y) begin--;
const width = end - begin;
if (volume >= width) {
for (let k = begin; k < end; k++) { mat[cells[k]] = WATER; flags[cells[k]] = F_NAT; }
volume -= width;
} else {
const left = cells[begin] % W, right = cells[end - 1] % W, mid = (left + right) >> 1;
for (let d = 0; volume > 0; d++) {
const x = mid + (d & 1 ? (d + 1) >> 1 : -(d >> 1));
if (x < left || x > right) continue;
const i = y * W + x;
if (region[i] === tag) { mat[i] = WATER; flags[i] = F_NAT; volume--; }
}
}
end = begin;
}
// Water top for each column of [x0, x1] on the 1D side-view surface: the lower of the highest
// ground to its left and to its right (Priority-Flood reduced to one dimension; y grows
// downward). Writes out[x0..x1] and returns the deepest water.
function spillLevels(x0, x1, out) {
for (let x = x0, top = surfY[x0]; x <= x1; x++) { top = Math.min(top, surfY[x]); out[x] = top; }
let deepest = 0;
for (let x = x1, top = surfY[x1]; x >= x0; x--) {
top = Math.min(top, surfY[x]); out[x] = Math.max(out[x], top);
deepest = Math.max(deepest, surfY[x] - out[x]);
}
return deepest;
}