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:
+37
-45
@@ -67,31 +67,38 @@ function* worldSteps(seedIn, w, h) {
|
||||
else dirtY[x] = Math.min(dirtY[x], surfY[x] + 20);
|
||||
}
|
||||
}
|
||||
// lakes: a basin dug below the lower of its two rims, filled to just under that rim (y grows
|
||||
// downward, so the lower rim is the larger surfY); kept 80 grains clear of spawn and the dock
|
||||
const lakeTop = new Int16Array(w).fill(h), noTree = new Uint8Array(w), desAt = new Uint8Array(w);
|
||||
// Lakes: lower the land in the nearest valley with a smooth bump (no slope at its edges, so the
|
||||
// banks keep the land's own shape), then fill the basin to its spill level. Water is flat and
|
||||
// contained by construction; lakes keep 80 grains clear of spawn and the dock.
|
||||
const lakeTop = new Int16Array(w).fill(h), noTree = new Uint8Array(w), lakeBed = new Uint8Array(w), desAt = new Uint8Array(w);
|
||||
for (const f of feats) {
|
||||
if (f.k !== 'lake') continue;
|
||||
const lw = Math.min(Math.round(90 + rng() * 40), Math.abs(f.x - spawnX) - 80), deep = 22 + rng() * 8;
|
||||
const lip = Math.max(surfY[Math.max(0, f.x - lw)], surfY[Math.min(w - 1, f.x + lw)]) - 1;
|
||||
const x0 = Math.max(0, f.x - lw), x1 = Math.min(w - 1, f.x + lw), nat = surfY.slice(x0, x1 + 1);
|
||||
for (let x = x0; x <= x1; x++) {
|
||||
const u = (x - f.x) / lw, depth = Math.max(0, 1 - u * u) * deep;
|
||||
if (depth > 0) surfY[x] = Math.max(surfY[x], Math.round(lip + depth));
|
||||
lakeTop[x] = lip + 2;
|
||||
const lwDraw = Math.round(90 + rng() * 40), deep = 22 + rng() * 8;
|
||||
// water gathers in valleys: take the lowest smoothed ground within 60 grains of the plan
|
||||
let c = f.x, low = -1;
|
||||
for (let x = f.x - 60; x <= f.x + 60; x++) {
|
||||
if (x - lwDraw < 0 || x + lwDraw >= w || Math.abs(x - spawnX) - 80 < lwDraw * 0.85) continue;
|
||||
let s = 0; for (let k = -15; k <= 15; k++) s += surfY[x + k];
|
||||
if (s > low) { low = s; c = x; }
|
||||
}
|
||||
// Dug to the lower rim, the high side would end in a cliff as tall as the gap between the
|
||||
// rims: from each edge inward the ground may fall at most 0.7 grains a column, never above nature.
|
||||
for (const d of [1, -1]) {
|
||||
let prev = surfY[d > 0 ? Math.max(0, x0 - 1) : Math.min(w - 1, x1 + 1)];
|
||||
for (let x = d > 0 ? x0 : x1; x >= x0 && x <= x1; x += d) {
|
||||
prev = Math.min(prev + 0.7, surfY[x]);
|
||||
surfY[x] = Math.max(nat[x - x0], Math.round(prev));
|
||||
prev = Math.max(prev, surfY[x]);
|
||||
const lw = Math.min(lwDraw, Math.abs(c - spawnX) - 80), x0 = Math.max(0, c - lw), x1 = Math.min(w - 1, c + lw);
|
||||
const nat = surfY.slice(x0, x1 + 1);
|
||||
// on sloping ground the low rim spills early, so deepen the bump until the lake holds water
|
||||
for (let scale = 1; scale <= 3; scale += 0.25) {
|
||||
for (let x = x0; x <= x1; x++) {
|
||||
// a bowl: flat floor in the middle 40%, banks easing in and out along a smoothstep
|
||||
const t = Math.min(1, Math.max(0, (1 - Math.abs(x - c) / lw) / 0.6)), b = t * t * (3 - 2 * t);
|
||||
surfY[x] = nat[x - x0] + Math.round(deep * scale * b * (1 + 0.15 * nDetail(x / 25, 6.6)));
|
||||
}
|
||||
if (spillLevels(x0, x1, lakeTop) >= 12) break;
|
||||
}
|
||||
for (let x = x0; x <= x1; x++) dirtY[x] = Math.max(dirtY[x], surfY[x] + 8);
|
||||
for (let x = Math.max(0, f.x - lw - 6); x <= Math.min(w - 1, f.x + lw + 6); x++) noTree[x] = 1;
|
||||
let wet0 = w, wet1 = -1;
|
||||
for (let x = x0; x <= x1; x++) {
|
||||
dirtY[x] = Math.max(dirtY[x], surfY[x] + 8);
|
||||
if (lakeTop[x] < surfY[x]) { wet0 = Math.min(wet0, x); wet1 = Math.max(wet1, x); } else lakeTop[x] = h;
|
||||
}
|
||||
for (let x = Math.max(0, wet0 - 6); x <= Math.min(w - 1, wet1 + 6); x++) noTree[x] = 1;
|
||||
for (let x = Math.max(0, wet0 - 12); x <= Math.min(w - 1, wet1 + 12); x++) lakeBed[x] = 1;
|
||||
}
|
||||
for (const f of feats) {
|
||||
if (f.k !== 'desert') continue;
|
||||
@@ -110,8 +117,9 @@ function* worldSteps(seedIn, w, h) {
|
||||
if (y >= lakeTop[x]) m = WATER;
|
||||
} else {
|
||||
const d = y - sy;
|
||||
const inDes = desAt[x] === 1;
|
||||
if (y < dirtY[x]) m = d === 0 && !inDes ? GRASS : DIRT;
|
||||
const inDes = desAt[x] === 1, bed = lakeBed[x] === 1;
|
||||
// a lake bottom is clay; the grass stops at the waterline
|
||||
if (y < dirtY[x]) m = lakeTop[x] < sy && d < 3 ? CLAY : d === 0 && !inDes ? GRASS : DIRT;
|
||||
else if (y < graniteY[x] + nWarp(x / 70, y / 45) * 26 + nWarp(x / 22 + 40, y / 15) * 8 + nPocket(x / 9, y / 9) * 3) m = STONE;
|
||||
else if (y < basaltY[x] + nWarp(x / 64 + 50, y / 40) * 22 + nWarp(x / 20 + 90, y / 14) * 7 + nPocket(x / 7, y / 7) * 3) m = GRANITE;
|
||||
else m = BASALT;
|
||||
@@ -126,8 +134,9 @@ function* worldSteps(seedIn, w, h) {
|
||||
else if (m === STONE) {
|
||||
if (d < 150 && pk > 0.52) m = DIRT;
|
||||
else if (d < 110 && pk2 > 0.5) m = CLAY;
|
||||
else if (pk < -0.56) m = GRAVEL;
|
||||
else if (pk2 < -0.6 && d < 220) m = SAND;
|
||||
// no loose pockets close under a lake: one falling into a cave would open a hole in its bed
|
||||
else if (pk < -0.56 && (!bed || d > 40)) m = GRAVEL;
|
||||
else if (pk2 < -0.6 && d < 220 && (!bed || d > 40)) m = SAND;
|
||||
}
|
||||
// ores
|
||||
if (m === STONE || m === DIRT || m === GRANITE || m === BASALT || m === CLAY) {
|
||||
@@ -160,8 +169,8 @@ function* worldSteps(seedIn, w, h) {
|
||||
// no ore where the rock is naturally hot enough to light or melt it: it would burn or run forever
|
||||
if (m !== host && (IGN[m] ? AMB[y] > IGN[m] - 120 : MELT[m] && AMB[y] > MELT[m] - 200)) m = host;
|
||||
}
|
||||
// caves
|
||||
if (d > 9) {
|
||||
// caves (kept deeper under lakes so none opens into one)
|
||||
if (d > (bed ? 40 : 9)) {
|
||||
const t1 = Math.abs(fbm(nCave, x / 130, y / 80, 2));
|
||||
const big = fy > 0.2 ? fbm(nCave2, x / 95, y / 64, 3) : -1;
|
||||
const small = nCave3(x / 26, y / 20);
|
||||
@@ -202,14 +211,12 @@ function* worldSteps(seedIn, w, h) {
|
||||
}
|
||||
yield [0.72, 'Snow and trees'];
|
||||
// trees
|
||||
const trees = [];
|
||||
for (let x = 12; x < w - 12;) {
|
||||
const sy = surfY[x];
|
||||
const ok = mat[sy * w + x] === GRASS && mat[sy * w + x + 1] === GRASS && !noTree[x] && Math.abs(x - spawnX) > 6 && !(x > spawnX + 14 && x < spawnX + 72); // none where the dock goes
|
||||
if (ok && rng() < 0.7) {
|
||||
const tree = { x, y: sy - 1, cells: [] }; trees.push(tree);
|
||||
const th = 9 + Math.floor(rng() * 8);
|
||||
const setW = (xx, yy) => { if (xx < 0 || yy < 1 || xx >= w) return; const i = yy * w + xx; if (mat[i] === EMPTY) { bgm[i] = WOOD; tree.cells.push(i, WOOD); } };
|
||||
const setW = (xx, yy) => { if (xx < 0 || yy < 1 || xx >= w) return; const i = yy * w + xx; if (mat[i] === EMPTY) bgm[i] = WOOD; };
|
||||
for (let k = 1; k <= th; k++) for (let dx = 0; dx < 2; dx++) setW(x + dx, sy - k);
|
||||
// two branches
|
||||
for (const side of [-1, 1]) { const by = sy - th + 3 + Math.floor(rng() * 3); for (let k = 1; k <= 3; k++) setW(x + (side < 0 ? -k : 1 + k), by - (k >> 1)); }
|
||||
@@ -220,7 +227,7 @@ function* worldSteps(seedIn, w, h) {
|
||||
for (let yy = Math.floor(cy - r); yy <= cy + r; yy++) for (let xx = Math.floor(cx - r); xx <= cx + r; xx++) {
|
||||
if (xx < 0 || yy < 1 || xx >= w) continue;
|
||||
const ex = (xx - cx) / r, ey = (yy - cy) / (r * 0.85);
|
||||
if (ex * ex + ey * ey <= 1 && hash2(xx, yy, worldSeed + 7) % 9 !== 0) { const i = yy * w + xx; if (mat[i] === EMPTY && !bgm[i]) { bgm[i] = LEAVES; tree.cells.push(i, LEAVES); } }
|
||||
if (ex * ex + ey * ey <= 1 && hash2(xx, yy, worldSeed + 7) % 9 !== 0) { const i = yy * w + xx; if (mat[i] === EMPTY && !bgm[i]) bgm[i] = LEAVES; }
|
||||
}
|
||||
}
|
||||
x += 16 + Math.floor(rng() * 20);
|
||||
@@ -268,26 +275,11 @@ function* worldSteps(seedIn, w, h) {
|
||||
if ((k & 31) === 0) yield [0.75 + 0.24 * k / 260, 'Settling loose ground'];
|
||||
}
|
||||
LSTEP = lstep; LFILM = lfilm; SORTP = sortp; CORNER_SEAL = cornerSeal; POWDER_FAN = powderFan;
|
||||
// Establish the dock's ground before levelling, so its foundation cannot erase water.
|
||||
placeDock(spawnX + 34);
|
||||
yield [0.99, 'Finishing'];
|
||||
levelWorldWater();
|
||||
// re-ambient after settling, then start clean
|
||||
for (let i = 0; i < N; i++) { const m = mat[i]; if (m !== MAGMA) temp[i] = AMB[(i / w) | 0]; life[i] = PH[m] === PH_LIQUID && FRZ[m] ? 255 : 0; }
|
||||
tick = 0;
|
||||
// Settling can carry lakes past noTree; leave their connected low banks treeless too.
|
||||
bgm.fill(0);
|
||||
for (const tree of trees) {
|
||||
let wet = false;
|
||||
for (let root = tree.x; root <= tree.x + 1 && !wet; root++) for (const d of [-1, 1]) {
|
||||
for (let x = root; x >= 0 && x < w; x += d) {
|
||||
const m = mat[tree.y * w + x];
|
||||
if (m === WATER) { wet = true; break; }
|
||||
if (m !== EMPTY && PH[m] !== PH_GAS) break;
|
||||
}
|
||||
}
|
||||
if (!wet) for (let k = 0; k < tree.cells.length; k += 2) bgm[tree.cells[k]] = tree.cells[k + 1];
|
||||
}
|
||||
findTreeBases();
|
||||
hot.fill(0); awake.fill(0); updateRoof();
|
||||
// Wake generated water so it responds from the first tick.
|
||||
|
||||
Reference in New Issue
Block a user