mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 09:41:24 +01:00
refactor: river generation start
This commit is contained in:
parent
4833a8ab35
commit
4e65616dbc
11 changed files with 285 additions and 265 deletions
|
|
@ -13,18 +13,19 @@ export function drawCoastline(vertices: IGraphVertices, features: TPackFeatures)
|
|||
|
||||
for (const feature of features) {
|
||||
if (!feature) continue;
|
||||
if (feature.type === "ocean") continue;
|
||||
|
||||
const points = clipPoly(feature.vertices.map(vertex => vertices.p[vertex]));
|
||||
const simplifiedPoints = simplify(points, SIMPLIFICATION_TOLERANCE);
|
||||
const path = round(lineGen(simplifiedPoints)!);
|
||||
|
||||
landMask
|
||||
.append("path")
|
||||
.attr("d", path)
|
||||
.attr("fill", "black")
|
||||
.attr("id", "land_" + feature.i);
|
||||
|
||||
if (feature.type === "lake") {
|
||||
landMask
|
||||
.append("path")
|
||||
.attr("d", path)
|
||||
.attr("fill", "black")
|
||||
.attr("id", "land_" + feature.i);
|
||||
|
||||
lakes
|
||||
.select("#freshwater")
|
||||
.append("path")
|
||||
|
|
@ -32,6 +33,12 @@ export function drawCoastline(vertices: IGraphVertices, features: TPackFeatures)
|
|||
.attr("id", "lake_" + feature.i)
|
||||
.attr("data-f", feature.i);
|
||||
} else {
|
||||
landMask
|
||||
.append("path")
|
||||
.attr("d", path)
|
||||
.attr("fill", "white")
|
||||
.attr("id", "land_" + feature.i);
|
||||
|
||||
waterMask
|
||||
.append("path")
|
||||
.attr("d", path)
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ export function drawStates() {
|
|||
// define inner-state lakes to omit on border render
|
||||
const innerLakes = features.map(feature => {
|
||||
if (feature.type !== "lake") return false;
|
||||
if (!feature.shoreline) Lakes.getShoreline(feature);
|
||||
|
||||
const states = feature.shoreline.map(i => cells.state[i]);
|
||||
const shoreline = feature.shoreline || [];
|
||||
const states = shoreline.map(i => cells.state[i]);
|
||||
return new Set(states).size > 1 ? false : true;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// @ts-nocheckd
|
||||
import * as d3 from "d3";
|
||||
|
||||
import {TIME} from "config/logging";
|
||||
import {rn} from "utils/numberUtils";
|
||||
// @ts-expect-error js module
|
||||
import {aleaPRNG} from "scripts/aleaPRNG";
|
||||
import {getInputNumber, getInputValue} from "utils/nodeUtils";
|
||||
import {DISTANCE_FIELD, MIN_LAND_HEIGHT} from "config/generation";
|
||||
|
|
@ -39,69 +39,6 @@ window.Lakes = (function () {
|
|||
return lakeOutCells;
|
||||
};
|
||||
|
||||
// get array of land cells aroound lake
|
||||
const getShoreline = function (lake: IPackFeatureLake, pack: IPack) {
|
||||
const uniqueCells = new Set();
|
||||
lake.vertices.forEach(v =>
|
||||
pack.vertices.c[v].forEach(c => pack.cells.h[c] >= MIN_LAND_HEIGHT && uniqueCells.add(c))
|
||||
);
|
||||
lake.shoreline = [...uniqueCells];
|
||||
};
|
||||
|
||||
const prepareLakeData = (h: Uint8Array, pack: IPack) => {
|
||||
const cells = pack.cells;
|
||||
const ELEVATION_LIMIT = getInputNumber("lakeElevationLimitOutput");
|
||||
|
||||
pack.features.forEach(feature => {
|
||||
if (!feature || feature.type !== "lake") return;
|
||||
delete feature.flux;
|
||||
delete feature.inlets;
|
||||
delete feature.outlet;
|
||||
delete feature.height;
|
||||
delete feature.closed;
|
||||
!feature.shoreline && getShoreline(feature, pack);
|
||||
|
||||
// lake surface height is as lowest land cells around
|
||||
const min = feature.shoreline.sort((a, b) => h[a] - h[b])[0];
|
||||
feature.height = h[min] - 0.1;
|
||||
|
||||
// check if lake can be open (not in deep depression)
|
||||
if (ELEVATION_LIMIT === 80) {
|
||||
feature.closed = false;
|
||||
return;
|
||||
}
|
||||
|
||||
let deep = true;
|
||||
const threshold = feature.height + ELEVATION_LIMIT;
|
||||
const queue = [min];
|
||||
const checked = [];
|
||||
checked[min] = true;
|
||||
|
||||
// check if elevated lake can potentially pour to another water body
|
||||
while (deep && queue.length) {
|
||||
const q = queue.pop();
|
||||
|
||||
for (const n of cells.c[q]) {
|
||||
if (checked[n]) continue;
|
||||
if (h[n] >= threshold) continue;
|
||||
|
||||
if (h[n] < 20) {
|
||||
const nFeature = pack.features[cells.f[n]];
|
||||
if ((nFeature && nFeature.type === "ocean") || feature.height > nFeature.height) {
|
||||
deep = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
checked[n] = true;
|
||||
queue.push(n);
|
||||
}
|
||||
}
|
||||
|
||||
feature.closed = deep;
|
||||
});
|
||||
};
|
||||
|
||||
const cleanupLakeData = function (pack: IPack) {
|
||||
for (const feature of pack.features) {
|
||||
if (feature.type !== "lake") continue;
|
||||
|
|
@ -277,11 +214,9 @@ window.Lakes = (function () {
|
|||
return {
|
||||
setClimateData,
|
||||
cleanupLakeData,
|
||||
prepareLakeData,
|
||||
defineGroup,
|
||||
generateName,
|
||||
getName,
|
||||
getShoreline,
|
||||
addLakesInDeepDepressions,
|
||||
openNearSeaLakes
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
import {MIN_LAND_HEIGHT, DISTANCE_FIELD} from "config/generation";
|
||||
import * as d3 from "d3";
|
||||
|
||||
import {DISTANCE_FIELD, MIN_LAND_HEIGHT} from "config/generation";
|
||||
import {TIME} from "config/logging";
|
||||
import {INT8_MAX} from "constants";
|
||||
// @ts-expect-error js module
|
||||
import {aleaPRNG} from "scripts/aleaPRNG";
|
||||
import {createTypedArray} from "utils/arrayUtils";
|
||||
import {dist2} from "utils/functionUtils";
|
||||
import {getFeatureVertices} from "scripts/connectVertices";
|
||||
import {createTypedArray, unique} from "utils/arrayUtils";
|
||||
import {dist2} from "utils/functionUtils";
|
||||
import {clipPoly} from "utils/lineUtils";
|
||||
import {rn} from "utils/numberUtils";
|
||||
|
||||
const {UNMARKED, LAND_COAST, WATER_COAST, LANDLOCKED, DEEPER_WATER} = DISTANCE_FIELD;
|
||||
|
||||
|
|
@ -132,23 +135,22 @@ export function markupPackFeatures(
|
|||
}
|
||||
|
||||
const featureVertices = getFeatureVertices({firstCell, vertices, cells, featureIds, featureId});
|
||||
|
||||
// let points = clipPoly(vchain.map(v => vertices.p[v]));
|
||||
// const area = d3.polygonArea(points); // area with lakes/islands
|
||||
// if (area > 0 && features[f].type === "lake") {
|
||||
// points = points.reverse();
|
||||
// vchain = vchain.reverse();
|
||||
// }
|
||||
const points = clipPoly(featureVertices.map(vertex => vertices.p[vertex]));
|
||||
const area = d3.polygonArea(points); // feature perimiter area
|
||||
|
||||
const feature = addFeature({
|
||||
vertices,
|
||||
heights: cells.h,
|
||||
features,
|
||||
featureIds,
|
||||
firstCell,
|
||||
land,
|
||||
border,
|
||||
featureVertices,
|
||||
featureId,
|
||||
cellNumber,
|
||||
gridCellsNumber
|
||||
gridCellsNumber,
|
||||
area
|
||||
});
|
||||
features.push(feature);
|
||||
|
||||
|
|
@ -164,16 +166,23 @@ export function markupPackFeatures(
|
|||
}
|
||||
|
||||
function addFeature({
|
||||
vertices,
|
||||
heights,
|
||||
features,
|
||||
featureIds,
|
||||
firstCell,
|
||||
land,
|
||||
border,
|
||||
featureVertices,
|
||||
featureId,
|
||||
cellNumber,
|
||||
gridCellsNumber
|
||||
gridCellsNumber,
|
||||
area
|
||||
}: {
|
||||
vertices: IGraphVertices;
|
||||
heights: Uint8Array;
|
||||
features: TPackFeatures;
|
||||
featureIds: Uint16Array;
|
||||
firstCell: number;
|
||||
land: boolean;
|
||||
border: boolean;
|
||||
|
|
@ -181,12 +190,15 @@ function addFeature({
|
|||
featureId: number;
|
||||
cellNumber: number;
|
||||
gridCellsNumber: number;
|
||||
area: number;
|
||||
}) {
|
||||
const OCEAN_MIN_SIZE = gridCellsNumber / 25;
|
||||
const SEA_MIN_SIZE = gridCellsNumber / 1000;
|
||||
const CONTINENT_MIN_SIZE = gridCellsNumber / 10;
|
||||
const ISLAND_MIN_SIZE = gridCellsNumber / 1000;
|
||||
|
||||
const absArea = Math.abs(rn(area));
|
||||
|
||||
if (land) return addIsland();
|
||||
if (border) return addOcean();
|
||||
return addLake();
|
||||
|
|
@ -201,7 +213,8 @@ function addFeature({
|
|||
border,
|
||||
cells: cellNumber,
|
||||
firstCell,
|
||||
vertices: featureVertices
|
||||
vertices: featureVertices,
|
||||
area: absArea
|
||||
};
|
||||
return feature;
|
||||
}
|
||||
|
|
@ -216,7 +229,8 @@ function addFeature({
|
|||
border: false,
|
||||
cells: cellNumber,
|
||||
firstCell,
|
||||
vertices: featureVertices
|
||||
vertices: featureVertices,
|
||||
area: absArea
|
||||
};
|
||||
return feature;
|
||||
}
|
||||
|
|
@ -224,6 +238,25 @@ function addFeature({
|
|||
function addLake() {
|
||||
const group = "freshwater"; // temp, to be defined later
|
||||
const name = ""; // temp, to be defined later
|
||||
|
||||
// ensure lake ring is clockwise (to form a hole)
|
||||
const lakeVertices = area > 0 ? featureVertices.reverse() : featureVertices;
|
||||
|
||||
const shoreline = getShoreline(); // land cells around lake
|
||||
const height = getLakeElevation();
|
||||
|
||||
function getShoreline() {
|
||||
const isLand = (cellId: number) => heights[cellId] >= MIN_LAND_HEIGHT;
|
||||
const cellsAround = lakeVertices.map(vertex => vertices.c[vertex].filter(isLand)).flat();
|
||||
return unique(cellsAround);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const feature: IPackFeatureLake = {
|
||||
i: featureId,
|
||||
type: "lake",
|
||||
|
|
@ -233,7 +266,10 @@ function addFeature({
|
|||
border: false,
|
||||
cells: cellNumber,
|
||||
firstCell,
|
||||
vertices: featureVertices
|
||||
vertices: lakeVertices,
|
||||
shoreline: shoreline,
|
||||
height,
|
||||
area: absArea
|
||||
};
|
||||
return feature;
|
||||
}
|
||||
|
|
@ -245,7 +281,7 @@ function addFeature({
|
|||
}
|
||||
|
||||
function defineIslandGroup() {
|
||||
const prevFeature = features.at(-1);
|
||||
const prevFeature = features[featureIds[firstCell - 1]];
|
||||
|
||||
if (prevFeature && prevFeature.type === "lake") return "lake_island";
|
||||
if (cellNumber > CONTINENT_MIN_SIZE) return "continent";
|
||||
|
|
|
|||
|
|
@ -6,28 +6,36 @@ import {rn} from "utils/numberUtils";
|
|||
import {round} from "utils/stringUtils";
|
||||
import {rw, each} from "utils/probabilityUtils";
|
||||
import {aleaPRNG} from "scripts/aleaPRNG";
|
||||
import {DISTANCE_FIELD, MIN_LAND_HEIGHT} from "config/generation";
|
||||
import {getInputNumber} from "utils/nodeUtils";
|
||||
|
||||
const {Lakes} = window;
|
||||
const {LAND_COAST} = DISTANCE_FIELD;
|
||||
|
||||
interface IRiverPackData {
|
||||
cells: Pick<IPack["cells"], "i" | "h" | "c" | "t">;
|
||||
features: TPackFeatures;
|
||||
}
|
||||
|
||||
window.Rivers = (function () {
|
||||
const generate = function (pack, grid, allowErosion = true) {
|
||||
const generate = function (grid: IGrid, {cells, features}: IRiverPackData, allowErosion = true) {
|
||||
TIME && console.time("generateRivers");
|
||||
|
||||
Math.random = aleaPRNG(seed);
|
||||
const {cells, features} = pack;
|
||||
|
||||
const riversData = {}; // rivers data
|
||||
const riverParents = {};
|
||||
const addCellToRiver = function (cell, river) {
|
||||
if (!riversData[river]) riversData[river] = [cell];
|
||||
else riversData[river].push(cell);
|
||||
};
|
||||
|
||||
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
|
||||
const cellsNumber = cells.i.length;
|
||||
const flux = new Uint16Array(cellsNumber);
|
||||
const riverIds = new Uint16Array(cellsNumber);
|
||||
const confluence = new Uint8Array(cellsNumber);
|
||||
|
||||
const h = alterHeights(pack.cells);
|
||||
Lakes.prepareLakeData(h, pack);
|
||||
resolveDepressions(pack, h);
|
||||
let nextRiverId = 1; // starts with 1
|
||||
|
||||
const alteredHeights = alterHeights({h: cells.h, c: cells.c, t: cells.t});
|
||||
|
||||
resolveDepressions(pack, alteredHeights);
|
||||
drainWater();
|
||||
defineRivers();
|
||||
|
||||
|
|
@ -35,7 +43,7 @@ window.Rivers = (function () {
|
|||
Lakes.cleanupLakeData(pack);
|
||||
|
||||
if (allowErosion) {
|
||||
cells.h = Uint8Array.from(h); // apply gradient
|
||||
cells.h = Uint8Array.from(alteredHeights); // apply gradient
|
||||
downcutRivers(); // downcut river beds
|
||||
}
|
||||
|
||||
|
|
@ -46,36 +54,36 @@ window.Rivers = (function () {
|
|||
const cellsNumberModifier = (pointsInput.dataset.cells / 10000) ** 0.25;
|
||||
|
||||
const prec = grid.cells.prec;
|
||||
const land = cells.i.filter(i => h[i] >= 20).sort((a, b) => h[b] - h[a]);
|
||||
const lakeOutCells = Lakes.setClimateData(h, pack, grid);
|
||||
const land = cells.i.filter(i => alteredHeights[i] >= 20).sort((a, b) => alteredHeights[b] - alteredHeights[a]);
|
||||
const lakeOutCells = Lakes.setClimateData(alteredHeights, pack, grid);
|
||||
|
||||
land.forEach(function (i) {
|
||||
cells.fl[i] += prec[cells.g[i]] / cellsNumberModifier; // add flux from precipitation
|
||||
flux[i] += prec[cells.g[i]] / cellsNumberModifier; // add flux from precipitation
|
||||
|
||||
// create lake outlet if lake is not in deep depression and 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);
|
||||
cells.fl[lakeCell] += Math.max(lake.flux - lake.evaporation, 0); // not evaporated lake water drains to outlet
|
||||
const lakeCell = cells.c[i].find(c => alteredHeights[c] < 20 && 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 (cells.r[lakeCell] !== lake.river) {
|
||||
const sameRiver = cells.c[lakeCell].some(c => cells.r[c] === lake.river);
|
||||
if (riverIds[lakeCell] !== lake.river) {
|
||||
const sameRiver = cells.c[lakeCell].some(c => riverIds[c] === lake.river);
|
||||
|
||||
if (sameRiver) {
|
||||
cells.r[lakeCell] = lake.river;
|
||||
riverIds[lakeCell] = lake.river;
|
||||
addCellToRiver(lakeCell, lake.river);
|
||||
} else {
|
||||
cells.r[lakeCell] = riverNext;
|
||||
addCellToRiver(lakeCell, riverNext);
|
||||
riverNext++;
|
||||
riverIds[lakeCell] = nextRiverId;
|
||||
addCellToRiver(lakeCell, nextRiverId);
|
||||
nextRiverId++;
|
||||
}
|
||||
}
|
||||
|
||||
lake.outlet = cells.r[lakeCell];
|
||||
flowDown(i, cells.fl[lakeCell], lake.outlet);
|
||||
lake.outlet = riverIds[lakeCell];
|
||||
flowDown(i, flux[lakeCell], lake.outlet);
|
||||
}
|
||||
|
||||
// assign all tributary rivers to outlet basin
|
||||
|
|
@ -88,21 +96,21 @@ window.Rivers = (function () {
|
|||
}
|
||||
|
||||
// near-border cell: pour water out of the screen
|
||||
if (cells.b[i] && cells.r[i]) return addCellToRiver(-1, cells.r[i]);
|
||||
if (cells.b[i] && riverIds[i]) return addCellToRiver(-1, riverIds[i]);
|
||||
|
||||
// downhill cell (make sure it's not in the source lake)
|
||||
let min = null;
|
||||
if (lakeOutCells[i]) {
|
||||
const filtered = cells.c[i].filter(c => !lakes.map(lake => lake.i).includes(cells.f[c]));
|
||||
min = filtered.sort((a, b) => h[a] - h[b])[0];
|
||||
min = filtered.sort((a, b) => alteredHeights[a] - alteredHeights[b])[0];
|
||||
} else if (cells.haven[i]) {
|
||||
min = cells.haven[i];
|
||||
} else {
|
||||
min = cells.c[i].sort((a, b) => h[a] - h[b])[0];
|
||||
min = cells.c[i].sort((a, b) => alteredHeights[a] - alteredHeights[b])[0];
|
||||
}
|
||||
|
||||
// cells is depressed
|
||||
if (h[i] <= h[min]) return;
|
||||
if (alteredHeights[i] <= alteredHeights[min]) return;
|
||||
|
||||
// debug
|
||||
// .append("line")
|
||||
|
|
@ -113,40 +121,45 @@ window.Rivers = (function () {
|
|||
// .attr("stroke", "#333")
|
||||
// .attr("stroke-width", 0.2);
|
||||
|
||||
if (cells.fl[i] < MIN_FLUX_TO_FORM_RIVER) {
|
||||
if (flux[i] < MIN_FLUX_TO_FORM_RIVER) {
|
||||
// flux is too small to operate as a river
|
||||
if (h[min] >= 20) cells.fl[min] += cells.fl[i];
|
||||
if (alteredHeights[min] >= 20) flux[min] += flux[i];
|
||||
return;
|
||||
}
|
||||
|
||||
// proclaim a new river
|
||||
if (!cells.r[i]) {
|
||||
cells.r[i] = riverNext;
|
||||
addCellToRiver(i, riverNext);
|
||||
riverNext++;
|
||||
if (!riverIds[i]) {
|
||||
riverIds[i] = nextRiverId;
|
||||
addCellToRiver(i, nextRiverId);
|
||||
nextRiverId++;
|
||||
}
|
||||
|
||||
flowDown(min, cells.fl[i], cells.r[i]);
|
||||
flowDown(min, flux[i], riverIds[i]);
|
||||
});
|
||||
}
|
||||
|
||||
function addCellToRiver(cellId: number, riverId: number) {
|
||||
if (!riversData[riverId]) riversData[riverId] = [cellId];
|
||||
else riversData[riverId].push(cellId);
|
||||
}
|
||||
|
||||
function flowDown(toCell, fromFlux, river) {
|
||||
const toFlux = cells.fl[toCell] - cells.conf[toCell];
|
||||
const toRiver = cells.r[toCell];
|
||||
const toFlux = flux[toCell] - confluence[toCell];
|
||||
const toRiver = riverIds[toCell];
|
||||
|
||||
if (toRiver) {
|
||||
// downhill cell already has river assigned
|
||||
if (fromFlux > toFlux) {
|
||||
cells.conf[toCell] += cells.fl[toCell]; // mark confluence
|
||||
if (h[toCell] >= 20) riverParents[toRiver] = river; // min river is a tributary of current river
|
||||
cells.r[toCell] = river; // re-assign river if downhill part has less flux
|
||||
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 {
|
||||
cells.conf[toCell] += fromFlux; // mark confluence
|
||||
if (h[toCell] >= 20) riverParents[river] = toRiver; // current river is a tributary of min river
|
||||
confluence[toCell] += fromFlux; // mark confluence
|
||||
if (alteredHeights[toCell] >= 20) riverParents[river] = toRiver; // current river is a tributary of min river
|
||||
}
|
||||
} else cells.r[toCell] = river; // assign the river to the downhill cell
|
||||
} else riverIds[toCell] = river; // assign the river to the downhill cell
|
||||
|
||||
if (h[toCell] < 20) {
|
||||
if (alteredHeights[toCell] < 20) {
|
||||
// pour water to the water body
|
||||
const waterBody = features[cells.f[toCell]];
|
||||
if (waterBody.type === "lake") {
|
||||
|
|
@ -160,7 +173,7 @@ window.Rivers = (function () {
|
|||
}
|
||||
} else {
|
||||
// propagate flux and add next river segment
|
||||
cells.fl[toCell] += fromFlux;
|
||||
flux[toCell] += fromFlux;
|
||||
}
|
||||
|
||||
addCellToRiver(toCell, river);
|
||||
|
|
@ -168,8 +181,8 @@ window.Rivers = (function () {
|
|||
|
||||
function defineRivers() {
|
||||
// re-initialize rivers and confluence arrays
|
||||
cells.r = new Uint16Array(cells.i.length);
|
||||
cells.conf = new Uint16Array(cells.i.length);
|
||||
riverIds = new Uint16Array(cellsNumber);
|
||||
confluence = new Uint16Array(cellsNumber);
|
||||
pack.rivers = [];
|
||||
|
||||
const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2);
|
||||
|
|
@ -184,8 +197,8 @@ window.Rivers = (function () {
|
|||
if (cell < 0 || cells.h[cell] < 20) continue;
|
||||
|
||||
// mark real confluences and assign river to cells
|
||||
if (cells.r[cell]) cells.conf[cell] = 1;
|
||||
else cells.r[cell] = riverId;
|
||||
if (riverIds[cell]) confluence[cell] = 1;
|
||||
else riverIds[cell] = riverId;
|
||||
}
|
||||
|
||||
const source = riverCells[0];
|
||||
|
|
@ -194,7 +207,7 @@ window.Rivers = (function () {
|
|||
|
||||
const widthFactor = !parent || parent === riverId ? mainStemWidthFactor : defaultWidthFactor;
|
||||
const meanderedPoints = addMeandering(pack, riverCells);
|
||||
const discharge = cells.fl[mouth]; // m3 in second
|
||||
const discharge = flux[mouth]; // m3 in second
|
||||
const length = getApproximateLength(meanderedPoints);
|
||||
const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, 0));
|
||||
|
||||
|
|
@ -218,48 +231,94 @@ window.Rivers = (function () {
|
|||
|
||||
for (const i of pack.cells.i) {
|
||||
if (cells.h[i] < 35) continue; // don't donwcut lowlands
|
||||
if (!cells.fl[i]) continue;
|
||||
if (!flux[i]) continue;
|
||||
|
||||
const higherCells = cells.c[i].filter(c => cells.h[c] > cells.h[i]);
|
||||
const higherFlux = higherCells.reduce((acc, c) => acc + cells.fl[c], 0) / higherCells.length;
|
||||
const higherFlux = higherCells.reduce((acc, c) => acc + flux[c], 0) / higherCells.length;
|
||||
if (!higherFlux) continue;
|
||||
|
||||
const downcut = Math.floor(cells.fl[i] / higherFlux);
|
||||
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 (!cells.conf[i]) continue;
|
||||
if (!confluence[i]) continue;
|
||||
|
||||
const sortedInflux = cells.c[i]
|
||||
.filter(c => cells.r[c] && h[c] > h[i])
|
||||
.map(c => cells.fl[c])
|
||||
.filter(c => riverIds[c] && alteredHeights[c] > alteredHeights[i])
|
||||
.map(c => flux[c])
|
||||
.sort((a, b) => b - a);
|
||||
cells.conf[i] = sortedInflux.reduce((acc, flux, index) => (index ? acc + flux : acc), 0);
|
||||
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}) => {
|
||||
return Array.from(h).map((h, i) => {
|
||||
if (h < 20 || t[i] < 1) return h;
|
||||
return h + t[i] / 100 + d3.mean(c[i].map(c => t[c])) / 10000;
|
||||
const alterHeights = ({h, c, t}: Pick<IPack["cells"], "h" | "c" | "t">) => {
|
||||
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 (pack, h) {
|
||||
const {cells, features} = pack;
|
||||
const maxIterations = +document.getElementById("resolveDepressionsStepsOutput").value;
|
||||
const maxIterations = getInputNumber("resolveDepressionsStepsOutput");
|
||||
const checkLakeMaxIteration = maxIterations * 0.85;
|
||||
const elevateLakeMaxIteration = maxIterations * 0.75;
|
||||
|
||||
const height = i => features[cells.f[i]].height || h[i]; // height of lake or specific cell
|
||||
|
||||
const lakes = features.filter(f => f.type === "lake");
|
||||
const lakes = features.filter(feature => feature.type === "lake");
|
||||
const canBePoured = () => {
|
||||
const ELEVATION_LIMIT = getInputNumber("lakeElevationLimitOutput");
|
||||
|
||||
const lakes = features.filter(feature => feature && feature.type === "lake") as IPackFeatureLake[];
|
||||
const lakeData = lakes.map(feature => {
|
||||
const minShoreHeight = d3.min(feature.shoreline.map(cellId => heights[cellId])) || MIN_LAND_HEIGHT;
|
||||
const minHeightCell =
|
||||
feature.shoreline.find(cellId => heights[cellId] === minShoreHeight) || feature.shoreline[0];
|
||||
|
||||
if (ELEVATION_LIMIT === 80) return {...feature, closed: false};
|
||||
|
||||
// check if lake can be open (not in deep depression)
|
||||
let deep = true;
|
||||
|
||||
const threshold = feature.height + ELEVATION_LIMIT;
|
||||
const queue = [minHeightCell];
|
||||
const checked = [];
|
||||
checked[minHeightCell] = true;
|
||||
|
||||
// check if elevated lake can potentially pour to another water body
|
||||
while (deep && queue.length) {
|
||||
const cellId = queue.pop()!;
|
||||
|
||||
for (const neibCellId of cells.c[cellId]) {
|
||||
if (checked[neibCellId]) continue;
|
||||
if (heights[neibCellId] >= threshold) continue;
|
||||
|
||||
if (heights[neibCellId] < MIN_LAND_HEIGHT) {
|
||||
const waterFeatureMet = features[cells.f[neibCellId]];
|
||||
|
||||
if ((waterFeatureMet && waterFeatureMet.type === "ocean") || feature.height > waterFeatureMet.height) {
|
||||
deep = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
checked[neibCellId] = true;
|
||||
queue.push(neibCellId);
|
||||
}
|
||||
}
|
||||
|
||||
return {...feature, closed: deep};
|
||||
});
|
||||
};
|
||||
|
||||
const land = cells.i.filter(i => h[i] >= 20 && !cells.b[i]); // exclude near-border cells
|
||||
land.sort((a, b) => h[a] - h[b]); // lowest cells go first
|
||||
|
||||
|
|
@ -476,10 +535,10 @@ window.Rivers = (function () {
|
|||
|
||||
// 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
|
||||
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) {
|
||||
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());
|
||||
|
|
@ -492,9 +551,9 @@ window.Rivers = (function () {
|
|||
pack.rivers = pack.rivers.filter(r => !riversToRemove.includes(r.i));
|
||||
};
|
||||
|
||||
const getBasin = function (r) {
|
||||
const parent = pack.rivers.find(river => river.i === r)?.parent;
|
||||
if (!parent || r === parent) return r;
|
||||
const getBasin = function (riverId: number) {
|
||||
const parent = pack.rivers.find(river => river.i === riverId)?.parent;
|
||||
if (!parent || riverId === parent) return riverId;
|
||||
return getBasin(parent);
|
||||
};
|
||||
|
||||
|
|
@ -1,20 +1,16 @@
|
|||
/*////////////////////////////////////////////////////////////////
|
||||
aleaPRNG 1.1
|
||||
//////////////////////////////////////////////////////////////////
|
||||
https://github.com/macmcmeans/aleaPRNG/blob/master/aleaPRNG-1.1.js
|
||||
//////////////////////////////////////////////////////////////////
|
||||
Original work copyright © 2010 Johannes Baagøe, under MIT license
|
||||
This is a derivative work copyright (c) 2017-2020, W. Mac" McMeans, under BSD license.
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
////////////////////////////////////////////////////////////////*/
|
||||
export function aleaPRNG() {
|
||||
return (function (args) {
|
||||
"use strict";
|
||||
// @ts-nocheck
|
||||
|
||||
// aleaPRNG 1.1: https://github.com/macmcmeans/aleaPRNG/blob/master/aleaPRNG-1.1.js
|
||||
// Original work copyright © 2010 Johannes Baagøe, under MIT license
|
||||
// This is a derivative work copyright (c) 2017-2020, W. Mac" McMeans, under BSD license.
|
||||
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
export function aleaPRNG(args) {
|
||||
return (function (args) {
|
||||
const version = "aleaPRNG 1.1.0";
|
||||
|
||||
var s0,
|
||||
|
|
@ -1,7 +1,4 @@
|
|||
import * as d3 from "d3";
|
||||
|
||||
import {ERROR} from "config/logging";
|
||||
import {clipPoly} from "utils/lineUtils";
|
||||
|
||||
export function getFeatureVertices({
|
||||
firstCell,
|
||||
|
|
@ -22,26 +19,6 @@ export function getFeatureVertices({
|
|||
const startingVertex = findStartingVertex({startingCell, featureIds, featureId, vertices, cells, packCellsNumber});
|
||||
const featureVertices = connectVertices({vertices, startingVertex, featureIds, featureId});
|
||||
|
||||
// temp: draw feature vertices
|
||||
cells.v[firstCell]
|
||||
.map(v => vertices.p[v])
|
||||
.forEach(([x, y]) => {
|
||||
d3.select("#debug").append("circle").attr("cx", x).attr("cy", y).attr("r", 0.2).attr("fill", "yellow");
|
||||
});
|
||||
|
||||
const [cx, cy] = vertices.p[startingVertex];
|
||||
d3.select("#debug").append("circle").attr("cx", cx).attr("cy", cy).attr("r", 1.5).attr("fill", "red");
|
||||
|
||||
const lineGen = d3.line();
|
||||
const points = clipPoly(featureVertices.map(v => vertices.p[v]));
|
||||
const path = lineGen(points)!;
|
||||
d3.select("#debug")
|
||||
.attr("fill", "none")
|
||||
.attr("stroke", "black")
|
||||
.attr("stroke-width", 0.1)
|
||||
.append("path")
|
||||
.attr("d", path);
|
||||
|
||||
return featureVertices;
|
||||
}
|
||||
|
||||
|
|
@ -106,7 +83,7 @@ function findStartingVertex({
|
|||
throw new Error(`Markup: firstCell ${startingCell} of feature ${featureId} has no neighbors of other features`);
|
||||
}
|
||||
|
||||
const index = neibCells.indexOf(d3.min(otherFeatureNeibs)!);
|
||||
const index = neibCells.indexOf(Math.min(...otherFeatureNeibs)!);
|
||||
return cellVertices[index];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import * as d3 from "d3";
|
|||
import {ERROR, INFO, WARN} from "config/logging";
|
||||
import {closeDialogs} from "dialogs/utils";
|
||||
import {openDialog} from "dialogs";
|
||||
import {initLayers, restoreLayers} from "layers";
|
||||
import {initLayers, renderLayer, restoreLayers} from "layers";
|
||||
// @ts-expect-error js module
|
||||
import {drawCoastline} from "layers/renderers/drawCoastline";
|
||||
// @ts-expect-error js module
|
||||
|
|
@ -16,7 +16,6 @@ import {applyMapSize, randomizeOptions} from "modules/ui/options";
|
|||
import {applyStyleOnLoad} from "modules/ui/stylePresets";
|
||||
// @ts-expect-error js module
|
||||
import {addZones} from "modules/zones";
|
||||
// @ts-expect-error js module
|
||||
import {aleaPRNG} from "scripts/aleaPRNG";
|
||||
import {hideLoading, showLoading} from "scripts/loading";
|
||||
import {clearMainTip, tip} from "scripts/tooltips";
|
||||
|
|
@ -63,6 +62,11 @@ async function generate(options?: IGenerationOptions) {
|
|||
grid = newGrid;
|
||||
pack = newPack;
|
||||
|
||||
// temp rendering for debug
|
||||
renderLayer("coastline", pack.vertices, pack.features);
|
||||
renderLayer("heightmap");
|
||||
renderLayer("rivers", pack);
|
||||
|
||||
WARN && console.warn(`TOTAL: ${rn((performance.now() - timeStart) / 1000, 2)}s`);
|
||||
showStatistics();
|
||||
INFO && console.groupEnd();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import * as d3 from "d3";
|
||||
|
||||
import {renderLayer} from "layers";
|
||||
// @ts-expect-error js module
|
||||
import {drawCoastline} from "layers/renderers/drawCoastline";
|
||||
import {markupPackFeatures} from "modules/markup";
|
||||
|
|
@ -23,49 +22,54 @@ export function createPack(grid: IGrid): IPack {
|
|||
const {vertices, cells} = repackGrid(grid);
|
||||
|
||||
const markup = markupPackFeatures(grid, vertices, pick(cells, "v", "c", "b", "p", "h"));
|
||||
const {features, featureIds, distanceField, haven, harbor} = markup;
|
||||
|
||||
renderLayer("coastline", vertices, markup.features);
|
||||
const riverCells = {...cells, f: featureIds, t: distanceField, haven};
|
||||
Rivers.generate(grid, {cells: riverCells, features}, true);
|
||||
|
||||
// drawCoastline({vertices, cells}); // split into vertices definition and rendering
|
||||
|
||||
// Rivers.generate(newPack, grid);
|
||||
// renderLayer("rivers", newPack);
|
||||
// Lakes.defineGroup(newPack);
|
||||
// Biomes.define(newPack, grid);
|
||||
|
||||
// const rankCellsData = pick(newPack.cells, "i", "f", "fl", "conf", "r", "h", "area", "biome", "haven", "harbor");
|
||||
// rankCells(newPack.features!, rankCellsData);
|
||||
|
||||
Cultures.generate();
|
||||
Cultures.expand();
|
||||
BurgsAndStates.generate();
|
||||
Religions.generate();
|
||||
BurgsAndStates.defineStateForms();
|
||||
BurgsAndStates.generateProvinces();
|
||||
BurgsAndStates.defineBurgFeatures();
|
||||
// Cultures.generate();
|
||||
// Cultures.expand();
|
||||
// BurgsAndStates.generate();
|
||||
// Religions.generate();
|
||||
// BurgsAndStates.defineStateForms();
|
||||
// BurgsAndStates.generateProvinces();
|
||||
// BurgsAndStates.defineBurgFeatures();
|
||||
|
||||
renderLayer("states");
|
||||
renderLayer("borders");
|
||||
BurgsAndStates.drawStateLabels();
|
||||
// renderLayer("states");
|
||||
// renderLayer("borders");
|
||||
// BurgsAndStates.drawStateLabels();
|
||||
|
||||
Rivers.specify();
|
||||
Lakes.generateName();
|
||||
// Rivers.specify();
|
||||
// Lakes.generateName();
|
||||
|
||||
Military.generate();
|
||||
Markers.generate();
|
||||
addZones();
|
||||
// Military.generate();
|
||||
// Markers.generate();
|
||||
// addZones();
|
||||
|
||||
OceanLayers(newGrid);
|
||||
// OceanLayers(newGrid);
|
||||
|
||||
drawScaleBar(window.scale);
|
||||
Names.getMapName();
|
||||
// drawScaleBar(window.scale);
|
||||
// Names.getMapName();
|
||||
|
||||
const pack = {
|
||||
const pack: IPack = {
|
||||
vertices,
|
||||
cells
|
||||
cells: {
|
||||
...cells,
|
||||
f: featureIds,
|
||||
t: distanceField,
|
||||
haven,
|
||||
harbor
|
||||
},
|
||||
features
|
||||
};
|
||||
|
||||
return pack as IPack;
|
||||
return pack;
|
||||
}
|
||||
|
||||
// repack grid cells: discart deep water cells, add land cells along the coast
|
||||
|
|
@ -133,6 +137,3 @@ function repackGrid(grid: IGrid) {
|
|||
TIME && console.timeEnd("repackGrid");
|
||||
return pack;
|
||||
}
|
||||
function drawLayer(arg0: string, vertices: IGraphVertices, features: TPackFeatures) {
|
||||
throw new Error("Function not implemented.");
|
||||
}
|
||||
|
|
|
|||
36
src/types/pack/feature.d.ts
vendored
Normal file
36
src/types/pack/feature.d.ts
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
interface IPackFeatureBase {
|
||||
i: number; // feature id starting from 1
|
||||
border: boolean; // if touches map border
|
||||
cells: number; // number of cells
|
||||
firstCell: number; // index of the top left cell
|
||||
vertices: number[]; // indexes of perimetric vertices
|
||||
area: number; // area of the feature perimetric polygon
|
||||
}
|
||||
3;
|
||||
|
||||
interface IPackFeatureOcean extends IPackFeatureBase {
|
||||
land: false;
|
||||
type: "ocean";
|
||||
group: "ocean" | "sea" | "gulf";
|
||||
}
|
||||
|
||||
interface IPackFeatureIsland extends IPackFeatureBase {
|
||||
land: true;
|
||||
type: "island";
|
||||
group: "continent" | "island" | "isle" | "lake_island";
|
||||
}
|
||||
|
||||
interface IPackFeatureLake extends IPackFeatureBase {
|
||||
land: false;
|
||||
type: "lake";
|
||||
group: "freshwater" | "salt" | "frozen" | "dry" | "sinkhole" | "lava";
|
||||
name: string;
|
||||
shoreline: number[];
|
||||
height: number;
|
||||
}
|
||||
|
||||
type TPackFeature = IPackFeatureOcean | IPackFeatureIsland | IPackFeatureLake;
|
||||
|
||||
type FirstElement = 0;
|
||||
|
||||
type TPackFeatures = [FirstElement, ...TPackFeature[]];
|
||||
31
src/types/pack.d.ts → src/types/pack/pack.d.ts
vendored
31
src/types/pack.d.ts → src/types/pack/pack.d.ts
vendored
|
|
@ -37,37 +37,6 @@ interface IPackBase extends IGraph {
|
|||
features?: TPackFeatures;
|
||||
}
|
||||
|
||||
interface IPackFeatureBase {
|
||||
i: number; // feature id starting from 1
|
||||
border: boolean; // if touches map border
|
||||
cells: number; // number of cells
|
||||
firstCell: number; // index of the top left cell
|
||||
vertices: number[]; // indexes of perimetric vertices
|
||||
}
|
||||
|
||||
interface IPackFeatureOcean extends IPackFeatureBase {
|
||||
land: false;
|
||||
type: "ocean";
|
||||
group: "ocean" | "sea" | "gulf";
|
||||
}
|
||||
|
||||
interface IPackFeatureIsland extends IPackFeatureBase {
|
||||
land: true;
|
||||
type: "island";
|
||||
group: "continent" | "island" | "isle" | "lake_island";
|
||||
}
|
||||
|
||||
interface IPackFeatureLake extends IPackFeatureBase {
|
||||
land: false;
|
||||
type: "lake";
|
||||
group: "freshwater" | "salt" | "frozen" | "dry" | "sinkhole" | "lava";
|
||||
name: string;
|
||||
}
|
||||
|
||||
type TPackFeature = IPackFeatureOcean | IPackFeatureIsland | IPackFeatureLake;
|
||||
|
||||
type TPackFeatures = [0, ...TPackFeature[]];
|
||||
|
||||
interface IState {
|
||||
i: number;
|
||||
name: string;
|
||||
Loading…
Add table
Add a link
Reference in a new issue