refactor: river generation start

This commit is contained in:
max 2022-07-19 02:01:56 +03:00
parent 4833a8ab35
commit 4e65616dbc
11 changed files with 285 additions and 265 deletions

View file

@ -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)

View file

@ -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;
});

View file

@ -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
};

View file

@ -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";

View file

@ -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);
};

View file

@ -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,

View file

@ -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];
}

View file

@ -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();

View file

@ -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
View 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[]];

View file

@ -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;