diff --git a/index.html b/index.html
index a46d059b..b8d31638 100644
--- a/index.html
+++ b/index.html
@@ -7641,7 +7641,7 @@
-
+
diff --git a/src/constants/index.ts b/src/config/constants.ts
similarity index 100%
rename from src/constants/index.ts
rename to src/config/constants.ts
diff --git a/src/layers/renderers/drawRivers.ts b/src/layers/renderers/drawRivers.ts
index 860418bf..43ea0b1f 100644
--- a/src/layers/renderers/drawRivers.ts
+++ b/src/layers/renderers/drawRivers.ts
@@ -1,3 +1,5 @@
+import {pick} from "utils/functionUtils";
+
export function drawRivers(pack: IPack) {
rivers.selectAll("*").remove();
@@ -12,7 +14,7 @@ export function drawRivers(pack: IPack) {
points = undefined;
}
- const meanderedPoints = addMeandering(pack, cells, points);
+ const meanderedPoints = addMeandering(pick(pack.cells, "fl", "conf", "h", "p"), cells, points);
const path = getRiverPath(meanderedPoints, widthFactor, sourceWidth);
return ``;
});
diff --git a/src/modules/lakes.ts b/src/modules/lakes.ts
index 2f2f69af..ea419169 100644
--- a/src/modules/lakes.ts
+++ b/src/modules/lakes.ts
@@ -1,70 +1,13 @@
-// @ts-nocheckd
+// @ts-nocheck
import * as d3 from "d3";
import {TIME} from "config/logging";
-import {rn} from "utils/numberUtils";
import {aleaPRNG} from "scripts/aleaPRNG";
import {getInputNumber, getInputValue} from "utils/nodeUtils";
import {DISTANCE_FIELD, MIN_LAND_HEIGHT} from "config/generation";
import {byId} from "utils/shorthands";
-import {getRealHeight} from "utils/unitUtils";
window.Lakes = (function () {
- const setClimateData = function (
- heights: Uint8Array,
- lakes: IPackFeatureLake[],
- gridReference: IPack["cells"]["g"],
- precipitation: IGrid["cells"]["prec"],
- temperature: IGrid["cells"]["temp"]
- ) {
- const lakeOutCells = new Uint16Array(gridReference.length);
-
- for (const lake of lakes) {
- const {firstCell, shoreline} = lake;
-
- // default flux: sum of precipitation around lake
- lake.flux = shoreline.reduce((acc, cellId) => acc + precipitation[gridReference[cellId]], 0);
-
- // temperature and evaporation to detect closed lakes
- lake.temp =
- lake.cells < 6
- ? temperature[gridReference[firstCell]]
- : rn(d3.mean(shoreline.map(cellId => temperature[gridReference[cellId]]))!, 1);
-
- const height = getRealHeight(lake.height); // height in meters
- const evaporation = ((700 * (lake.temp + 0.006 * height)) / 50 + 75) / (80 - lake.temp); // based on Penman formula, [1-11]
- lake.evaporation = rn(evaporation * lake.cells);
-
- // no outlet for lakes in depressed areas
- // if (lake.closed) continue;
-
- // lake outlet cell
- const outCell = shoreline[d3.scan(shoreline, (a, b) => heights[a] - heights[b])!];
- lake.outCell = outCell;
- lakeOutCells[lake.outCell] = lake.i;
- }
-
- return lakeOutCells;
- };
-
- const cleanupLakeData = function (pack: IPack) {
- for (const feature of pack.features) {
- if (feature.type !== "lake") continue;
- delete feature.river;
- delete feature.enteringFlux;
- delete feature.outCell;
- delete feature.closed;
- feature.height = rn(feature.height, 3);
-
- 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;
- }
- };
-
const defineGroup = function (pack: IPack) {
for (const feature of pack.features) {
if (feature && feature.type === "lake") {
@@ -220,8 +163,6 @@ window.Lakes = (function () {
}
return {
- setClimateData,
- cleanupLakeData,
defineGroup,
generateName,
getName,
diff --git a/src/modules/markup.ts b/src/modules/markup.ts
index b9c79bba..f7c01d87 100644
--- a/src/modules/markup.ts
+++ b/src/modules/markup.ts
@@ -2,7 +2,7 @@ import * as d3 from "d3";
import {DISTANCE_FIELD, MIN_LAND_HEIGHT} from "config/generation";
import {TIME} from "config/logging";
-import {INT8_MAX} from "constants";
+import {INT8_MAX} from "config/constants";
import {aleaPRNG} from "scripts/aleaPRNG";
import {getFeatureVertices} from "scripts/connectVertices";
import {createTypedArray, unique} from "utils/arrayUtils";
@@ -254,7 +254,7 @@ function addFeature({
function getLakeElevation() {
const MIN_ELEVATION_DELTA = 0.1;
const minShoreHeight = d3.min(shoreline.map(cellId => heights[cellId])) || MIN_LAND_HEIGHT;
- return minShoreHeight - MIN_ELEVATION_DELTA;
+ return rn(minShoreHeight - MIN_ELEVATION_DELTA, 2);
}
const feature: IPackFeatureLake = {
diff --git a/src/modules/river-generator.ts b/src/modules/river-generator.ts
deleted file mode 100644
index 97669393..00000000
--- a/src/modules/river-generator.ts
+++ /dev/null
@@ -1,620 +0,0 @@
-import * as d3 from "d3";
-
-import {TIME, WARN} from "config/logging";
-import {last} from "utils/arrayUtils";
-import {rn} from "utils/numberUtils";
-import {round} from "utils/stringUtils";
-import {rw, each} from "utils/probabilityUtils";
-import {aleaPRNG} from "scripts/aleaPRNG";
-import {DISTANCE_FIELD, MAX_HEIGHT, MIN_LAND_HEIGHT} from "config/generation";
-import {getInputNumber} from "utils/nodeUtils";
-import {pick} from "utils/functionUtils";
-import {byId} from "utils/shorthands";
-
-const {Lakes} = window;
-const {LAND_COAST} = DISTANCE_FIELD;
-
-window.Rivers = (function () {
- const generate = function (
- precipitation: IGrid["cells"]["prec"],
- temperature: IGrid["cells"]["temp"],
- cells: Pick,
- features: TPackFeatures,
- allowErosion = true
- ) {
- TIME && console.time("generateRivers");
-
- Math.random = aleaPRNG(seed);
-
- const riversData = {}; // rivers data
- const riverParents = {};
-
- const cellsNumber = cells.i.length;
- const riverIds = new Uint16Array(cellsNumber);
- const confluence = new Uint8Array(cellsNumber);
-
- let nextRiverId = 1; // starts with 1
-
- const gradientHeights = alterHeights({h: cells.h, c: cells.c, t: cells.t});
- const [currentCellHeights, currentLakeHeights] = resolveDepressions(
- pick(cells, "i", "c", "b", "f"),
- features,
- gradientHeights
- );
-
- const flux = drainWater();
- defineRivers();
-
- calculateConfluenceFlux();
- Lakes.cleanupLakeData(pack);
-
- if (allowErosion) {
- cells.h = Uint8Array.from(currentCellHeights); // mutate heightmap
- downcutRivers(); // downcut river beds
- }
-
- TIME && console.timeEnd("generateRivers");
-
- function drainWater() {
- const MIN_FLUX_TO_FORM_RIVER = 30;
- const points = Number(byId("pointsInput")?.dataset.cells);
- const cellsNumberModifier = (points / 10000) ** 0.25;
-
- const land = cells.i.filter(i => currentCellHeights[i] >= MIN_LAND_HEIGHT);
- land.sort((a, b) => currentCellHeights[b] - currentCellHeights[a]);
-
- const flux = new Uint16Array(cellsNumber);
-
- const lakes = features.filter(feature => feature && feature.type === "lake") as IPackFeatureLake[];
- const lakeOutCells = Lakes.setClimateData(currentCellHeights, lakes, cells.g, precipitation, temperature);
-
- land.forEach(cellId => {
- flux[cellId] += precipitation[cells.g[cellId]] / cellsNumberModifier;
-
- // create lake outlet if lake is not in deep depression and flux > evaporation
- const openLakes = lakeOutCells[cellId]
- ? lakes.filter(({outCell, flux = 0, evaporation = 0}) => cellId === outCell && flux > evaporation)
- : [];
-
- for (const lake of openLakes) {
- const lakeCell = cells.c[cellId].find(c => currentCellHeights[c] < MIN_LAND_HEIGHT && cells.f[c] === lake.i);
- flux[lakeCell] += Math.max(lake.flux - lake.evaporation, 0); // not evaporated lake water drains to outlet
-
- // allow chain lakes to retain identity
- if (riverIds[lakeCell] !== lake.river) {
- const sameRiver = cells.c[lakeCell].some(c => riverIds[c] === lake.river);
-
- if (sameRiver) {
- riverIds[lakeCell] = lake.river;
- addCellToRiver(lakeCell, lake.river);
- } else {
- riverIds[lakeCell] = nextRiverId;
- addCellToRiver(lakeCell, nextRiverId);
- nextRiverId++;
- }
- }
-
- lake.outlet = riverIds[lakeCell];
- flowDown(cellId, flux[lakeCell], lake.outlet);
- }
-
- // assign all tributary rivers to outlet basin
- const outlet = openLakes[0]?.outlet;
- for (const lake of openLakes) {
- if (!Array.isArray(lake.inlets)) continue;
- for (const inlet of lake.inlets) {
- riverParents[inlet] = outlet;
- }
- }
-
- // near-border cell: pour water out of the screen
- if (cells.b[cellId] && riverIds[cellId]) return addCellToRiver(-1, riverIds[cellId]);
-
- // downhill cell (make sure it's not in the source lake)
- let min = null;
- if (lakeOutCells[cellId]) {
- const filtered = cells.c[cellId].filter(c => !openLakes.map(lake => lake.i).includes(cells.f[c]));
- min = filtered.sort((a, b) => alteredHeights[a] - alteredHeights[b])[0];
- } else if (cells.haven[cellId]) {
- min = cells.haven[cellId];
- } else {
- min = cells.c[cellId].sort((a, b) => alteredHeights[a] - alteredHeights[b])[0];
- }
-
- // cells is depressed
- if (alteredHeights[cellId] <= alteredHeights[min]) return;
-
- // debug
- // .append("line")
- // .attr("x1", pack.cells.p[i][0])
- // .attr("y1", pack.cells.p[i][1])
- // .attr("x2", pack.cells.p[min][0])
- // .attr("y2", pack.cells.p[min][1])
- // .attr("stroke", "#333")
- // .attr("stroke-width", 0.2);
-
- if (flux[cellId] < MIN_FLUX_TO_FORM_RIVER) {
- // flux is too small to operate as a river
- if (alteredHeights[min] >= 20) flux[min] += flux[cellId];
- return;
- }
-
- // proclaim a new river
- if (!riverIds[cellId]) {
- riverIds[cellId] = nextRiverId;
- addCellToRiver(cellId, nextRiverId);
- nextRiverId++;
- }
-
- flowDown(min, flux[cellId], riverIds[cellId]);
- });
-
- return flux;
- }
-
- function addCellToRiver(cellId: number, riverId: number) {
- if (!riversData[riverId]) riversData[riverId] = [cellId];
- else riversData[riverId].push(cellId);
- }
-
- function flowDown(toCell, fromFlux, river) {
- const toFlux = flux[toCell] - confluence[toCell];
- const toRiver = riverIds[toCell];
-
- if (toRiver) {
- // downhill cell already has river assigned
- if (fromFlux > toFlux) {
- confluence[toCell] += flux[toCell]; // mark confluence
- if (alteredHeights[toCell] >= 20) riverParents[toRiver] = river; // min river is a tributary of current river
- riverIds[toCell] = river; // re-assign river if downhill part has less flux
- } else {
- confluence[toCell] += fromFlux; // mark confluence
- if (alteredHeights[toCell] >= 20) riverParents[river] = toRiver; // current river is a tributary of min river
- }
- } else riverIds[toCell] = river; // assign the river to the downhill cell
-
- if (alteredHeights[toCell] < 20) {
- // pour water to the water body
- 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;
- if (!waterBody.inlets) waterBody.inlets = [river];
- else waterBody.inlets.push(river);
- }
- } else {
- // propagate flux and add next river segment
- flux[toCell] += fromFlux;
- }
-
- addCellToRiver(toCell, river);
- }
-
- function defineRivers() {
- // re-initialize rivers and confluence arrays
- riverIds = new Uint16Array(cellsNumber);
- confluence = new Uint16Array(cellsNumber);
- pack.rivers = [];
-
- const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2);
- const mainStemWidthFactor = defaultWidthFactor * 1.2;
-
- for (const key in riversData) {
- const riverCells = riversData[key];
- if (riverCells.length < 3) continue; // exclude tiny rivers
-
- const riverId = +key;
- for (const cell of riverCells) {
- if (cell < 0 || cells.h[cell] < 20) continue;
-
- // mark real confluences and assign river to cells
- if (riverIds[cell]) confluence[cell] = 1;
- else riverIds[cell] = riverId;
- }
-
- const source = riverCells[0];
- const mouth = riverCells[riverCells.length - 2];
- const parent = riverParents[key] || 0;
-
- const widthFactor = !parent || parent === riverId ? mainStemWidthFactor : defaultWidthFactor;
- const meanderedPoints = addMeandering(pack, riverCells);
- const discharge = flux[mouth]; // m3 in second
- const length = getApproximateLength(meanderedPoints);
- const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, 0));
-
- pack.rivers.push({
- i: riverId,
- source,
- mouth,
- discharge,
- length,
- width,
- widthFactor,
- sourceWidth: 0,
- parent,
- cells: riverCells
- });
- }
- }
-
- function downcutRivers() {
- const MAX_DOWNCUT = 5;
-
- for (const i of pack.cells.i) {
- if (cells.h[i] < 35) continue; // don't donwcut lowlands
- if (!flux[i]) continue;
-
- const higherCells = cells.c[i].filter(c => cells.h[c] > cells.h[i]);
- const higherFlux = higherCells.reduce((acc, c) => acc + flux[c], 0) / higherCells.length;
- if (!higherFlux) continue;
-
- const downcut = Math.floor(flux[i] / higherFlux);
- if (downcut) cells.h[i] -= Math.min(downcut, MAX_DOWNCUT);
- }
- }
-
- function calculateConfluenceFlux() {
- for (const i of cells.i) {
- if (!confluence[i]) continue;
-
- const sortedInflux = cells.c[i]
- .filter(c => riverIds[c] && alteredHeights[c] > alteredHeights[i])
- .map(c => flux[c])
- .sort((a, b) => b - a);
- confluence[i] = sortedInflux.reduce((acc, flux, index) => (index ? acc + flux : acc), 0);
- }
- }
- };
-
- // add distance to water value to land cells to make map less depressed
- const alterHeights = ({h, c, t}: Pick) => {
- return Array.from(h).map((height, index) => {
- if (height < MIN_LAND_HEIGHT || t[index] < LAND_COAST) return height;
- const mean = d3.mean(c[index].map(c => t[c])) || 0;
- return height + t[index] / 100 + mean / 10000;
- });
- };
-
- // depression filling algorithm (for a correct water flux modeling)
- const resolveDepressions = function (
- cells: Pick,
- features: TPackFeatures,
- heights: number[]
- ): [number[], Dict] {
- const MAX_INTERATIONS = getInputNumber("resolveDepressionsStepsOutput");
- const checkLakeMaxIteration = MAX_INTERATIONS * 0.85;
- const elevateLakeMaxIteration = MAX_INTERATIONS * 0.75;
-
- const ELEVATION_LIMIT = getInputNumber("lakeElevationLimitOutput");
-
- const LAND_ELEVATION_INCREMENT = 0.1;
- const LAKE_ELEVATION_INCREMENT = 0.2;
-
- const lakes = features.filter(feature => feature && feature.type === "lake") as IPackFeatureLake[];
- lakes.sort((a, b) => a.height - b.height); // lowest lakes go first
-
- const currentCellHeights = Array.from(heights);
- const currentLakeHeights = Object.fromEntries(lakes.map(({i, height}) => [i, height]));
-
- const getHeight = (i: number) => currentLakeHeights[cells.f[i]] || currentCellHeights[i];
- const getMinHeight = (cellsIds: number[]) => Math.min(...cellsIds.map(getHeight));
-
- const drainableLakes = checkLakesDrainability();
-
- const landCells = cells.i.filter(i => heights[i] >= MIN_LAND_HEIGHT && !cells.b[i]);
- landCells.sort((a, b) => heights[a] - heights[b]); // lowest cells go first
-
- const depressions: number[] = [];
-
- for (let iteration = 0; iteration && depressions.at(-1) && iteration < MAX_INTERATIONS; iteration++) {
- let depressionsLeft = 0;
-
- // elevate potentially drainable lakes
- if (iteration < checkLakeMaxIteration) {
- for (const lake of lakes) {
- if (drainableLakes[lake.i] !== true) continue;
-
- const minShoreHeight = getMinHeight(lake.shoreline);
- if (minShoreHeight >= MAX_HEIGHT || lake.height > minShoreHeight) continue;
-
- if (iteration > elevateLakeMaxIteration) {
- for (const shoreCellId of lake.shoreline) {
- // reset heights
- currentCellHeights[shoreCellId] = heights[shoreCellId];
- currentLakeHeights[lake.i] = lake.height;
- }
-
- drainableLakes[lake.i] = false;
- continue;
- }
-
- currentLakeHeights[lake.i] = minShoreHeight + LAKE_ELEVATION_INCREMENT;
- depressionsLeft++;
- }
- }
-
- for (const cellId of landCells) {
- const minHeight = getMinHeight(cells.c[cellId]);
- if (minHeight >= MAX_HEIGHT || currentCellHeights[cellId] > minHeight) continue;
-
- currentCellHeights[cellId] = minHeight + LAND_ELEVATION_INCREMENT;
- depressionsLeft++;
- }
-
- depressions.push(depressionsLeft);
-
- // check depression resolving progress
- if (depressions.length > 5) {
- const depressionsInitial = depressions.at(0) || 0;
- const depressiosRecently = depressions.at(-6) || 0;
-
- const isProgressingOverall = depressionsInitial < depressionsLeft;
- if (!isProgressingOverall) return [heights, Object.fromEntries(lakes.map(({i, height}) => [i, height]))];
-
- const isProgressingRecently = depressiosRecently < depressionsLeft;
- if (!isProgressingRecently) return [currentCellHeights, currentLakeHeights];
- }
- }
-
- // define lakes that potentially can be open (drained into another water body)
- function checkLakesDrainability() {
- const canBeDrained: Dict = {}; // all false by default
- const drainAllLakes = ELEVATION_LIMIT === MAX_HEIGHT - MIN_LAND_HEIGHT;
-
- for (const lake of lakes) {
- if (drainAllLakes) {
- canBeDrained[lake.i] = true;
- continue;
- }
-
- canBeDrained[lake.i] = false;
- const minShoreHeight = getMinHeight(lake.shoreline);
- const minHeightShoreCell =
- lake.shoreline.find(cellId => heights[cellId] === minShoreHeight) || lake.shoreline[0];
-
- const queue = [minHeightShoreCell];
- const checked = [];
- checked[minHeightShoreCell] = true;
- const breakableHeight = lake.height + ELEVATION_LIMIT;
-
- loopCellsAroundLake: while (queue.length) {
- const cellId = queue.pop()!;
-
- for (const neibCellId of cells.c[cellId]) {
- if (checked[neibCellId]) continue;
- if (heights[neibCellId] >= breakableHeight) continue;
-
- if (heights[neibCellId] < MIN_LAND_HEIGHT) {
- const waterFeatureMet = features[cells.f[neibCellId]];
- const isOceanMet = waterFeatureMet && waterFeatureMet.type === "ocean";
- const isLakeMet = waterFeatureMet && waterFeatureMet.type === "lake";
-
- if (isOceanMet || (isLakeMet && lake.height > waterFeatureMet.height)) {
- canBeDrained[lake.i] = true;
- break loopCellsAroundLake;
- }
- }
-
- checked[neibCellId] = true;
- queue.push(neibCellId);
- }
- }
- }
-
- return canBeDrained;
- }
-
- depressions && WARN && console.warn(`Unresolved depressions: ${depressions}. Edit heightmap to fix`);
-
- return [currentCellHeights, currentLakeHeights];
- };
-
- // add points at 1/3 and 2/3 of a line between adjacents river cells
- const addMeandering = (pack, riverCells, riverPoints = null, meandering = 0.5) => {
- const {fl, conf, h} = pack.cells;
- const meandered = [];
- const lastStep = riverCells.length - 1;
- const points = getRiverPoints(pack, riverCells, riverPoints);
- let step = h[riverCells[0]] < 20 ? 1 : 10;
-
- let fluxPrev = 0;
- const getFlux = (step, flux) => (step === lastStep ? fluxPrev : flux);
-
- for (let i = 0; i <= lastStep; i++, step++) {
- const cell = riverCells[i];
- const isLastCell = i === lastStep;
-
- const [x1, y1] = points[i];
- const flux1 = getFlux(i, fl[cell]);
- fluxPrev = flux1;
-
- meandered.push([x1, y1, flux1]);
- if (isLastCell) break;
-
- const nextCell = riverCells[i + 1];
- const [x2, y2] = points[i + 1];
-
- if (nextCell === -1) {
- meandered.push([x2, y2, fluxPrev]);
- break;
- }
-
- const dist2 = (x2 - x1) ** 2 + (y2 - y1) ** 2; // square distance between cells
- if (dist2 <= 25 && riverCells.length >= 6) continue;
-
- const flux2 = getFlux(i + 1, fl[nextCell]);
- const keepInitialFlux = conf[nextCell] || flux1 === flux2;
-
- const meander = meandering + 1 / step + Math.max(meandering - step / 100, 0);
- const angle = Math.atan2(y2 - y1, x2 - x1);
- const sinMeander = Math.sin(angle) * meander;
- const cosMeander = Math.cos(angle) * meander;
-
- if (step < 10 && (dist2 > 64 || (dist2 > 36 && riverCells.length < 5))) {
- // if dist2 is big or river is small add extra points at 1/3 and 2/3 of segment
- const p1x = (x1 * 2 + x2) / 3 + -sinMeander;
- const p1y = (y1 * 2 + y2) / 3 + cosMeander;
- const p2x = (x1 + x2 * 2) / 3 + sinMeander / 2;
- const p2y = (y1 + y2 * 2) / 3 - cosMeander / 2;
- const [p1fl, p2fl] = keepInitialFlux ? [flux1, flux1] : [(flux1 * 2 + flux2) / 3, (flux1 + flux2 * 2) / 3];
- meandered.push([p1x, p1y, p1fl], [p2x, p2y, p2fl]);
- } else if (dist2 > 25 || riverCells.length < 6) {
- // if dist is medium or river is small add 1 extra middlepoint
- const p1x = (x1 + x2) / 2 + -sinMeander;
- const p1y = (y1 + y2) / 2 + cosMeander;
- const p1fl = keepInitialFlux ? flux1 : (flux1 + flux2) / 2;
- meandered.push([p1x, p1y, p1fl]);
- }
- }
-
- return meandered;
- };
-
- const getRiverPoints = (pack, riverCells, riverPoints) => {
- if (riverPoints) return riverPoints;
-
- const {p} = pack.cells;
- return riverCells.map((cell, i) => {
- if (cell === -1) return getBorderPoint(pack, riverCells[i - 1]);
- return p[cell];
- });
- };
-
- const getBorderPoint = (pack, i) => {
- const [x, y] = pack.cells.p[i];
- const min = Math.min(y, graphHeight - y, x, graphWidth - x);
- if (min === y) return [x, 0];
- else if (min === graphHeight - y) return [x, graphHeight];
- else if (min === x) return [0, y];
- return [graphWidth, y];
- };
-
- const FLUX_FACTOR = 500;
- const MAX_FLUX_WIDTH = 2;
- const LENGTH_FACTOR = 200;
- const STEP_WIDTH = 1 / LENGTH_FACTOR;
- const LENGTH_PROGRESSION = [1, 1, 2, 3, 5, 8, 13, 21, 34].map(n => n / LENGTH_FACTOR);
- const MAX_PROGRESSION = last(LENGTH_PROGRESSION);
-
- const getOffset = (flux, pointNumber, widthFactor, startingWidth = 0) => {
- const fluxWidth = Math.min(flux ** 0.9 / FLUX_FACTOR, MAX_FLUX_WIDTH);
- const lengthWidth = pointNumber * STEP_WIDTH + (LENGTH_PROGRESSION[pointNumber] || MAX_PROGRESSION);
- return widthFactor * (lengthWidth + fluxWidth) + startingWidth;
- };
-
- const lineGen = d3.line().curve(d3.curveBasis);
-
- // build polygon from a list of points and calculated offset (width)
- const getRiverPath = function (points, widthFactor, startingWidth = 0) {
- const riverPointsLeft = [];
- const riverPointsRight = [];
-
- for (let p = 0; p < points.length; p++) {
- const [x0, y0] = points[p - 1] || points[p];
- const [x1, y1, flux] = points[p];
- const [x2, y2] = points[p + 1] || points[p];
-
- const offset = getOffset(flux, p, widthFactor, startingWidth);
- const angle = Math.atan2(y0 - y2, x0 - x2);
- const sinOffset = Math.sin(angle) * offset;
- const cosOffset = Math.cos(angle) * offset;
-
- riverPointsLeft.push([x1 - sinOffset, y1 + cosOffset]);
- riverPointsRight.push([x1 + sinOffset, y1 - cosOffset]);
- }
-
- const right = lineGen(riverPointsRight.reverse());
- let left = lineGen(riverPointsLeft);
- left = left.substring(left.indexOf("C"));
-
- return round(right + left, 1);
- };
-
- const specify = function () {
- const rivers = pack.rivers;
- if (!rivers.length) return;
-
- for (const river of rivers) {
- river.basin = getBasin(river.i);
- river.name = getName(river.mouth);
- river.type = getType(river);
- }
- };
-
- const getName = function (cell) {
- return Names.getCulture(pack.cells.culture[cell]);
- };
-
- // weighted arrays of river type names
- const riverTypes = {
- main: {
- big: {River: 1},
- small: {Creek: 9, River: 3, Brook: 3, Stream: 1}
- },
- fork: {
- big: {Fork: 1},
- small: {Branch: 1}
- }
- };
-
- let smallLength = null;
- const getType = function ({i, length, parent}) {
- if (smallLength === null) {
- const threshold = Math.ceil(pack.rivers.length * 0.15);
- smallLength = pack.rivers.map(r => r.length || 0).sort((a, b) => a - b)[threshold];
- }
-
- const isSmall = length < smallLength;
- const isFork = each(3)(i) && parent && parent !== i;
- return rw(riverTypes[isFork ? "fork" : "main"][isSmall ? "small" : "big"]);
- };
-
- const getApproximateLength = points => {
- const length = points.reduce((s, v, i, p) => s + (i ? Math.hypot(v[0] - p[i - 1][0], v[1] - p[i - 1][1]) : 0), 0);
- return rn(length, 2);
- };
-
- // Real mouth width examples: Amazon 6000m, Volga 6000m, Dniepr 3000m, Mississippi 1300m, Themes 900m,
- // Danube 800m, Daugava 600m, Neva 500m, Nile 450m, Don 400m, Wisla 300m, Pripyat 150m, Bug 140m, Muchavets 40m
- const getWidth = (offset: number) => rn((offset / 1.5) ** 1.8, 2); // mouth width in km
-
- // remove river and all its tributaries
- const remove = function (id: number) {
- const cells = pack.cells;
- const riversToRemove = pack.rivers.filter(r => r.i === id || r.parent === id || r.basin === id).map(r => r.i);
- riversToRemove.forEach(r => rivers.select("#river" + r).remove());
- cells.r.forEach((r, i) => {
- if (!r || !riversToRemove.includes(r)) return;
- cells.r[i] = 0;
- cells.fl[i] = grid.cells.prec[cells.g[i]];
- cells.conf[i] = 0;
- });
- pack.rivers = pack.rivers.filter(r => !riversToRemove.includes(r.i));
- };
-
- const getBasin = function (riverId: number) {
- const parent = pack.rivers.find(river => river.i === riverId)?.parent;
- if (!parent || riverId === parent) return riverId;
- return getBasin(parent);
- };
-
- return {
- generate,
- alterHeights,
- resolveDepressions,
- addMeandering,
- getRiverPath,
- specify,
- getName,
- getType,
- getBasin,
- getWidth,
- getOffset,
- getApproximateLength,
- getRiverPoints,
- remove
- };
-})();
diff --git a/src/modules/rivers.js b/src/modules/rivers.js
new file mode 100644
index 00000000..b0148789
--- /dev/null
+++ b/src/modules/rivers.js
@@ -0,0 +1,210 @@
+import * as d3 from "d3";
+
+import {last} from "utils/arrayUtils";
+import {rn} from "utils/numberUtils";
+import {round} from "utils/stringUtils";
+import {rw, each} from "utils/probabilityUtils";
+import {MIN_LAND_HEIGHT} from "config/generation";
+
+window.Rivers = (function () {
+ // add points at 1/3 and 2/3 of a line between adjacents river cells
+ const addMeandering = ({fl, conf, h, p}, riverCells, riverPoints = null, meandering = 0.5) => {
+ const meandered = [];
+ const lastStep = riverCells.length - 1;
+ const points = getRiverPoints(p, riverCells, riverPoints);
+ let step = h[riverCells[0]] < MIN_LAND_HEIGHT ? 1 : 10;
+
+ let fluxPrev = 0;
+ const getFlux = (step, flux) => (step === lastStep ? fluxPrev : flux);
+
+ for (let i = 0; i <= lastStep; i++, step++) {
+ const cell = riverCells[i];
+ const isLastCell = i === lastStep;
+
+ const [x1, y1] = points[i];
+ const flux1 = getFlux(i, fl[cell]);
+ fluxPrev = flux1;
+
+ meandered.push([x1, y1, flux1]);
+ if (isLastCell) break;
+
+ const nextCell = riverCells[i + 1];
+ const [x2, y2] = points[i + 1];
+
+ if (nextCell === -1) {
+ meandered.push([x2, y2, fluxPrev]);
+ break;
+ }
+
+ const dist2 = (x2 - x1) ** 2 + (y2 - y1) ** 2; // square distance between cells
+ if (dist2 <= 25 && riverCells.length >= 6) continue;
+
+ const flux2 = getFlux(i + 1, fl[nextCell]);
+ const keepInitialFlux = conf[nextCell] || flux1 === flux2;
+
+ const meander = meandering + 1 / step + Math.max(meandering - step / 100, 0);
+ const angle = Math.atan2(y2 - y1, x2 - x1);
+ const sinMeander = Math.sin(angle) * meander;
+ const cosMeander = Math.cos(angle) * meander;
+
+ if (step < 10 && (dist2 > 64 || (dist2 > 36 && riverCells.length < 5))) {
+ // if dist2 is big or river is small add extra points at 1/3 and 2/3 of segment
+ const p1x = (x1 * 2 + x2) / 3 + -sinMeander;
+ const p1y = (y1 * 2 + y2) / 3 + cosMeander;
+ const p2x = (x1 + x2 * 2) / 3 + sinMeander / 2;
+ const p2y = (y1 + y2 * 2) / 3 - cosMeander / 2;
+ const [p1fl, p2fl] = keepInitialFlux ? [flux1, flux1] : [(flux1 * 2 + flux2) / 3, (flux1 + flux2 * 2) / 3];
+ meandered.push([p1x, p1y, p1fl], [p2x, p2y, p2fl]);
+ } else if (dist2 > 25 || riverCells.length < 6) {
+ // if dist is medium or river is small add 1 extra middlepoint
+ const p1x = (x1 + x2) / 2 + -sinMeander;
+ const p1y = (y1 + y2) / 2 + cosMeander;
+ const p1fl = keepInitialFlux ? flux1 : (flux1 + flux2) / 2;
+ meandered.push([p1x, p1y, p1fl]);
+ }
+ }
+
+ return meandered;
+ };
+
+ const getRiverPoints = (points, riverCells, riverPoints) => {
+ if (riverPoints) return riverPoints;
+
+ return riverCells.map((cell, i) => {
+ if (cell === -1) return getBorderPoint(points, riverCells[i - 1]);
+ return points[cell];
+ });
+ };
+
+ const getBorderPoint = (points, i) => {
+ const [x, y] = points[i];
+ const min = Math.min(y, graphHeight - y, x, graphWidth - x);
+ if (min === y) return [x, 0];
+ else if (min === graphHeight - y) return [x, graphHeight];
+ else if (min === x) return [0, y];
+ return [graphWidth, y];
+ };
+
+ const FLUX_FACTOR = 500;
+ const MAX_FLUX_WIDTH = 2;
+ const LENGTH_FACTOR = 200;
+ const STEP_WIDTH = 1 / LENGTH_FACTOR;
+ const LENGTH_PROGRESSION = [1, 1, 2, 3, 5, 8, 13, 21, 34].map(n => n / LENGTH_FACTOR);
+ const MAX_PROGRESSION = last(LENGTH_PROGRESSION);
+
+ const getOffset = (flux, pointNumber, widthFactor, startingWidth = 0) => {
+ const fluxWidth = Math.min(flux ** 0.9 / FLUX_FACTOR, MAX_FLUX_WIDTH);
+ const lengthWidth = pointNumber * STEP_WIDTH + (LENGTH_PROGRESSION[pointNumber] || MAX_PROGRESSION);
+ return widthFactor * (lengthWidth + fluxWidth) + startingWidth;
+ };
+
+ const lineGen = d3.line().curve(d3.curveBasis);
+
+ // build polygon from a list of points and calculated offset (width)
+ const getRiverPath = function (points, widthFactor, startingWidth = 0) {
+ const riverPointsLeft = [];
+ const riverPointsRight = [];
+
+ for (let p = 0; p < points.length; p++) {
+ const [x0, y0] = points[p - 1] || points[p];
+ const [x1, y1, flux] = points[p];
+ const [x2, y2] = points[p + 1] || points[p];
+
+ const offset = getOffset(flux, p, widthFactor, startingWidth);
+ const angle = Math.atan2(y0 - y2, x0 - x2);
+ const sinOffset = Math.sin(angle) * offset;
+ const cosOffset = Math.cos(angle) * offset;
+
+ riverPointsLeft.push([x1 - sinOffset, y1 + cosOffset]);
+ riverPointsRight.push([x1 + sinOffset, y1 - cosOffset]);
+ }
+
+ const right = lineGen(riverPointsRight.reverse());
+ let left = lineGen(riverPointsLeft);
+ left = left.substring(left.indexOf("C"));
+
+ return round(right + left, 1);
+ };
+
+ const specify = function () {
+ const rivers = pack.rivers;
+ if (!rivers.length) return;
+
+ for (const river of rivers) {
+ river.basin = getBasin(river.i);
+ river.name = getName(river.mouth);
+ river.type = getType(river);
+ }
+ };
+
+ const getName = function (cell) {
+ return Names.getCulture(pack.cells.culture[cell]);
+ };
+
+ // weighted arrays of river type names
+ const riverTypes = {
+ main: {
+ big: {River: 1},
+ small: {Creek: 9, River: 3, Brook: 3, Stream: 1}
+ },
+ fork: {
+ big: {Fork: 1},
+ small: {Branch: 1}
+ }
+ };
+
+ let smallLength = null;
+ const getType = function ({i, length, parent}) {
+ if (smallLength === null) {
+ const threshold = Math.ceil(pack.rivers.length * 0.15);
+ smallLength = pack.rivers.map(r => r.length || 0).sort((a, b) => a - b)[threshold];
+ }
+
+ const isSmall = length < smallLength;
+ const isFork = each(3)(i) && parent && parent !== i;
+ return rw(riverTypes[isFork ? "fork" : "main"][isSmall ? "small" : "big"]);
+ };
+
+ const getApproximateLength = points => {
+ const length = points.reduce((s, v, i, p) => s + (i ? Math.hypot(v[0] - p[i - 1][0], v[1] - p[i - 1][1]) : 0), 0);
+ return rn(length, 2);
+ };
+
+ // Real mouth width examples: Amazon 6000m, Volga 6000m, Dniepr 3000m, Mississippi 1300m, Themes 900m,
+ // Danube 800m, Daugava 600m, Neva 500m, Nile 450m, Don 400m, Wisla 300m, Pripyat 150m, Bug 140m, Muchavets 40m
+ const getWidth = offset => rn((offset / 1.5) ** 1.8, 2); // mouth width in km
+
+ // remove river and all its tributaries
+ const remove = function (id) {
+ const cells = pack.cells;
+ const riversToRemove = pack.rivers.filter(r => r.i === id || r.parent === id || r.basin === id).map(r => r.i);
+ riversToRemove.forEach(r => rivers.select("#river" + r).remove());
+ cells.r.forEach((r, i) => {
+ if (!r || !riversToRemove.includes(r)) return;
+ cells.r[i] = 0;
+ cells.fl[i] = grid.cells.prec[cells.g[i]];
+ cells.conf[i] = 0;
+ });
+ pack.rivers = pack.rivers.filter(r => !riversToRemove.includes(r.i));
+ };
+
+ const getBasin = function (riverId) {
+ const parent = pack.rivers.find(river => river.i === riverId)?.parent;
+ if (!parent || riverId === parent) return riverId;
+ return getBasin(parent);
+ };
+
+ return {
+ addMeandering,
+ getRiverPath,
+ specify,
+ getName,
+ getType,
+ getBasin,
+ getWidth,
+ getOffset,
+ getApproximateLength,
+ getRiverPoints,
+ remove
+ };
+})();
diff --git a/src/modules/ui/tools.js b/src/modules/ui/tools.js
index eff5f287..867fcd82 100644
--- a/src/modules/ui/tools.js
+++ b/src/modules/ui/tools.js
@@ -570,7 +570,7 @@ function addRiverOnClick() {
if (cells.b[i]) return;
const {
- alterHeights,
+ applyDistanceField,
resolveDepressions,
addMeandering,
getRiverPath,
@@ -588,7 +588,7 @@ function addRiverOnClick() {
const initialFlux = grid.cells.prec[cells.g[i]];
cells.fl[i] = initialFlux;
- const h = alterHeights(pacl.cells);
+ const h = applyDistanceField(pacl.cells);
resolveDepressions(pack, h);
while (i) {
diff --git a/src/scripts/generation/generation.ts b/src/scripts/generation/generation.ts
index e5d64387..00699cc0 100644
--- a/src/scripts/generation/generation.ts
+++ b/src/scripts/generation/generation.ts
@@ -26,7 +26,7 @@ import {generateSeed} from "utils/probabilityUtils";
import {byId} from "utils/shorthands";
import {showStatistics} from "../statistics";
import {createGrid} from "./grid";
-import {createPack} from "./pack";
+import {createPack} from "./pack/pack";
import {getInputValue, setInputValue} from "utils/nodeUtils";
// import {Ruler} from "modules/measurers";
@@ -68,7 +68,7 @@ async function generate(options?: IGenerationOptions) {
renderLayer("rivers", pack);
WARN && console.warn(`TOTAL: ${rn((performance.now() - timeStart) / 1000, 2)}s`);
- showStatistics();
+ // showStatistics();
INFO && console.groupEnd();
} catch (error) {
showGenerationError(error as Error);
diff --git a/src/scripts/generation/pack/lakes.ts b/src/scripts/generation/pack/lakes.ts
new file mode 100644
index 00000000..333ea114
--- /dev/null
+++ b/src/scripts/generation/pack/lakes.ts
@@ -0,0 +1,72 @@
+// @ts-nocheckd
+import * as d3 from "d3";
+
+import {rn} from "utils/numberUtils";
+import {getRealHeight} from "utils/unitUtils";
+
+export interface ILakeClimateData extends IPackFeatureLake {
+ flux: number;
+ temp: number;
+ evaporation: number;
+ outCell: number | undefined;
+ river?: number;
+ enteringFlux?: number;
+}
+
+export const getClimateData = function (
+ lakes: IPackFeatureLake[],
+ heights: number[],
+ drainableLakes: Dict,
+ gridReference: IPack["cells"]["g"],
+ precipitation: IGrid["cells"]["prec"],
+ temperature: IGrid["cells"]["temp"]
+): ILakeClimateData[] {
+ const lakeData = lakes.map(lake => {
+ const {shoreline} = lake;
+
+ // default flux: sum of precipitation around lake
+ const flux = shoreline.reduce((acc, cellId) => acc + precipitation[gridReference[cellId]], 0);
+
+ // temperature and evaporation to detect closed lakes
+ const temp = rn(d3.mean(shoreline.map(cellId => temperature[gridReference[cellId]]))!, 1);
+
+ const height = getRealHeight(lake.height); // height in meters
+ const cellEvaporation = ((700 * (temp + 0.006 * height)) / 50 + 75) / (80 - temp); // based on Penman formula, [1-11]
+ const evaporation = rn(cellEvaporation * lake.cells);
+
+ const outCell =
+ flux > evaporation && drainableLakes[lake.i]
+ ? shoreline[d3.scan(shoreline, (a, b) => heights[a] - heights[b])!]
+ : undefined;
+
+ return {...lake, flux, temp, evaporation, outCell};
+ });
+
+ return lakeData;
+};
+
+export const mergeLakeData = function (
+ features: TPackFeatures,
+ lakeData: ILakeClimateData[],
+ rivers: Pick[]
+) {
+ const updatedFeatures = features.map(feature => {
+ if (!feature) return 0;
+ if (feature.type !== "lake") return feature;
+
+ const lake = lakeData.find(lake => lake.i === feature.i);
+ if (!lake) return feature;
+
+ const {flux, temp, evaporation} = lake;
+ const inlets = lake.inlets?.filter(inlet => rivers.find(river => river.i === inlet));
+ const outlet = rivers.find(river => river.i === lake.outlet)?.i;
+
+ const lakeFeature: IPackFeatureLake = {...feature, flux, temp, evaporation, inlets, outlet};
+ if (!inlets || !inlets.length) delete lakeFeature.inlets;
+ if (!outlet) delete lakeFeature.outlet;
+
+ return lakeFeature;
+ });
+
+ return updatedFeatures as TPackFeatures;
+};
diff --git a/src/scripts/generation/pack.ts b/src/scripts/generation/pack/pack.ts
similarity index 90%
rename from src/scripts/generation/pack.ts
rename to src/scripts/generation/pack/pack.ts
index 1bde2e60..be15c068 100644
--- a/src/scripts/generation/pack.ts
+++ b/src/scripts/generation/pack/pack.ts
@@ -9,14 +9,15 @@ import {drawScaleBar} from "modules/measurers";
import {addZones} from "modules/zones";
import {DISTANCE_FIELD, MIN_LAND_HEIGHT} from "config/generation";
import {TIME} from "config/logging";
-import {UINT16_MAX} from "constants";
+import {UINT16_MAX} from "config/constants";
import {calculateVoronoi} from "scripts/generation/graph";
import {createTypedArray} from "utils/arrayUtils";
import {pick} from "utils/functionUtils";
import {rn} from "utils/numberUtils";
+import {generateRivers} from "./rivers";
const {LAND_COAST, WATER_COAST, DEEPER_WATER} = DISTANCE_FIELD;
-const {Lakes, OceanLayers, Rivers, Biomes, Cultures, BurgsAndStates, Religions, Military, Markers, Names} = window;
+// const {Lakes, OceanLayers, Biomes, Cultures, BurgsAndStates, Religions, Military, Markers, Names} = window;
export function createPack(grid: IGrid): IPack {
const {vertices, cells} = repackGrid(grid);
@@ -24,12 +25,11 @@ export function createPack(grid: IGrid): IPack {
const markup = markupPackFeatures(grid, vertices, pick(cells, "v", "c", "b", "p", "h"));
const {features, featureIds, distanceField, haven, harbor} = markup;
- Rivers.generate(
+ const {heights, flux, r, conf, rivers, mergedFeatures} = generateRivers(
grid.cells.prec,
grid.cells.temp,
- pick({...cells, f: featureIds, t: distanceField, haven}, "i", "c", "b", "g", "t", "h", "f", "haven"),
- features,
- true
+ pick({...cells, f: featureIds, t: distanceField, haven}, "i", "c", "b", "g", "t", "h", "f", "haven", "p"),
+ features
);
// Lakes.defineGroup(newPack);
@@ -66,12 +66,17 @@ export function createPack(grid: IGrid): IPack {
vertices,
cells: {
...cells,
+ h: new Uint8Array(heights),
f: featureIds,
t: distanceField,
haven,
- harbor
+ harbor,
+ fl: flux,
+ r,
+ conf
},
- features
+ features: mergedFeatures,
+ rivers
};
return pack;
diff --git a/src/scripts/generation/pack/rivers.ts b/src/scripts/generation/pack/rivers.ts
new file mode 100644
index 00000000..4d6132bd
--- /dev/null
+++ b/src/scripts/generation/pack/rivers.ts
@@ -0,0 +1,425 @@
+import * as d3 from "d3";
+
+import {TIME, WARN} from "config/logging";
+import {rn} from "utils/numberUtils";
+import {aleaPRNG} from "scripts/aleaPRNG";
+import {DISTANCE_FIELD, MAX_HEIGHT, MIN_LAND_HEIGHT} from "config/generation";
+import {getInputNumber} from "utils/nodeUtils";
+import {pick} from "utils/functionUtils";
+import {byId} from "utils/shorthands";
+import {mergeLakeData, getClimateData, ILakeClimateData} from "./lakes";
+
+const {Rivers} = window;
+const {LAND_COAST} = DISTANCE_FIELD;
+
+export function generateRivers(
+ precipitation: IGrid["cells"]["prec"],
+ temperature: IGrid["cells"]["temp"],
+ cells: Pick,
+ features: TPackFeatures
+) {
+ TIME && console.time("generateRivers");
+
+ Math.random = aleaPRNG(seed);
+
+ const riversData: {[river: string]: number[]} = {};
+ const riverParents: {[river: string]: number} = {};
+
+ const cellsNumber = cells.i.length;
+
+ let nextRiverId = 1; // starts with 1
+
+ const gradientHeights = applyDistanceField({h: cells.h, c: cells.c, t: cells.t});
+ const [currentCellHeights, drainableLakes] = resolveDepressions(
+ pick(cells, "i", "c", "b", "f"),
+ features,
+ gradientHeights
+ );
+
+ const points = Number(byId("pointsInput")?.dataset.cells);
+ const cellsNumberModifier = (points / 10000) ** 0.25;
+
+ const {flux, lakeData} = drainWater();
+ const {r, conf, rivers} = defineRivers();
+ const heights = downcutRivers(currentCellHeights);
+
+ const mergedFeatures = mergeLakeData(features, lakeData, rivers);
+
+ TIME && console.timeEnd("generateRivers");
+
+ return {heights, flux, r, conf, rivers, mergedFeatures};
+
+ function drainWater() {
+ const MIN_FLUX_TO_FORM_RIVER = 30;
+
+ const riverIds = new Uint16Array(cellsNumber);
+ const confluence = new Uint8Array(cellsNumber);
+ const flux = new Uint16Array(cellsNumber);
+
+ const lakes = features.filter(feature => feature && feature.type === "lake") as IPackFeatureLake[];
+
+ const lakeData: ILakeClimateData[] = getClimateData(
+ lakes,
+ currentCellHeights,
+ drainableLakes,
+ cells.g,
+ precipitation,
+ temperature
+ );
+ const openLakes = lakeData.filter(lake => lake.outCell !== undefined);
+
+ const land = cells.i.filter(i => currentCellHeights[i] >= MIN_LAND_HEIGHT);
+ land.sort((a, b) => currentCellHeights[b] - currentCellHeights[a]);
+
+ land.forEach(cellId => {
+ flux[cellId] += precipitation[cells.g[cellId]] / cellsNumberModifier;
+
+ const lakesDrainingToCell = openLakes.filter(lake => lake.outCell === cellId);
+ for (const lake of lakesDrainingToCell) {
+ const lakeCell = cells.c[cellId].find(c => currentCellHeights[c] < MIN_LAND_HEIGHT && cells.f[c] === lake.i);
+ if (!lakeCell) continue;
+
+ flux[lakeCell] += Math.max(lake.flux - lake.evaporation, 0); // not evaporated lake water drains to outlet
+
+ // allow to chain lakes to keep river identity
+ if (riverIds[lakeCell] !== lake.river) {
+ const sameRiver = cells.c[lakeCell].some(c => riverIds[c] === lake.river);
+
+ if (lake.river && sameRiver) {
+ riverIds[lakeCell] = lake.river;
+ addCellToRiver(lakeCell, lake.river);
+ } else {
+ riverIds[lakeCell] = nextRiverId;
+ addCellToRiver(lakeCell, nextRiverId);
+ nextRiverId++;
+ }
+ }
+
+ lake.outlet = riverIds[lakeCell];
+ flowDown(cellId, flux[lakeCell], lake.outlet);
+ }
+
+ if (lakesDrainingToCell.length && lakesDrainingToCell[0].outlet) {
+ // assign all tributary rivers to outlet basin
+ const outlet = lakesDrainingToCell[0].outlet;
+ for (const lakeDrainingToCell of lakesDrainingToCell) {
+ if (!Array.isArray(lakeDrainingToCell.inlets)) continue;
+ for (const inlet of lakeDrainingToCell.inlets) {
+ riverParents[inlet] = outlet;
+ }
+ }
+ }
+
+ // near-border cell: pour water out of the screen
+ if (cells.b[cellId] && riverIds[cellId]) return addCellToRiver(-1, riverIds[cellId]);
+
+ // downhill cell (make sure it's not in the source lake)
+ let min = null;
+ if (lakesDrainingToCell.length) {
+ const filtered = cells.c[cellId].filter(c => !lakesDrainingToCell.map(lake => lake.i).includes(cells.f[c]));
+ min = filtered.sort((a, b) => currentCellHeights[a] - currentCellHeights[b])[0];
+ } else if (cells.haven[cellId]) {
+ min = cells.haven[cellId];
+ } else {
+ min = cells.c[cellId].sort((a, b) => currentCellHeights[a] - currentCellHeights[b])[0];
+ }
+
+ // cells is depressed
+ if (currentCellHeights[cellId] <= currentCellHeights[min]) return;
+
+ debug
+ .append("line")
+ .attr("x1", cells.p[cellId][0])
+ .attr("y1", cells.p[cellId][1])
+ .attr("x2", cells.p[min][0])
+ .attr("y2", cells.p[min][1])
+ .attr("stroke", "#333")
+ .attr("stroke-width", 0.1);
+
+ if (flux[cellId] < MIN_FLUX_TO_FORM_RIVER) {
+ // flux is too small to operate as a river
+ if (currentCellHeights[min] >= MIN_LAND_HEIGHT) flux[min] += flux[cellId];
+ return;
+ }
+
+ // create a new river
+ if (!riverIds[cellId]) {
+ riverIds[cellId] = nextRiverId;
+ addCellToRiver(cellId, nextRiverId);
+ nextRiverId++;
+ }
+
+ flowDown(min, flux[cellId], riverIds[cellId]);
+ });
+
+ return {flux, lakeData};
+
+ function flowDown(toCell: number, fromFlux: number, riverId: number) {
+ const toFlux = flux[toCell] - confluence[toCell];
+ const toRiver = riverIds[toCell];
+
+ if (toRiver) {
+ // downhill cell already has river assigned
+ if (fromFlux > toFlux) {
+ confluence[toCell] += flux[toCell]; // mark confluence
+ if (currentCellHeights[toCell] >= MIN_LAND_HEIGHT) {
+ // min river is a tributary of current river
+ riverParents[toRiver] = riverId;
+ }
+ riverIds[toCell] = riverId; // re-assign river if downhill part has less flux
+ } else {
+ confluence[toCell] += fromFlux; // mark confluence
+ if (currentCellHeights[toCell] >= MIN_LAND_HEIGHT) {
+ // current river is a tributary of min river
+ riverParents[riverId] = toRiver;
+ }
+ }
+ } else riverIds[toCell] = riverId; // assign the river to the downhill cell
+
+ if (currentCellHeights[toCell] < MIN_LAND_HEIGHT) {
+ // pour water to the water body
+ const lake = lakeData.find(lake => lake.i === cells.f[toCell]);
+
+ if (lake) {
+ if (!lake.river || fromFlux > (lake.enteringFlux || 0)) {
+ lake.river = riverId;
+ lake.enteringFlux = fromFlux;
+ }
+ lake.flux = lake.flux + fromFlux;
+ if (lake.inlets) lake.inlets.push(riverId);
+ else lake.inlets = [riverId];
+ }
+ } else {
+ // propagate flux and add next river segment
+ flux[toCell] += fromFlux;
+ }
+
+ addCellToRiver(toCell, riverId);
+ }
+
+ function addCellToRiver(cellId: number, riverId: number) {
+ if (riversData[riverId]) riversData[riverId].push(cellId);
+ else riversData[riverId] = [cellId];
+ }
+ }
+
+ function defineRivers() {
+ const r = new Uint16Array(cellsNumber);
+ const conf = new Uint16Array(cellsNumber);
+ const rivers: Omit[] = [];
+
+ const defaultWidthFactor = rn(1 / cellsNumberModifier, 2);
+ const mainStemWidthFactor = defaultWidthFactor * 1.2;
+
+ for (const key in riversData) {
+ const riverId = +key;
+ const riverCells = riversData[key];
+ if (riverCells.length < 3) continue; // exclude tiny rivers
+
+ for (const cell of riverCells) {
+ if (cell < 0 || cells.h[cell] < MIN_LAND_HEIGHT) continue;
+
+ // mark confluences and assign river to cells
+ if (r[cell]) conf[cell] = 1;
+ else r[cell] = riverId;
+ }
+
+ const source = riverCells[0];
+ const mouth = riverCells.at(-2) || 0;
+ const parent = riverParents[key] || 0;
+
+ const widthFactor = !parent || parent === riverId ? mainStemWidthFactor : defaultWidthFactor;
+ const meanderedPoints: number[] = Rivers.addMeandering({fl: flux, conf, h: cells.h, p: cells.p}, riverCells);
+ const discharge = flux[mouth]; // m3 in second
+ const length: number = Rivers.getApproximateLength(meanderedPoints);
+ const width: number = Rivers.getWidth(Rivers.getOffset(discharge, meanderedPoints.length, widthFactor, 0));
+
+ rivers.push({
+ i: riverId,
+ source,
+ mouth,
+ discharge,
+ length,
+ width,
+ widthFactor,
+ sourceWidth: 0,
+ parent,
+ cells: riverCells
+ });
+ }
+
+ // calculate confluence flux
+ for (const i of cells.i) {
+ if (!conf[i]) continue;
+
+ const sortedInflux = cells.c[i]
+ .filter(c => r[c] && currentCellHeights[c] > currentCellHeights[i])
+ .map(c => flux[c])
+ .sort((a, b) => b - a);
+ conf[i] = sortedInflux.reduce((acc, flux, index) => (index ? acc + flux : acc), 0);
+ }
+
+ return {r, conf, rivers};
+ }
+
+ function downcutRivers(heights: number[]) {
+ const MAX_DOWNCUT = 5;
+ const MIN_HEIGHT_TO_DOWNCUT = 35;
+
+ for (const i of cells.i) {
+ if (heights[i] < MIN_HEIGHT_TO_DOWNCUT) continue; // don't downcut lowlands
+ if (!flux[i]) continue;
+
+ const higherCells = cells.c[i].filter(c => heights[c] > heights[i]);
+ const higherFlux = higherCells.reduce((acc, c) => acc + flux[c], 0) / higherCells.length;
+ if (!higherFlux) continue;
+
+ const downcut = Math.floor(flux[i] / higherFlux);
+ if (downcut) heights[i] -= Math.min(downcut, MAX_DOWNCUT);
+ }
+
+ return heights;
+ }
+}
+
+// add distance to water value to land cells to make map less depressed
+const applyDistanceField = ({h, c, t}: Pick) => {
+ return Array.from(h).map((height, index) => {
+ if (height < MIN_LAND_HEIGHT || t[index] < LAND_COAST) return height;
+ const mean = d3.mean(c[index].map(c => t[c])) || 0;
+ return height + t[index] / 100 + mean / 10000;
+ });
+};
+
+// depression filling algorithm (for a correct water flux modeling)
+const resolveDepressions = function (
+ cells: Pick,
+ features: TPackFeatures,
+ heights: number[]
+): [number[], Dict] {
+ const MAX_INTERATIONS = getInputNumber("resolveDepressionsStepsOutput");
+ const checkLakeMaxIteration = MAX_INTERATIONS * 0.85;
+ const elevateLakeMaxIteration = MAX_INTERATIONS * 0.75;
+
+ const ELEVATION_LIMIT = getInputNumber("lakeElevationLimitOutput");
+
+ const LAND_ELEVATION_INCREMENT = 0.1;
+ const LAKE_ELEVATION_INCREMENT = 0.2;
+
+ const lakes = features.filter(feature => feature && feature.type === "lake") as IPackFeatureLake[];
+ lakes.sort((a, b) => a.height - b.height); // lowest lakes go first
+
+ const currentCellHeights = Array.from(heights);
+ const currentLakeHeights = Object.fromEntries(lakes.map(({i, height}) => [i, height]));
+
+ const getHeight = (i: number) => currentLakeHeights[cells.f[i]] || currentCellHeights[i];
+ const getMinHeight = (cellsIds: number[]) => Math.min(...cellsIds.map(getHeight));
+
+ const drainableLakes = checkLakesDrainability();
+
+ const landCells = cells.i.filter(i => heights[i] >= MIN_LAND_HEIGHT && !cells.b[i]);
+ landCells.sort((a, b) => heights[a] - heights[b]); // lowest cells go first
+
+ const depressions: number[] = [];
+
+ for (let iteration = 0; iteration && depressions.at(-1) && iteration < MAX_INTERATIONS; iteration++) {
+ let depressionsLeft = 0;
+
+ // elevate potentially drainable lakes
+ if (iteration < checkLakeMaxIteration) {
+ for (const lake of lakes) {
+ if (drainableLakes[lake.i] !== true) continue;
+
+ const minShoreHeight = getMinHeight(lake.shoreline);
+ if (minShoreHeight >= MAX_HEIGHT || lake.height > minShoreHeight) continue;
+
+ if (iteration > elevateLakeMaxIteration) {
+ for (const shoreCellId of lake.shoreline) {
+ // reset heights
+ currentCellHeights[shoreCellId] = heights[shoreCellId];
+ currentLakeHeights[lake.i] = lake.height;
+ }
+
+ drainableLakes[lake.i] = false;
+ continue;
+ }
+
+ currentLakeHeights[lake.i] = minShoreHeight + LAKE_ELEVATION_INCREMENT;
+ depressionsLeft++;
+ }
+ }
+
+ for (const cellId of landCells) {
+ const minHeight = getMinHeight(cells.c[cellId]);
+ if (minHeight >= MAX_HEIGHT || currentCellHeights[cellId] > minHeight) continue;
+
+ currentCellHeights[cellId] = minHeight + LAND_ELEVATION_INCREMENT;
+ depressionsLeft++;
+ }
+
+ depressions.push(depressionsLeft);
+
+ // check depression resolving progress
+ if (depressions.length > 5) {
+ const depressionsInitial = depressions.at(0) || 0;
+ const depressiosRecently = depressions.at(-6) || 0;
+
+ const isProgressingOverall = depressionsInitial < depressionsLeft;
+ if (!isProgressingOverall) return [heights, drainableLakes];
+
+ const isProgressingRecently = depressiosRecently < depressionsLeft;
+ if (!isProgressingRecently) return [currentCellHeights, drainableLakes];
+ }
+ }
+
+ // define lakes that potentially can be open (drained into another water body)
+ function checkLakesDrainability() {
+ const canBeDrained: Dict = {}; // all false by default
+ const drainAllLakes = ELEVATION_LIMIT === MAX_HEIGHT - MIN_LAND_HEIGHT;
+
+ for (const lake of lakes) {
+ if (drainAllLakes) {
+ canBeDrained[lake.i] = true;
+ continue;
+ }
+
+ canBeDrained[lake.i] = false;
+ const minShoreHeight = getMinHeight(lake.shoreline);
+ const minHeightShoreCell = lake.shoreline.find(cellId => heights[cellId] === minShoreHeight) || lake.shoreline[0];
+
+ const queue = [minHeightShoreCell];
+ const checked = [];
+ checked[minHeightShoreCell] = true;
+ const breakableHeight = lake.height + ELEVATION_LIMIT;
+
+ loopCellsAroundLake: while (queue.length) {
+ const cellId = queue.pop()!;
+
+ for (const neibCellId of cells.c[cellId]) {
+ if (checked[neibCellId]) continue;
+ if (heights[neibCellId] >= breakableHeight) continue;
+
+ if (heights[neibCellId] < MIN_LAND_HEIGHT) {
+ const waterFeatureMet = features[cells.f[neibCellId]];
+ const isOceanMet = waterFeatureMet && waterFeatureMet.type === "ocean";
+ const isLakeMet = waterFeatureMet && waterFeatureMet.type === "lake";
+
+ if (isOceanMet || (isLakeMet && lake.height > waterFeatureMet.height)) {
+ canBeDrained[lake.i] = true;
+ break loopCellsAroundLake;
+ }
+ }
+
+ checked[neibCellId] = true;
+ queue.push(neibCellId);
+ }
+ }
+ }
+
+ return canBeDrained;
+ }
+
+ depressions && WARN && console.warn(`Unresolved depressions: ${depressions}. Edit heightmap to fix`);
+
+ return [currentCellHeights, drainableLakes];
+};
diff --git a/src/scripts/tooltips.ts b/src/scripts/tooltips.ts
index a2c42a75..9ba68077 100644
--- a/src/scripts/tooltips.ts
+++ b/src/scripts/tooltips.ts
@@ -1,4 +1,4 @@
-import {MOBILE} from "../constants";
+import {MOBILE} from "../config/constants";
import {byId} from "../utils/shorthands";
const $tooltip = byId("tooltip")!;
diff --git a/src/types/overrides.d.ts b/src/types/overrides.d.ts
index ea68331e..c1e7c312 100644
--- a/src/types/overrides.d.ts
+++ b/src/types/overrides.d.ts
@@ -14,6 +14,7 @@ interface Window {
// untyped IIFE modules
Biomes: any;
+ Rivers: any;
Names: any;
ThreeD: any;
ReliefIcons: any;
@@ -21,7 +22,6 @@ interface Window {
Lakes: any;
HeightmapGenerator: any;
OceanLayers: any;
- Rivers: any;
Cultures: any;
BurgsAndStates: any;
Religions: any;
diff --git a/src/types/pack/feature.d.ts b/src/types/pack/feature.d.ts
index 43e499dc..b1642cc9 100644
--- a/src/types/pack/feature.d.ts
+++ b/src/types/pack/feature.d.ts
@@ -30,7 +30,8 @@ interface IPackFeatureLake extends IPackFeatureBase {
flux?: number;
temp?: number;
evaporation?: number;
- outCell?: number;
+ inlets?: number[];
+ outlet?: number;
}
type TPackFeature = IPackFeatureOcean | IPackFeatureIsland | IPackFeatureLake;
diff --git a/src/types/pack/pack.d.ts b/src/types/pack/pack.d.ts
index fe2ceef2..758572f1 100644
--- a/src/types/pack/pack.d.ts
+++ b/src/types/pack/pack.d.ts
@@ -17,9 +17,9 @@ interface IPackCells {
g: UintArray;
s: IntArray;
pop: Float32Array;
- fl: UintArray;
- conf: UintArray;
- r: UintArray;
+ fl: Uint16Array; // flux volume, defined by drainWater() in river-generator.ts
+ r: Uint16Array; // river id, defined by defineRivers() in river-generator.ts
+ conf: Uint16Array; // conluence, defined by defineRivers() in river-generator.ts
biome: UintArray;
area: UintArray;
state: UintArray;
diff --git a/src/utils/arrayUtils.ts b/src/utils/arrayUtils.ts
index 586456fb..490617d5 100644
--- a/src/utils/arrayUtils.ts
+++ b/src/utils/arrayUtils.ts
@@ -1,4 +1,4 @@
-import {UINT16_MAX, UINT32_MAX, UINT8_MAX} from "../constants";
+import {UINT16_MAX, UINT32_MAX, UINT8_MAX} from "../config/constants";
export function last(array: T[]) {
return array[array.length - 1];
diff --git a/vite.config.js b/vite.config.js
index af9eb798..099d37f0 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -68,7 +68,6 @@ export default defineConfig(({mode}) => {
{find: "src", replacement: path.resolve(pathName, "./src")},
{find: "components", replacement: path.resolve(pathName, "./src/components")},
{find: "config", replacement: path.resolve(pathName, "./src/config")},
- {find: "constants", replacement: path.resolve(pathName, "./src/constants")},
{find: "dialogs", replacement: path.resolve(pathName, "./src/dialogs")},
{find: "layers", replacement: path.resolve(pathName, "./src/layers")},
{find: "libs", replacement: path.resolve(pathName, "./src/libs")},