v1.5.76 - major river-lake redesign

This commit is contained in:
Azgaar 2021-02-28 13:32:04 +03:00
parent 8563aaf50d
commit 07f0eff52c
7 changed files with 227 additions and 194 deletions

77
main.js
View file

@ -547,6 +547,7 @@ function generate() {
drawCoastline(); drawCoastline();
Rivers.generate(); Rivers.generate();
defineLakesGroup();
defineBiomes(); defineBiomes();
rankCells(); rankCells();
@ -673,18 +674,13 @@ function markFeatures() {
TIME && console.timeEnd("markFeatures"); TIME && console.timeEnd("markFeatures");
} }
// How to handle lakes generated near seas? They can be both open or closed. // near sea lakes usually get a lot of water inflow, most of them should brake treshold and flow out to sea (see Ancylus Lake)
// As these lakes are usually get a lot of water inflow, most of them should have brake the treshold and flow to sea via river or strait (see Ancylus Lake).
// So I will help this process and open these kind of lakes setting a treshold cell heigh below the sea level (=19).
function openNearSeaLakes() { function openNearSeaLakes() {
if (templateInput.value === "Atoll") return; // no need for Atolls if (templateInput.value === "Atoll") return; // no need for Atolls
const cells = grid.cells, features = grid.features; const cells = grid.cells, features = grid.features;
if (!features.find(f => f.type === "lake")) return; // no lakes if (!features.find(f => f.type === "lake")) return; // no lakes
TIME && console.time("openLakes"); TIME && console.time("openLakes");
const limit = 50; // max height that can be breached by water const LIMIT = 22; // max height that can be breached by water
for (let t = 0, removed = true; t < 5 && removed; t++) {
removed = false;
for (const i of cells.i) { for (const i of cells.i) {
const lake = cells.f[i]; const lake = cells.f[i];
@ -692,19 +688,17 @@ function openNearSeaLakes() {
check_neighbours: check_neighbours:
for (const c of cells.c[i]) { for (const c of cells.c[i]) {
if (cells.t[c] !== 1 || cells.h[c] > limit) continue; // water cannot brake this if (cells.t[c] !== 1 || cells.h[c] > LIMIT) continue; // water cannot brake this
for (const n of cells.c[c]) { for (const n of cells.c[c]) {
const ocean = cells.f[n]; const ocean = cells.f[n];
if (features[ocean].type !== "ocean") continue; // not an ocean if (features[ocean].type !== "ocean") continue; // not an ocean
removed = removeLake(c, lake, ocean); removeLake(c, lake, ocean);
break check_neighbours; break check_neighbours;
} }
} }
} }
}
function removeLake(treshold, lake, ocean) { function removeLake(treshold, lake, ocean) {
cells.h[treshold] = 19; cells.h[treshold] = 19;
cells.t[treshold] = -1; cells.t[treshold] = -1;
@ -713,7 +707,7 @@ function openNearSeaLakes() {
if (cells.h[c] >= 20) cells.t[c] = 1; // mark as coastline if (cells.h[c] >= 20) cells.t[c] = 1; // mark as coastline
}); });
features[lake].type = "ocean"; // mark former lake as ocean features[lake].type = "ocean"; // mark former lake as ocean
return true; debug.append("circle").attr("cx", grid.points[treshold][0]).attr("cy", grid.points[treshold][1]).attr("r", 2);
} }
TIME && console.timeEnd("openLakes"); TIME && console.timeEnd("openLakes");
@ -999,7 +993,7 @@ function drawCoastline() {
if (features[f].type === "lake") { if (features[f].type === "lake") {
landMask.append("path").attr("d", path).attr("fill", "black").attr("id", "land_"+f); landMask.append("path").attr("d", path).attr("fill", "black").attr("id", "land_"+f);
// waterMask.append("path").attr("d", path).attr("fill", "white").attr("id", "water_"+id); // uncomment to show over lakes // waterMask.append("path").attr("d", path).attr("fill", "white").attr("id", "water_"+id); // uncomment to show over lakes
lakes.select("#"+features[f].group).append("path").attr("d", path).attr("id", "lake_"+f).attr("data-f", f); // draw the lake lakes.select("#freshwater").append("path").attr("d", path).attr("id", "lake_"+f).attr("data-f", f); // draw the lake
} else { } else {
landMask.append("path").attr("d", path).attr("fill", "white").attr("id", "land_"+f); landMask.append("path").attr("d", path).attr("fill", "white").attr("id", "land_"+f);
waterMask.append("path").attr("d", path).attr("fill", "black").attr("id", "water_"+f); waterMask.append("path").attr("d", path).attr("fill", "black").attr("id", "water_"+f);
@ -1105,23 +1099,12 @@ function reMarkFeatures() {
const type = land ? "island" : border ? "ocean" : "lake"; const type = land ? "island" : border ? "ocean" : "lake";
let group; let group;
if (type === "lake") group = defineLakeGroup(start, cellNumber, temp[cells.g[start]]); if (type === "ocean") group = defineOceanGroup(cellNumber);
else if (type === "ocean") group = defineOceanGroup(cellNumber);
else if (type === "island") group = defineIslandGroup(start, cellNumber); else if (type === "island") group = defineIslandGroup(start, cellNumber);
features.push({i, land, border, type, cells: cellNumber, firstCell: start, group}); features.push({i, land, border, type, cells: cellNumber, firstCell: start, group});
queue[0] = cells.f.findIndex(f => !f); // find unmarked cell queue[0] = cells.f.findIndex(f => !f); // find unmarked cell
} }
function defineLakeGroup(cell, number, temp) {
if (temp > 31) return "dry";
if (temp > 24) return "salt";
if (temp < -3) return "frozen";
const height = d3.max(cells.c[cell].map(c => cells.h[c]));
if (height > 69 && number < 3 && cell%5 === 0) return "sinkhole";
if (height > 69 && number < 10 && cell%5 === 0) return "lava";
return "freshwater";
}
function defineOceanGroup(number) { function defineOceanGroup(number) {
if (number > grid.cells.i.length / 25) return "ocean"; if (number > grid.cells.i.length / 25) return "ocean";
if (number > grid.cells.i.length / 100) return "sea"; if (number > grid.cells.i.length / 100) return "sea";
@ -1138,6 +1121,34 @@ function reMarkFeatures() {
TIME && console.timeEnd("reMarkFeatures"); TIME && console.timeEnd("reMarkFeatures");
} }
function defineLakesGroup() {
for (const feature of pack.features) {
if (feature.type !== "lake") continue;
const lakeEl = lakes.select(`[data-f="${feature.i}"]`).node();
if (!lakeEl) continue;
feature.group = defineGroup(feature);
document.getElementById(feature.group).appendChild(lakeEl);
}
function defineGroup(feature) {
const gridCell = pack.cells.g[feature.firstCell];
const temp = grid.cells.temp[gridCell];
if (temp < -3) return "frozen";
if (feature.height > 60 && feature.cells < 10 && feature.firstCell % 5 === 0) return "lava";
if (!feature.inlets && !feature.outlet) {
if (feature.evaporation / 2 > feature.flux) return "dry";
if (feature.cells < 3 && feature.firstCell % 5 === 0) return "sinkhole";
}
if (!feature.outlet && feature.evaporation > feature.flux) return "salt";
return "freshwater";
}
}
// assign biome id for each cell // assign biome id for each cell
function defineBiomes() { function defineBiomes() {
TIME && console.time("defineBiomes"); TIME && console.time("defineBiomes");
@ -1174,7 +1185,7 @@ function getBiomeId(moisture, temperature, height) {
// assess cells suitability to calculate population and rand cells for culture center and burgs placement // assess cells suitability to calculate population and rand cells for culture center and burgs placement
function rankCells() { function rankCells() {
TIME && console.time('rankCells'); TIME && console.time('rankCells');
const cells = pack.cells, f = pack.features; const {cells, features} = pack;
cells.s = new Int16Array(cells.i.length); // cell suitability array cells.s = new Int16Array(cells.i.length); // cell suitability array
cells.pop = new Float32Array(cells.i.length); // cell population array cells.pop = new Float32Array(cells.i.length); // cell population array
@ -1190,12 +1201,14 @@ function rankCells() {
if (cells.t[i] === 1) { if (cells.t[i] === 1) {
if (cells.r[i]) s += 15; // estuary is valued if (cells.r[i]) s += 15; // estuary is valued
const type = f[cells.f[cells.haven[i]]].type; const feature = features[cells.f[cells.haven[i]]];
const group = f[cells.f[cells.haven[i]]].group; if (feature.type === "lake") {
if (type === "lake") { if (feature.group === "freshwater") s += 30;
// lake coast is valued else if (feature.group == "salt") s += 10;
if (group === "freshwater") s += 30; else if (feature.group == "frozen") s += 1;
else if (group !== "lava" && group !== "dry") s += 10; else if (feature.group == "dry") s -= 5;
else if (feature.group == "sinkhole") s -= 5;
else if (feature.group == "lava") s -= 30;
} else { } else {
s += 5; // ocean coast is valued s += 5; // ocean coast is valued
if (cells.harbor[i] === 1) s += 20; // safe sea harbor is valued if (cells.harbor[i] === 1) s += 20; // safe sea harbor is valued

View file

@ -9,124 +9,108 @@ const generate = function(changeHeights = true) {
Math.random = aleaPRNG(seed); Math.random = aleaPRNG(seed);
const cells = pack.cells, p = cells.p, features = pack.features; const cells = pack.cells, p = cells.p, features = pack.features;
const riversData = []; // rivers data
cells.fl = new Uint16Array(cells.i.length); // water flux array
cells.r = new Uint16Array(cells.i.length); // rivers array
cells.conf = new Uint8Array(cells.i.length); // confluences array
let riverNext = 1; // first river id is 1
markupLand();
const h = alterHeights();
removeStoredLakeData();
resolveDepressions(h);
drainWater();
defineRivers();
cleanupLakeData();
if (changeHeights) cells.h = Uint8Array.from(h); // apply changed heights as basic one
TIME && console.timeEnd('generateRivers');
// build distance field in cells from water (cells.t) // build distance field in cells from water (cells.t)
void function markupLand() { function markupLand() {
const q = t => cells.i.filter(i => cells.t[i] === t); const q = t => cells.i.filter(i => cells.t[i] === t);
for (let t = 2, queue = q(t); queue.length; t++, queue = q(t)) { for (let t = 2, queue = q(t); queue.length; t++, queue = q(t)) {
queue.forEach(i => cells.c[i].forEach(c => { queue.forEach(i => cells.c[i].forEach(c => {
if (!cells.t[c]) cells.t[c] = t+1; if (!cells.t[c]) cells.t[c] = t+1;
})); }));
} }
}() }
// height with added t value to make map less depressed // height with added t value to make map less depressed
function alterHeights() {
const h = Array.from(cells.h) const h = Array.from(cells.h)
.map((h, i) => h < 20 || cells.t[i] < 1 ? h : h + cells.t[i] / 100) .map((h, i) => h < 20 || cells.t[i] < 1 ? h : h + cells.t[i] / 100)
.map((h, i) => h < 20 || cells.t[i] < 1 ? h : h + d3.mean(cells.c[i].map(c => cells.t[c])) / 10000); .map((h, i) => h < 20 || cells.t[i] < 1 ? h : h + d3.mean(cells.c[i].map(c => cells.t[c])) / 10000);
return h;
resolveDepressions(h);
features.forEach(f => {delete f.river; delete f.flux; delete f.inlets});
const riversData = []; // rivers data
cells.fl = new Uint16Array(cells.i.length); // water flux array
cells.r = new Uint16Array(cells.i.length); // rivers array
cells.conf = new Uint8Array(cells.i.length); // confluences array
let riverNext = 1; // first river id is 1, not 0
void function drainWater() {
const land = cells.i.filter(i => h[i] >= 20).sort((a,b) => h[b] - h[a]);
const outlets = new Uint32Array(features.length);
// enumerate lake outlet positions
features.filter(f => f.type === "lake" && (f.group === "freshwater" || f.group === "frozen")).forEach(l => {
let outlet = 0;
if (l.shoreline) {
outlet = l.shoreline[d3.scan(l.shoreline, (a,b) => h[a] - h[b])];
} else { // in case it got missed or deleted
WARN && console.warn('Re-scanning shoreline of a lake');
const shallows = cells.i.filter(j => cells.t[j] === -1 && cells.f[j] === l.i);
let shoreline = [];
shallows.map(w => cells.c[w]).forEach(cList => cList.forEach(s => shoreline.push(s)));
outlet = shoreline[d3.scan(shoreline, (a,b) => h[a] - h[b])];
} }
outlets[l.i] = outlet;
delete l.shoreline // cleanup temp data once used function removeStoredLakeData() {
features.forEach(f => {
delete f.flux;
delete f.inlets;
delete f.outlet;
delete f.height;
}); });
}
const flowDown = function(min, mFlux, iFlux, ri, i = 0){ function drainWater() {
if (cells.r[min]) { // downhill cell already has river assigned const MIN_FLUX_TO_FORM_RIVER = 30;
if (mFlux < iFlux) { const land = cells.i.filter(i => h[i] >= 20).sort((a,b) => h[b] - h[a]);
cells.conf[min] = cells.fl[min]; // mark confluence
if (h[min] >= 20) riversData.find(r => r.river === cells.r[min]).parent = ri; // min river is a tributary of current river
cells.r[min] = ri; // re-assign river if downhill part has less flux
} else {
cells.conf[min] += iFlux; // mark confluence
if (h[min] >= 20) riversData.find(r => r.river === ri).parent = cells.r[min]; // current river is a tributary of min river
}
} else cells.r[min] = ri; // assign the river to the downhill cell
if (h[min] < 20) { const lakeOutCells = new Uint16Array(cells.i.length); // to enumerate lake outlet positions
// pour water to the sea haven features.forEach(f => {
const oh = i ? cells.haven[i] : min; if (f.type !== "lake") return;
riversData.push({river: ri, cell: oh, x: p[min][0], y: p[min][1]}); const gridCell = cells.g[f.firstCell];
const mf = features[cells.f[min]]; // feature of min cell
if (mf.type === "lake") { // lake possible outlet: cell around with min height
if (!mf.river || iFlux > mf.flux) { f.outCell = f.shoreline[d3.scan(f.shoreline, (a,b) => h[a] - h[b])];
mf.river = ri; // pour water to lake lakeOutCells[f.outCell] = f.i;
mf.flux = iFlux; // entering flux
} // default flux: sum of precipition around lake first cell
mf.totalFlux = (mf.totalFlux || 0) + iFlux; f.flux = rn(d3.sum(f.shoreline.map(c => grid.cells.prec[cells.g[c]])) / 2);
if (mf.inlets) {
mf.inlets.push(ri); // temperature and evaporation to detect closed lakes
} else { f.temp = f.cells < 6 ? grid.cells.temp[gridCell] : rn(d3.mean(f.shoreline.map(c => grid.cells.temp[cells.g[c]])), 1);
mf.inlets = [ri]; const height = (f.height - 18) ** heightExponentInput.value; // height in meters
} const evaporation = (700 * (f.temp + .006 * height) / 50 + 75) / (80 - f.temp); // based on Penman formula, [1-11]
} f.evaporation = rn(evaporation * f.cells);
} else { });
cells.fl[min] += iFlux; // propagate flux
riversData.push({river: ri, cell: min, x: p[min][0], y: p[min][1]}); // add next River segment
}
}
land.forEach(function(i) { land.forEach(function(i) {
cells.fl[i] += grid.cells.prec[cells.g[i]]; // flux from precipitation cells.fl[i] += grid.cells.prec[cells.g[i]]; // flux from precipitation
const x = p[i][0], y = p[i][1]; const x = p[i][0], y = p[i][1];
// lake outlets draw from lake // create lake outlet if flux > evaporation
let n = -1, out2 = 0; const lakes = !lakeOutCells[i] ? [] : features.filter(feature => i === feature.outCell && feature.flux > feature.evaporation);
while (outlets.includes(i, n+1)) { for (const lake of lakes) {
n = outlets.indexOf(i, n+1); const lakeCell = cells.c[i].find(c => h[c] < 20 && cells.f[c] === lake.i);
const l = features[n];
if (!l) continue;
const j = cells.haven[i];
// allow chain lakes to retain identity // allow chain lakes to retain identity
if(cells.r[j] !== l.river) { if (cells.r[lakeCell] !== lake.river) {
let touch = false; const sameRiver = cells.c[lakeCell].some(c => cells.r[c] === lake.river);
for (const c of cells.c[j]){ if (sameRiver) {
if (cells.r[c] === l.river) { cells.r[lakeCell] = lake.river;
touch = true; riversData.push({river: lake.river, cell: lakeCell, x: p[lakeCell][0], y: p[lakeCell][1]});
break;
}
}
if (touch) {
cells.r[j] = l.river;
riversData.push({river: l.river, cell: j, x: p[j][0], y: p[j][1]});
} else { } else {
cells.r[j] = riverNext; cells.r[lakeCell] = riverNext;
riversData.push({river: riverNext, cell: j, x: p[j][0], y: p[j][1]}); riversData.push({river: riverNext, cell: lakeCell, x: p[lakeCell][0], y: p[lakeCell][1]});
riverNext++; riverNext++;
} }
} }
cells.fl[j] = l.totalFlux; // signpost river size
flowDown(i, cells.fl[i], l.totalFlux, cells.r[j]); lake.outlet = cells.r[lakeCell];
// prevent dropping imediately back into the lake cells.fl[lakeCell] += Math.max(lake.flux - lake.evaporation, 0); // not evaporated lake water drains to outlet
out2 = cells.c[i].filter(c => (h[c] >= 20 || cells.f[c] !== cells.f[j])).sort((a,b) => h[a] - h[b])[0]; // downhill cell not in the source lake flowDown(i, cells.fl[i], cells.fl[lakeCell], lake.outlet);
// assign all to outlet basin
if (l.inlets) l.inlets.forEach(fork => riversData.find(r => r.river === fork).parent = cells.r[j]);
} }
// near-border cell: pour out of the screen // assign all tributary rivers to outlet basin
if (cells.b[i]) { for (let outlet = lakes[0]?.outlet, l = 0; l < lakes.length; l++) {
if (cells.r[i]) { lakes[l].inlets?.forEach(fork => riversData.find(r => r.river === fork).parent = outlet);
}
// near-border cell: pour water out of the screen
if (cells.b[i] && cells.r[i]) {
const to = []; const to = [];
const min = Math.min(y, graphHeight - y, x, graphWidth - x); const min = Math.min(y, graphHeight - y, x, graphWidth - x);
if (min === y) {to[0] = x; to[1] = 0;} else if (min === y) {to[0] = x; to[1] = 0;} else
@ -134,18 +118,20 @@ const generate = function(changeHeights = true) {
if (min === x) {to[0] = 0; to[1] = y;} else if (min === x) {to[0] = 0; to[1] = y;} else
if (min === graphWidth - x) {to[0] = graphWidth; to[1] = y;} if (min === graphWidth - x) {to[0] = graphWidth; to[1] = y;}
riversData.push({river: cells.r[i], cell: i, x: to[0], y: to[1]}); riversData.push({river: cells.r[i], cell: i, x: to[0], y: to[1]});
}
return; return;
} }
const min = out2 || cells.c[i][d3.scan(cells.c[i], (a, b) => h[a] - h[b])]; // downhill cell // downhill cell (make sure it's not in the source lake)
const min = lakeOutCells[i]
? cells.c[i].filter(c => !lakes.map(lake => lake.i).includes(cells.f[c])).sort((a, b) => h[a] - h[b])[0]
: cells.c[i].sort((a, b) => h[a] - h[b])[0];
if (cells.fl[i] < 30) { if (cells.fl[i] < MIN_FLUX_TO_FORM_RIVER) {
if (h[min] >= 20) cells.fl[min] += cells.fl[i]; if (h[min] >= 20) cells.fl[min] += cells.fl[i];
return; // flux is too small to operate as river return; // flux is too small to operate as river
} }
// Proclaim a new river // proclaim a new river
if (!cells.r[i]) { if (!cells.r[i]) {
cells.r[i] = riverNext; cells.r[i] = riverNext;
riversData.push({river: riverNext, cell: i, x, y}); riversData.push({river: riverNext, cell: i, x, y});
@ -153,11 +139,44 @@ const generate = function(changeHeights = true) {
} }
flowDown(min, cells.fl[min], cells.fl[i], cells.r[i], i); flowDown(min, cells.fl[min], cells.fl[i], cells.r[i], i);
}); });
}() }
void function defineRivers() { function flowDown(toCell, toFlux, fromFlux, river, fromCell = 0) {
if (cells.r[toCell]) {
// downhill cell already has river assigned
if (toFlux < fromFlux) {
cells.conf[toCell] = cells.fl[toCell]; // mark confluence
if (h[toCell] >= 20) riversData.find(r => r.river === cells.r[toCell]).parent = river; // min river is a tributary of current river
cells.r[toCell] = river; // re-assign river if downhill part has less flux
} else {
cells.conf[toCell] += fromFlux; // mark confluence
if (h[toCell] >= 20) riversData.find(r => r.river === river).parent = cells.r[toCell]; // current river is a tributary of min river
}
} else cells.r[toCell] = river; // assign the river to the downhill cell
if (h[toCell] < 20) {
// pour water to the water body
const haven = fromCell ? cells.haven[fromCell] : toCell;
riversData.push({river, cell: haven, x: p[toCell][0], y: p[toCell][1]});
const waterBody = features[cells.f[toCell]];
if (waterBody.type === "lake") {
if (!waterBody.river || fromFlux > waterBody.enteringFlux) {
waterBody.river = river;
waterBody.enteringFlux = fromFlux;
}
waterBody.flux = waterBody.flux + fromFlux;
waterBody.inlets ? waterBody.inlets.push(river) : waterBody.inlets = [river];
}
} else {
// propagate flux and add next river segment
cells.fl[toCell] += fromFlux;
riversData.push({river, cell: toCell, x: p[toCell][0], y: p[toCell][1]});
}
}
function defineRivers() {
pack.rivers = []; // rivers data pack.rivers = []; // rivers data
const riverPaths = []; // temporary data for all rivers const riverPaths = []; // temporary data for all rivers
@ -184,59 +203,62 @@ const generate = function(changeHeights = true) {
rivers.selectAll("path").data(riverPaths).enter() rivers.selectAll("path").data(riverPaths).enter()
.append("path").attr("d", d => d[1]).attr("id", d => "river"+d[0]) .append("path").attr("d", d => d[1]).attr("id", d => "river"+d[0])
.attr("data-width", d => d[2]).attr("data-increment", d => d[3]); .attr("data-width", d => d[2]).attr("data-increment", d => d[3]);
}() }
// apply change heights as basic one function cleanupLakeData() {
if (changeHeights) cells.h = Uint8Array.from(h); for (const feature of features) {
if (feature.type !== "lake") continue;
delete feature.river;
delete feature.enteringFlux;
delete feature.shoreline;
delete feature.outCell;
feature.height = rn(feature.height);
TIME && console.timeEnd('generateRivers'); const inlets = feature.inlets?.filter(r => pack.rivers.find(river => river.i === r));
if (!inlets || !inlets.length) delete feature.inlets;
else feature.inlets = inlets;
const outlet = feature.outlet && pack.rivers.find(river => river.i === feature.outlet);
if (!outlet) delete feature.outlet;
}
}
} }
// depression filling algorithm (for a correct water flux modeling) // depression filling algorithm (for a correct water flux modeling)
const resolveDepressions = function(h) { const resolveDepressions = function(h) {
const cells = pack.cells; const {cells, features} = pack;
const land = cells.i.filter(i => h[i] >= 20 && h[i] < 100 && !cells.b[i]); // exclude near-border cells
const lakes = pack.features.filter(f => f.type === "lake" && (f.group === "freshwater" || f.group === "frozen")); // to keep lakes flat const lakes = features.filter(f => f.type === "lake");
lakes.forEach(l => { lakes.forEach(l => {
l.shoreline = []; const uniqueCells = new Set();
l.height = 21; l.vertices.forEach(v => pack.vertices.c[v].forEach(c => cells.h[c] >= 20 && uniqueCells.add(c)));
l.totalFlux = grid.cells.prec[cells.g[l.firstCell]]; l.shoreline = [...uniqueCells];
}); l.height = 21;
for (let i of land.filter(i => cells.t[i] === 1)) { // select shoreline cells });
cells.c[i].map(c => pack.features[cells.f[c]]).forEach(cf => {
if (lakes.includes(cf) && !cf.shoreline.includes(i)) { const land = cells.i.filter(i => h[i] >= 20 && h[i] < 100 && !cells.b[i]); // exclude near-border cells
cf.shoreline.push(i); land.sort((a,b) => h[b] - h[a]); // highest cells go first
}
}) let depressions = Infinity;
} for (let l = 0; depressions && l < 100; l++) {
land.sort((a,b) => h[b] - h[a]); // highest cells go first depressions = 0;
let depressed = false;
for (let l = 0, depression = Infinity; depression && l < 100; l++) {
depression = 0;
for (const l of lakes) { for (const l of lakes) {
const minHeight = d3.min(l.shoreline.map(s => h[s])); const minHeight = d3.min(l.shoreline.map(s => h[s]));
if (minHeight === 100) continue; // already max height if (minHeight >= 100 || l.height > minHeight) continue;
if (l.height <= minHeight) { l.height = minHeight + 1;
l.height = Math.min(rn(minHeight + 1), 100); depressions++;
depression++;
depressed = true;
}
} }
for (const i of land) { for (const i of land) {
const minHeight = d3.min(cells.c[i].map(c => cells.t[c] > 0 ? h[c] : const minHeight = d3.min(cells.c[i].map(c => cells.t[c] > 0 ? h[c] : pack.features[cells.f[c]].height || h[c]));
pack.features[cells.f[c]].height || h[c] // NB undefined is falsy (a || b is short for a ? a : b) if (minHeight >= 100 || h[i] > minHeight) continue;
)); h[i] = minHeight + 1;
if (minHeight === 100) continue; // already max height depressions++;
if (h[i] <= minHeight) {
h[i] = Math.min(minHeight + 1, 100);
depression++;
depressed = true;
}
} }
} }
return depressed; depressions && ERROR && console.error("Heightmap is depressed. Issues with rivers expected. Remove depressed areas to resolve");
} }
// add more river points on 1/3 and 2/3 of length // add more river points on 1/3 and 2/3 of length

View file

@ -238,12 +238,7 @@ function toDMS(coord, c) {
function getElevation(f, h) { function getElevation(f, h) {
if (f.land) return getHeight(h) + " (" + h + ")"; // land: usual height if (f.land) return getHeight(h) + " (" + h + ")"; // land: usual height
if (f.border) return "0 " + heightUnit.value; // ocean: 0 if (f.border) return "0 " + heightUnit.value; // ocean: 0
if (f.type === "lake") return getHeight(f.height) + " (" + f.height + ")"; // lake: defined on river generation
// lake: lowest coast height - 1
const lakeCells = Array.from(pack.cells.i.filter(i => pack.cells.f[i] === f.i));
const heights = lakeCells.map(i => pack.cells.c[i].map(c => pack.cells.h[c])).flat().filter(h => h > 19);
const elevation = (d3.min(heights)||20) - 1;
return getHeight(elevation) + " (" + elevation + ")";
} }
// get water depth // get water depth

View file

@ -185,6 +185,7 @@ function editHeightmap() {
} }
} }
defineLakesGroup();
defineBiomes(); defineBiomes();
rankCells(); rankCells();
Cultures.generate(); Cultures.generate();

View file

@ -434,7 +434,7 @@ function randomizeOptions() {
if (randomize || !locked("culturesSet")) randomizeCultureSet(); if (randomize || !locked("culturesSet")) randomizeCultureSet();
// 'Configure World' settings // 'Configure World' settings
if (randomize || !locked("prec")) precInput.value = precOutput.value = gauss(120, 20, 5, 500); if (randomize || !locked("prec")) precInput.value = precOutput.value = gauss(100, 40, 5, 500);
const tMax = +temperatureEquatorOutput.max, tMin = +temperatureEquatorOutput.min; // temperature extremes const tMax = +temperatureEquatorOutput.max, tMin = +temperatureEquatorOutput.min; // temperature extremes
if (randomize || !locked("temperatureEquator")) temperatureEquatorOutput.value = temperatureEquatorInput.value = rand(tMax-6, tMax); if (randomize || !locked("temperatureEquator")) temperatureEquatorOutput.value = temperatureEquatorInput.value = rand(tMax-6, tMax);
if (randomize || !locked("temperaturePole")) temperaturePoleOutput.value = temperaturePoleInput.value = rand(tMin, tMin+10); if (randomize || !locked("temperaturePole")) temperaturePoleOutput.value = temperaturePoleInput.value = rand(tMin, tMin+10);

View file

@ -95,6 +95,7 @@ async function openEmblemEditor() {
function regenerateRivers() { function regenerateRivers() {
Rivers.generate(); Rivers.generate();
defineLakesGroup();
Rivers.specify(); Rivers.specify();
if (!layerIsOn("toggleRivers")) toggleRivers(); if (!layerIsOn("toggleRivers")) toggleRivers();
} }

View file

@ -47,6 +47,7 @@ function editWorld() {
generatePrecipitation(); generatePrecipitation();
const heights = new Uint8Array(pack.cells.h); const heights = new Uint8Array(pack.cells.h);
Rivers.generate(); Rivers.generate();
defineLakesGroup();
Rivers.specify(); Rivers.specify();
pack.cells.h = new Float32Array(heights); pack.cells.h = new Float32Array(heights);
defineBiomes(); defineBiomes();