mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 01:41:22 +01:00
v1.5.76 - major river-lake redesign
This commit is contained in:
parent
8563aaf50d
commit
07f0eff52c
7 changed files with 227 additions and 194 deletions
77
main.js
77
main.js
|
|
@ -547,6 +547,7 @@ function generate() {
|
|||
drawCoastline();
|
||||
|
||||
Rivers.generate();
|
||||
defineLakesGroup();
|
||||
defineBiomes();
|
||||
|
||||
rankCells();
|
||||
|
|
@ -673,18 +674,13 @@ function markFeatures() {
|
|||
TIME && console.timeEnd("markFeatures");
|
||||
}
|
||||
|
||||
// How to handle lakes generated near seas? They can be both open or closed.
|
||||
// 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).
|
||||
// near sea lakes usually get a lot of water inflow, most of them should brake treshold and flow out to sea (see Ancylus Lake)
|
||||
function openNearSeaLakes() {
|
||||
if (templateInput.value === "Atoll") return; // no need for Atolls
|
||||
const cells = grid.cells, features = grid.features;
|
||||
if (!features.find(f => f.type === "lake")) return; // no lakes
|
||||
TIME && console.time("openLakes");
|
||||
const limit = 50; // max height that can be breached by water
|
||||
|
||||
for (let t = 0, removed = true; t < 5 && removed; t++) {
|
||||
removed = false;
|
||||
const LIMIT = 22; // max height that can be breached by water
|
||||
|
||||
for (const i of cells.i) {
|
||||
const lake = cells.f[i];
|
||||
|
|
@ -692,19 +688,17 @@ function openNearSeaLakes() {
|
|||
|
||||
check_neighbours:
|
||||
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]) {
|
||||
const ocean = cells.f[n];
|
||||
if (features[ocean].type !== "ocean") continue; // not an ocean
|
||||
removed = removeLake(c, lake, ocean);
|
||||
removeLake(c, lake, ocean);
|
||||
break check_neighbours;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function removeLake(treshold, lake, ocean) {
|
||||
cells.h[treshold] = 19;
|
||||
cells.t[treshold] = -1;
|
||||
|
|
@ -713,7 +707,7 @@ function openNearSeaLakes() {
|
|||
if (cells.h[c] >= 20) cells.t[c] = 1; // mark as coastline
|
||||
});
|
||||
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");
|
||||
|
|
@ -999,7 +993,7 @@ function drawCoastline() {
|
|||
if (features[f].type === "lake") {
|
||||
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
|
||||
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 {
|
||||
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);
|
||||
|
|
@ -1105,23 +1099,12 @@ function reMarkFeatures() {
|
|||
|
||||
const type = land ? "island" : border ? "ocean" : "lake";
|
||||
let group;
|
||||
if (type === "lake") group = defineLakeGroup(start, cellNumber, temp[cells.g[start]]);
|
||||
else if (type === "ocean") group = defineOceanGroup(cellNumber);
|
||||
if (type === "ocean") group = defineOceanGroup(cellNumber);
|
||||
else if (type === "island") group = defineIslandGroup(start, cellNumber);
|
||||
features.push({i, land, border, type, cells: cellNumber, firstCell: start, group});
|
||||
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) {
|
||||
if (number > grid.cells.i.length / 25) return "ocean";
|
||||
if (number > grid.cells.i.length / 100) return "sea";
|
||||
|
|
@ -1138,6 +1121,34 @@ function 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
|
||||
function 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
|
||||
function 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.pop = new Float32Array(cells.i.length); // cell population array
|
||||
|
||||
|
|
@ -1190,12 +1201,14 @@ function rankCells() {
|
|||
|
||||
if (cells.t[i] === 1) {
|
||||
if (cells.r[i]) s += 15; // estuary is valued
|
||||
const type = f[cells.f[cells.haven[i]]].type;
|
||||
const group = f[cells.f[cells.haven[i]]].group;
|
||||
if (type === "lake") {
|
||||
// lake coast is valued
|
||||
if (group === "freshwater") s += 30;
|
||||
else if (group !== "lava" && group !== "dry") s += 10;
|
||||
const feature = features[cells.f[cells.haven[i]]];
|
||||
if (feature.type === "lake") {
|
||||
if (feature.group === "freshwater") s += 30;
|
||||
else if (feature.group == "salt") s += 10;
|
||||
else if (feature.group == "frozen") s += 1;
|
||||
else if (feature.group == "dry") s -= 5;
|
||||
else if (feature.group == "sinkhole") s -= 5;
|
||||
else if (feature.group == "lava") s -= 30;
|
||||
} else {
|
||||
s += 5; // ocean coast is valued
|
||||
if (cells.harbor[i] === 1) s += 20; // safe sea harbor is valued
|
||||
|
|
|
|||
|
|
@ -9,124 +9,108 @@ const generate = function(changeHeights = true) {
|
|||
Math.random = aleaPRNG(seed);
|
||||
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)
|
||||
void function markupLand() {
|
||||
function markupLand() {
|
||||
const q = t => cells.i.filter(i => cells.t[i] === t);
|
||||
for (let t = 2, queue = q(t); queue.length; t++, queue = q(t)) {
|
||||
queue.forEach(i => cells.c[i].forEach(c => {
|
||||
if (!cells.t[c]) cells.t[c] = t+1;
|
||||
}));
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// height with added t value to make map less depressed
|
||||
function alterHeights() {
|
||||
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 + d3.mean(cells.c[i].map(c => cells.t[c])) / 10000);
|
||||
|
||||
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])];
|
||||
return h;
|
||||
}
|
||||
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){
|
||||
if (cells.r[min]) { // downhill cell already has river assigned
|
||||
if (mFlux < iFlux) {
|
||||
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
|
||||
function drainWater() {
|
||||
const MIN_FLUX_TO_FORM_RIVER = 30;
|
||||
const land = cells.i.filter(i => h[i] >= 20).sort((a,b) => h[b] - h[a]);
|
||||
|
||||
if (h[min] < 20) {
|
||||
// pour water to the sea haven
|
||||
const oh = i ? cells.haven[i] : min;
|
||||
riversData.push({river: ri, cell: oh, x: p[min][0], y: p[min][1]});
|
||||
const mf = features[cells.f[min]]; // feature of min cell
|
||||
if (mf.type === "lake") {
|
||||
if (!mf.river || iFlux > mf.flux) {
|
||||
mf.river = ri; // pour water to lake
|
||||
mf.flux = iFlux; // entering flux
|
||||
}
|
||||
mf.totalFlux = (mf.totalFlux || 0) + iFlux;
|
||||
if (mf.inlets) {
|
||||
mf.inlets.push(ri);
|
||||
} else {
|
||||
mf.inlets = [ri];
|
||||
}
|
||||
}
|
||||
} 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
|
||||
}
|
||||
}
|
||||
const lakeOutCells = new Uint16Array(cells.i.length); // to enumerate lake outlet positions
|
||||
features.forEach(f => {
|
||||
if (f.type !== "lake") return;
|
||||
const gridCell = cells.g[f.firstCell];
|
||||
|
||||
// lake possible outlet: cell around with min height
|
||||
f.outCell = f.shoreline[d3.scan(f.shoreline, (a,b) => h[a] - h[b])];
|
||||
lakeOutCells[f.outCell] = f.i;
|
||||
|
||||
// default flux: sum of precipition around lake first cell
|
||||
f.flux = rn(d3.sum(f.shoreline.map(c => grid.cells.prec[cells.g[c]])) / 2);
|
||||
|
||||
// temperature and evaporation to detect closed lakes
|
||||
f.temp = f.cells < 6 ? grid.cells.temp[gridCell] : rn(d3.mean(f.shoreline.map(c => grid.cells.temp[cells.g[c]])), 1);
|
||||
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);
|
||||
});
|
||||
|
||||
land.forEach(function(i) {
|
||||
cells.fl[i] += grid.cells.prec[cells.g[i]]; // flux from precipitation
|
||||
const x = p[i][0], y = p[i][1];
|
||||
|
||||
// lake outlets draw from lake
|
||||
let n = -1, out2 = 0;
|
||||
while (outlets.includes(i, n+1)) {
|
||||
n = outlets.indexOf(i, n+1);
|
||||
const l = features[n];
|
||||
if (!l) continue;
|
||||
const j = cells.haven[i];
|
||||
// create lake outlet if flux > evaporation
|
||||
const lakes = !lakeOutCells[i] ? [] : features.filter(feature => i === feature.outCell && feature.flux > feature.evaporation);
|
||||
for (const lake of lakes) {
|
||||
const lakeCell = cells.c[i].find(c => h[c] < 20 && cells.f[c] === lake.i);
|
||||
|
||||
// allow chain lakes to retain identity
|
||||
if(cells.r[j] !== l.river) {
|
||||
let touch = false;
|
||||
for (const c of cells.c[j]){
|
||||
if (cells.r[c] === l.river) {
|
||||
touch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (touch) {
|
||||
cells.r[j] = l.river;
|
||||
riversData.push({river: l.river, cell: j, x: p[j][0], y: p[j][1]});
|
||||
if (cells.r[lakeCell] !== lake.river) {
|
||||
const sameRiver = cells.c[lakeCell].some(c => cells.r[c] === lake.river);
|
||||
if (sameRiver) {
|
||||
cells.r[lakeCell] = lake.river;
|
||||
riversData.push({river: lake.river, cell: lakeCell, x: p[lakeCell][0], y: p[lakeCell][1]});
|
||||
} else {
|
||||
cells.r[j] = riverNext;
|
||||
riversData.push({river: riverNext, cell: j, x: p[j][0], y: p[j][1]});
|
||||
cells.r[lakeCell] = riverNext;
|
||||
riversData.push({river: riverNext, cell: lakeCell, x: p[lakeCell][0], y: p[lakeCell][1]});
|
||||
riverNext++;
|
||||
}
|
||||
}
|
||||
cells.fl[j] = l.totalFlux; // signpost river size
|
||||
flowDown(i, cells.fl[i], l.totalFlux, cells.r[j]);
|
||||
// prevent dropping imediately back into the lake
|
||||
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
|
||||
// assign all to outlet basin
|
||||
if (l.inlets) l.inlets.forEach(fork => riversData.find(r => r.river === fork).parent = cells.r[j]);
|
||||
|
||||
lake.outlet = cells.r[lakeCell];
|
||||
cells.fl[lakeCell] += Math.max(lake.flux - lake.evaporation, 0); // not evaporated lake water drains to outlet
|
||||
flowDown(i, cells.fl[i], cells.fl[lakeCell], lake.outlet);
|
||||
}
|
||||
|
||||
// near-border cell: pour out of the screen
|
||||
if (cells.b[i]) {
|
||||
if (cells.r[i]) {
|
||||
// assign all tributary rivers to outlet basin
|
||||
for (let outlet = lakes[0]?.outlet, l = 0; l < lakes.length; l++) {
|
||||
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 min = Math.min(y, graphHeight - y, x, graphWidth - x);
|
||||
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 === graphWidth - x) {to[0] = graphWidth; to[1] = y;}
|
||||
riversData.push({river: cells.r[i], cell: i, x: to[0], y: to[1]});
|
||||
}
|
||||
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];
|
||||
return; // flux is too small to operate as river
|
||||
}
|
||||
|
||||
// Proclaim a new river
|
||||
// proclaim a new river
|
||||
if (!cells.r[i]) {
|
||||
cells.r[i] = riverNext;
|
||||
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);
|
||||
|
||||
});
|
||||
}()
|
||||
}
|
||||
|
||||
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
|
||||
const riverPaths = []; // temporary data for all rivers
|
||||
|
||||
|
|
@ -184,59 +203,62 @@ const generate = function(changeHeights = true) {
|
|||
rivers.selectAll("path").data(riverPaths).enter()
|
||||
.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]);
|
||||
}()
|
||||
}
|
||||
|
||||
// apply change heights as basic one
|
||||
if (changeHeights) cells.h = Uint8Array.from(h);
|
||||
function cleanupLakeData() {
|
||||
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)
|
||||
const resolveDepressions = function(h) {
|
||||
const cells = pack.cells;
|
||||
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
|
||||
lakes.forEach(l => {
|
||||
l.shoreline = [];
|
||||
l.height = 21;
|
||||
l.totalFlux = grid.cells.prec[cells.g[l.firstCell]];
|
||||
});
|
||||
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)) {
|
||||
cf.shoreline.push(i);
|
||||
}
|
||||
})
|
||||
}
|
||||
land.sort((a,b) => h[b] - h[a]); // highest cells go first
|
||||
let depressed = false;
|
||||
const {cells, features} = pack;
|
||||
|
||||
const lakes = features.filter(f => f.type === "lake");
|
||||
lakes.forEach(l => {
|
||||
const uniqueCells = new Set();
|
||||
l.vertices.forEach(v => pack.vertices.c[v].forEach(c => cells.h[c] >= 20 && uniqueCells.add(c)));
|
||||
l.shoreline = [...uniqueCells];
|
||||
l.height = 21;
|
||||
});
|
||||
|
||||
const land = cells.i.filter(i => h[i] >= 20 && h[i] < 100 && !cells.b[i]); // exclude near-border cells
|
||||
land.sort((a,b) => h[b] - h[a]); // highest cells go first
|
||||
|
||||
let depressions = Infinity;
|
||||
for (let l = 0; depressions && l < 100; l++) {
|
||||
depressions = 0;
|
||||
|
||||
for (let l = 0, depression = Infinity; depression && l < 100; l++) {
|
||||
depression = 0;
|
||||
for (const l of lakes) {
|
||||
const minHeight = d3.min(l.shoreline.map(s => h[s]));
|
||||
if (minHeight === 100) continue; // already max height
|
||||
if (l.height <= minHeight) {
|
||||
l.height = Math.min(rn(minHeight + 1), 100);
|
||||
depression++;
|
||||
depressed = true;
|
||||
}
|
||||
if (minHeight >= 100 || l.height > minHeight) continue;
|
||||
l.height = minHeight + 1;
|
||||
depressions++;
|
||||
}
|
||||
|
||||
for (const i of land) {
|
||||
const minHeight = d3.min(cells.c[i].map(c => cells.t[c] > 0 ? 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) continue; // already max height
|
||||
if (h[i] <= minHeight) {
|
||||
h[i] = Math.min(minHeight + 1, 100);
|
||||
depression++;
|
||||
depressed = true;
|
||||
}
|
||||
const minHeight = d3.min(cells.c[i].map(c => cells.t[c] > 0 ? h[c] : pack.features[cells.f[c]].height || h[c]));
|
||||
if (minHeight >= 100 || h[i] > minHeight) continue;
|
||||
h[i] = minHeight + 1;
|
||||
depressions++;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -238,12 +238,7 @@ function toDMS(coord, c) {
|
|||
function getElevation(f, h) {
|
||||
if (f.land) return getHeight(h) + " (" + h + ")"; // land: usual height
|
||||
if (f.border) return "0 " + heightUnit.value; // ocean: 0
|
||||
|
||||
// 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 + ")";
|
||||
if (f.type === "lake") return getHeight(f.height) + " (" + f.height + ")"; // lake: defined on river generation
|
||||
}
|
||||
|
||||
// get water depth
|
||||
|
|
|
|||
|
|
@ -185,6 +185,7 @@ function editHeightmap() {
|
|||
}
|
||||
}
|
||||
|
||||
defineLakesGroup();
|
||||
defineBiomes();
|
||||
rankCells();
|
||||
Cultures.generate();
|
||||
|
|
|
|||
|
|
@ -434,7 +434,7 @@ function randomizeOptions() {
|
|||
if (randomize || !locked("culturesSet")) randomizeCultureSet();
|
||||
|
||||
// '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
|
||||
if (randomize || !locked("temperatureEquator")) temperatureEquatorOutput.value = temperatureEquatorInput.value = rand(tMax-6, tMax);
|
||||
if (randomize || !locked("temperaturePole")) temperaturePoleOutput.value = temperaturePoleInput.value = rand(tMin, tMin+10);
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ async function openEmblemEditor() {
|
|||
|
||||
function regenerateRivers() {
|
||||
Rivers.generate();
|
||||
defineLakesGroup();
|
||||
Rivers.specify();
|
||||
if (!layerIsOn("toggleRivers")) toggleRivers();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ function editWorld() {
|
|||
generatePrecipitation();
|
||||
const heights = new Uint8Array(pack.cells.h);
|
||||
Rivers.generate();
|
||||
defineLakesGroup();
|
||||
Rivers.specify();
|
||||
pack.cells.h = new Float32Array(heights);
|
||||
defineBiomes();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue