Level lakes at the end of world generation, keeping their water (t96)

Volume-preserving levelling pass for water bodies of 50+ cells; the dock foundation is placed
first so it cannot erase levelled water. Seed hashes re-baselined (water and air only).
Started by Codex, finished and reviewed by Claude after Codex hit its usage limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-29 19:28:23 -04:00
co-authored by Claude Opus 5.5
parent 75d4e8ee4e
commit ae78756fa2
10 changed files with 227 additions and 50 deletions
+71 -1
View File
@@ -1,4 +1,4 @@
// World generation helpers: noise and removal of tiny hard rock specks.
// World generation helpers: noise, hard rock specks and lake levelling.
// ---------- Perlin noise ----------
function makeNoise(rng) {
const perm = new Uint8Array(512), gx = new Float32Array(256), gy = new Float32Array(256);
@@ -59,3 +59,73 @@ 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;
}
}
}