diff --git a/package-lock.json b/package-lock.json
index 67512031..55b1e80f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1999,7 +1999,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
diff --git a/public/modules/features.js b/public/modules/features.js
deleted file mode 100644
index 714d4f38..00000000
--- a/public/modules/features.js
+++ /dev/null
@@ -1,267 +0,0 @@
-"use strict";
-
-window.Features = (function () {
- const DEEPER_LAND = 3;
- const LANDLOCKED = 2;
- const LAND_COAST = 1;
- const UNMARKED = 0;
- const WATER_COAST = -1;
- const DEEP_WATER = -2;
-
- // calculate distance to coast for every cell
- function markup({distanceField, neighbors, start, increment, limit = INT8_MAX}) {
- for (let distance = start, marked = Infinity; marked > 0 && distance !== limit; distance += increment) {
- marked = 0;
- const prevDistance = distance - increment;
- for (let cellId = 0; cellId < neighbors.length; cellId++) {
- if (distanceField[cellId] !== prevDistance) continue;
-
- for (const neighborId of neighbors[cellId]) {
- if (distanceField[neighborId] !== UNMARKED) continue;
- distanceField[neighborId] = distance;
- marked++;
- }
- }
- }
- }
-
- // mark Grid features (ocean, lakes, islands) and calculate distance field
- function markupGrid() {
- TIME && console.time("markupGrid");
- Math.random = aleaPRNG(seed); // get the same result on heightmap edit in Erase mode
-
- const {h: heights, c: neighbors, b: borderCells, i} = grid.cells;
- const cellsNumber = i.length;
- const distanceField = new Int8Array(cellsNumber); // gird.cells.t
- const featureIds = new Uint16Array(cellsNumber); // gird.cells.f
- const features = [0];
-
- const queue = [0];
- for (let featureId = 1; queue[0] !== -1; featureId++) {
- const firstCell = queue[0];
- featureIds[firstCell] = featureId;
-
- const land = heights[firstCell] >= 20;
- let border = false; // set true if feature touches map edge
-
- while (queue.length) {
- const cellId = queue.pop();
- if (!border && borderCells[cellId]) border = true;
-
- for (const neighborId of neighbors[cellId]) {
- const isNeibLand = heights[neighborId] >= 20;
-
- if (land === isNeibLand && featureIds[neighborId] === UNMARKED) {
- featureIds[neighborId] = featureId;
- queue.push(neighborId);
- } else if (land && !isNeibLand) {
- distanceField[cellId] = LAND_COAST;
- distanceField[neighborId] = WATER_COAST;
- }
- }
- }
-
- const type = land ? "island" : border ? "ocean" : "lake";
- features.push({i: featureId, land, border, type});
-
- queue[0] = featureIds.findIndex(f => f === UNMARKED); // find unmarked cell
- }
-
- // markup deep ocean cells
- markup({distanceField, neighbors, start: DEEP_WATER, increment: -1, limit: -10});
-
- grid.cells.t = distanceField;
- grid.cells.f = featureIds;
- grid.features = features;
-
- TIME && console.timeEnd("markupGrid");
- }
-
- // mark Pack features (ocean, lakes, islands), calculate distance field and add properties
- function markupPack() {
- TIME && console.time("markupPack");
-
- const {cells, vertices} = pack;
- const {c: neighbors, b: borderCells, i} = cells;
- const packCellsNumber = i.length;
- if (!packCellsNumber) return; // no cells -> there is nothing to do
-
- const distanceField = new Int8Array(packCellsNumber); // pack.cells.t
- const featureIds = new Uint16Array(packCellsNumber); // pack.cells.f
- const haven = createTypedArray({maxValue: packCellsNumber, length: packCellsNumber}); // haven: opposite water cell
- const harbor = new Uint8Array(packCellsNumber); // harbor: number of adjacent water cells
- const features = [0];
-
- const queue = [0];
- for (let featureId = 1; queue[0] !== -1; featureId++) {
- const firstCell = queue[0];
- featureIds[firstCell] = featureId;
-
- const land = isLand(firstCell);
- let border = Boolean(borderCells[firstCell]); // true if feature touches map border
- let totalCells = 1; // count cells in a feature
-
- while (queue.length) {
- const cellId = queue.pop();
- if (borderCells[cellId]) border = true;
- if (!border && borderCells[cellId]) border = true;
-
- for (const neighborId of neighbors[cellId]) {
- const isNeibLand = isLand(neighborId);
-
- if (land && !isNeibLand) {
- distanceField[cellId] = LAND_COAST;
- distanceField[neighborId] = WATER_COAST;
- if (!haven[cellId]) defineHaven(cellId);
- } else if (land && isNeibLand) {
- if (distanceField[neighborId] === UNMARKED && distanceField[cellId] === LAND_COAST)
- distanceField[neighborId] = LANDLOCKED;
- else if (distanceField[cellId] === UNMARKED && distanceField[neighborId] === LAND_COAST)
- distanceField[cellId] = LANDLOCKED;
- }
-
- if (!featureIds[neighborId] && land === isNeibLand) {
- queue.push(neighborId);
- featureIds[neighborId] = featureId;
- totalCells++;
- }
- }
- }
-
- features.push(addFeature({firstCell, land, border, featureId, totalCells}));
- queue[0] = featureIds.findIndex(f => f === UNMARKED); // find unmarked cell
- }
-
- markup({distanceField, neighbors, start: DEEPER_LAND, increment: 1}); // markup pack land
- markup({distanceField, neighbors, start: DEEP_WATER, increment: -1, limit: -10}); // markup pack water
-
- pack.cells.t = distanceField;
- pack.cells.f = featureIds;
- pack.cells.haven = haven;
- pack.cells.harbor = harbor;
- pack.features = features;
-
- TIME && console.timeEnd("markupPack");
-
- function defineHaven(cellId) {
- const waterCells = neighbors[cellId].filter(isWater);
- const distances = waterCells.map(neibCellId => dist2(cells.p[cellId], cells.p[neibCellId]));
- const closest = distances.indexOf(Math.min.apply(Math, distances));
-
- haven[cellId] = waterCells[closest];
- harbor[cellId] = waterCells.length;
- }
-
- function addFeature({firstCell, land, border, featureId, totalCells}) {
- const type = land ? "island" : border ? "ocean" : "lake";
- const [startCell, featureVertices] = getCellsData(type, firstCell);
- const points = clipPoly(featureVertices.map(vertex => vertices.p[vertex]));
- const area = d3.polygonArea(points); // feature perimiter area
- const absArea = Math.abs(rn(area));
-
- const feature = {
- i: featureId,
- type,
- land,
- border,
- cells: totalCells,
- firstCell: startCell,
- vertices: featureVertices,
- area: absArea
- };
-
- if (type === "lake") {
- if (area > 0) feature.vertices = feature.vertices.reverse();
- feature.shoreline = unique(feature.vertices.map(vertex => vertices.c[vertex].filter(isLand)).flat());
- feature.height = Lakes.getHeight(feature);
- }
-
- return feature;
-
- function getCellsData(featureType, firstCell) {
- if (featureType === "ocean") return [firstCell, []];
-
- const getType = cellId => featureIds[cellId];
- const type = getType(firstCell);
- const ofSameType = cellId => getType(cellId) === type;
- const ofDifferentType = cellId => getType(cellId) !== type;
-
- const startCell = findOnBorderCell(firstCell);
- const featureVertices = getFeatureVertices(startCell);
- return [startCell, featureVertices];
-
- function findOnBorderCell(firstCell) {
- const isOnBorder = cellId => borderCells[cellId] || neighbors[cellId].some(ofDifferentType);
- if (isOnBorder(firstCell)) return firstCell;
-
- const startCell = cells.i.filter(ofSameType).find(isOnBorder);
- if (startCell === undefined)
- throw new Error(`Markup: firstCell ${firstCell} is not on the feature or map border`);
-
- return startCell;
- }
-
- function getFeatureVertices(startCell) {
- const startingVertex = cells.v[startCell].find(v => vertices.c[v].some(ofDifferentType));
- if (startingVertex === undefined)
- throw new Error(`Markup: startingVertex for cell ${startCell} is not found`);
-
- return connectVertices({vertices, startingVertex, ofSameType, closeRing: false});
- }
- }
- }
- }
-
- // add properties to pack features
- function defineGroups() {
- const gridCellsNumber = grid.cells.i.length;
- const OCEAN_MIN_SIZE = gridCellsNumber / 25;
- const SEA_MIN_SIZE = gridCellsNumber / 1000;
- const CONTINENT_MIN_SIZE = gridCellsNumber / 10;
- const ISLAND_MIN_SIZE = gridCellsNumber / 1000;
-
- for (const feature of pack.features) {
- if (!feature || feature.type === "ocean") continue;
-
- if (feature.type === "lake") feature.height = Lakes.getHeight(feature);
- feature.group = defineGroup(feature);
- }
-
- function defineGroup(feature) {
- if (feature.type === "island") return defineIslandGroup(feature);
- if (feature.type === "ocean") return defineOceanGroup();
- if (feature.type === "lake") return defineLakeGroup(feature);
- throw new Error(`Markup: unknown feature type ${feature.type}`);
- }
-
- function defineOceanGroup(feature) {
- if (feature.cells > OCEAN_MIN_SIZE) return "ocean";
- if (feature.cells > SEA_MIN_SIZE) return "sea";
- return "gulf";
- }
-
- function defineIslandGroup(feature) {
- const prevFeature = pack.features[pack.cells.f[feature.firstCell - 1]];
- if (prevFeature && prevFeature.type === "lake") return "lake_island";
- if (feature.cells > CONTINENT_MIN_SIZE) return "continent";
- if (feature.cells > ISLAND_MIN_SIZE) return "island";
- return "isle";
- }
-
- function defineLakeGroup(feature) {
- if (feature.temp < -3) return "frozen";
- if (feature.height > 60 && feature.cells < 10 && feature.firstCell % 10 === 0) return "lava";
-
- if (!feature.inlets && !feature.outlet) {
- if (feature.evaporation > feature.flux * 4) return "dry";
- if (feature.cells < 3 && feature.firstCell % 10 === 0) return "sinkhole";
- }
-
- if (!feature.outlet && feature.evaporation > feature.flux) return "salt";
-
- return "freshwater";
- }
- }
-
- return {markupGrid, markupPack, defineGroups};
-})();
diff --git a/public/modules/ocean-layers.js b/public/modules/ocean-layers.js
deleted file mode 100644
index 281fad0a..00000000
--- a/public/modules/ocean-layers.js
+++ /dev/null
@@ -1,92 +0,0 @@
-"use strict";
-
-window.OceanLayers = (function () {
- let cells, vertices, pointsN, used;
-
- const OceanLayers = function OceanLayers() {
- const outline = oceanLayers.attr("layers");
- if (outline === "none") return;
- TIME && console.time("drawOceanLayers");
-
- lineGen.curve(d3.curveBasisClosed);
- (cells = grid.cells), (pointsN = grid.cells.i.length), (vertices = grid.vertices);
- const limits = outline === "random" ? randomizeOutline() : outline.split(",").map(s => +s);
-
- const chains = [];
- const opacity = rn(0.4 / limits.length, 2);
- used = new Uint8Array(pointsN); // to detect already passed cells
-
- for (const i of cells.i) {
- const t = cells.t[i];
- if (t > 0) continue;
- if (used[i] || !limits.includes(t)) continue;
- const start = findStart(i, t);
- if (!start) continue;
- used[i] = 1;
- const chain = connectVertices(start, t); // vertices chain to form a path
- if (chain.length < 4) continue;
- const relax = 1 + t * -2; // select only n-th point
- const relaxed = chain.filter((v, i) => !(i % relax) || vertices.c[v].some(c => c >= pointsN));
- if (relaxed.length < 4) continue;
- const points = clipPoly(
- relaxed.map(v => vertices.p[v]),
- 1
- );
- chains.push([t, points]);
- }
-
- for (const t of limits) {
- const layer = chains.filter(c => c[0] === t);
- let path = layer.map(c => round(lineGen(c[1]))).join("");
- if (path) oceanLayers.append("path").attr("d", path).attr("fill", "#ecf2f9").attr("fill-opacity", opacity);
- }
-
- // find eligible cell vertex to start path detection
- function findStart(i, t) {
- if (cells.b[i]) return cells.v[i].find(v => vertices.c[v].some(c => c >= pointsN)); // map border cell
- return cells.v[i][cells.c[i].findIndex(c => cells.t[c] < t || !cells.t[c])];
- }
-
- TIME && console.timeEnd("drawOceanLayers");
- };
-
- function randomizeOutline() {
- const limits = [];
- let odd = 0.2;
- for (let l = -9; l < 0; l++) {
- if (P(odd)) {
- odd = 0.2;
- limits.push(l);
- } else {
- odd *= 2;
- }
- }
- return limits;
- }
-
- // connect vertices to chain
- function connectVertices(start, t) {
- const chain = []; // vertices chain to form a path
- for (let i = 0, current = start; i === 0 || (current !== start && i < 10000); i++) {
- const prev = chain[chain.length - 1]; // previous vertex in chain
- chain.push(current); // add current vertex to sequence
- const c = vertices.c[current]; // cells adjacent to vertex
- c.filter(c => cells.t[c] === t).forEach(c => (used[c] = 1));
- const v = vertices.v[current]; // neighboring vertices
- const c0 = !cells.t[c[0]] || cells.t[c[0]] === t - 1;
- const c1 = !cells.t[c[1]] || cells.t[c[1]] === t - 1;
- const c2 = !cells.t[c[2]] || cells.t[c[2]] === t - 1;
- if (v[0] !== undefined && v[0] !== prev && c0 !== c1) current = v[0];
- else if (v[1] !== undefined && v[1] !== prev && c1 !== c2) current = v[1];
- else if (v[2] !== undefined && v[2] !== prev && c0 !== c2) current = v[2];
- if (current === chain[chain.length - 1]) {
- ERROR && console.error("Next vertex is not found");
- break;
- }
- }
- chain.push(chain[0]); // push first vertex as the last one
- return chain;
- }
-
- return OceanLayers;
-})();
diff --git a/src/index.html b/src/index.html
index 3fd7ba17..d14cea96 100644
--- a/src/index.html
+++ b/src/index.html
@@ -8493,11 +8493,6 @@
-
-
-
-
-
diff --git a/public/modules/biomes.js b/src/modules/biomes.ts
similarity index 64%
rename from public/modules/biomes.js
rename to src/modules/biomes.ts
index 06280fad..321ea77a 100644
--- a/public/modules/biomes.js
+++ b/src/modules/biomes.ts
@@ -1,10 +1,15 @@
-"use strict";
+import { range, mean } from "d3";
+import { rn } from "../utils";
-window.Biomes = (function () {
- const MIN_LAND_HEIGHT = 20;
+declare global {
+ var Biomes: BiomesModule;
+}
- const getDefault = () => {
- const name = [
+class BiomesModule {
+ private MIN_LAND_HEIGHT = 20;
+
+ getDefault() {
+ const name: string[] = [
"Marine",
"Hot desert",
"Cold desert",
@@ -20,7 +25,7 @@ window.Biomes = (function () {
"Wetland"
];
- const color = [
+ const color: string[] = [
"#466eab",
"#fbe79f",
"#b5b887",
@@ -35,9 +40,9 @@ window.Biomes = (function () {
"#d5e7eb",
"#0b9131"
];
- const habitability = [0, 4, 10, 22, 30, 50, 100, 80, 90, 12, 4, 0, 12];
- const iconsDensity = [0, 3, 2, 120, 120, 120, 120, 150, 150, 100, 5, 0, 250];
- const icons = [
+ const habitability: number[] = [0, 4, 10, 22, 30, 50, 100, 80, 90, 12, 4, 0, 12];
+ const iconsDensity: number[] = [0, 3, 2, 120, 120, 120, 120, 150, 150, 100, 5, 0, 250];
+ const icons: Array<{[key: string]: number}> = [
{},
{dune: 3, cactus: 6, deadTree: 1},
{dune: 9, deadTree: 1},
@@ -52,8 +57,8 @@ window.Biomes = (function () {
{},
{swamp: 1}
];
- const cost = [10, 200, 150, 60, 50, 70, 70, 80, 90, 200, 1000, 5000, 150]; // biome movement cost
- const biomesMartix = [
+ const cost: number[] = [10, 200, 150, 60, 50, 70, 70, 80, 90, 200, 1000, 5000, 150]; // biome movement cost
+ const biomesMatrix: Uint8Array[] = [
// hot ↔ cold [>19°C; <-4°C]; dry ↕ wet
new Uint8Array([1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 10]),
new Uint8Array([3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 9, 9, 9, 9, 10, 10, 10]),
@@ -63,66 +68,66 @@ window.Biomes = (function () {
];
// parse icons weighted array into a simple array
+ const parsedIcons: string[][] = [];
for (let i = 0; i < icons.length; i++) {
- const parsed = [];
+ const parsed: string[] = [];
for (const icon in icons[i]) {
for (let j = 0; j < icons[i][icon]; j++) {
parsed.push(icon);
}
}
- icons[i] = parsed;
+ parsedIcons[i] = parsed;
}
- return {i: d3.range(0, name.length), name, color, biomesMartix, habitability, iconsDensity, icons, cost};
+ return {i: range(0, name.length), name, color, biomesMatrix, habitability, iconsDensity, icons: parsedIcons, cost};
};
- // assign biome id for each cell
- function define() {
+ define() {
TIME && console.time("defineBiomes");
const {fl: flux, r: riverIds, h: heights, c: neighbors, g: gridReference} = pack.cells;
const {temp, prec} = grid.cells;
pack.cells.biome = new Uint8Array(pack.cells.i.length); // biomes array
- for (let cellId = 0; cellId < heights.length; cellId++) {
- const height = heights[cellId];
- const moisture = height < MIN_LAND_HEIGHT ? 0 : calculateMoisture(cellId);
- const temperature = temp[gridReference[cellId]];
- pack.cells.biome[cellId] = getId(moisture, temperature, height, Boolean(riverIds[cellId]));
- }
-
- function calculateMoisture(cellId) {
+ const calculateMoisture = (cellId: number) => {
let moisture = prec[gridReference[cellId]];
if (riverIds[cellId]) moisture += Math.max(flux[cellId] / 10, 2);
const moistAround = neighbors[cellId]
- .filter(neibCellId => heights[neibCellId] >= MIN_LAND_HEIGHT)
- .map(c => prec[gridReference[c]])
+ .filter((neibCellId: number) => heights[neibCellId] >= this.MIN_LAND_HEIGHT)
+ .map((c: number) => prec[gridReference[c]])
.concat([moisture]);
- return rn(4 + d3.mean(moistAround));
+ return rn(4 + (mean(moistAround) as number));
+ }
+
+ for (let cellId = 0; cellId < heights.length; cellId++) {
+ const height = heights[cellId];
+ const moisture = height < this.MIN_LAND_HEIGHT ? 0 : calculateMoisture(cellId);
+ const temperature = temp[gridReference[cellId]];
+ pack.cells.biome[cellId] = this.getId(moisture, temperature, height, Boolean(riverIds[cellId]));
}
TIME && console.timeEnd("defineBiomes");
}
- function getId(moisture, temperature, height, hasRiver) {
+ getId(moisture: number, temperature: number, height: number, hasRiver: boolean) {
if (height < 20) return 0; // all water cells: marine biome
if (temperature < -5) return 11; // too cold: permafrost biome
if (temperature >= 25 && !hasRiver && moisture < 8) return 1; // too hot and dry: hot desert biome
- if (isWetland(moisture, temperature, height)) return 12; // too wet: wetland biome
+ if (this.isWetland(moisture, temperature, height)) return 12; // too wet: wetland biome
// in other cases use biome matrix
const moistureBand = Math.min((moisture / 5) | 0, 4); // [0-4]
const temperatureBand = Math.min(Math.max(20 - temperature, 0), 25); // [0-25]
- return biomesData.biomesMartix[moistureBand][temperatureBand];
+ return biomesData.biomesMatrix[moistureBand][temperatureBand];
}
- function isWetland(moisture, temperature, height) {
+ private isWetland(moisture: number, temperature: number, height: number) {
if (temperature <= -2) return false; // too cold
if (moisture > 40 && height < 25) return true; // near coast
if (moisture > 24 && height > 24 && height < 60) return true; // off coast
return false;
}
+}
- return {getDefault, define, getId};
-})();
+window.Biomes = new BiomesModule();
diff --git a/src/modules/features.ts b/src/modules/features.ts
new file mode 100644
index 00000000..bedb48ff
--- /dev/null
+++ b/src/modules/features.ts
@@ -0,0 +1,328 @@
+import { clipPoly, connectVertices, createTypedArray, distanceSquared, isLand, isWater, rn, TYPED_ARRAY_MAX_VALUES, unique } from "../utils";
+import Alea from "alea";
+import { polygonArea } from "d3";
+
+declare global {
+ var Features: FeatureModule;
+}
+
+type FeatureType = "ocean" | "lake" | "island";
+
+export interface PackedGraphFeature {
+ i: number;
+ type: FeatureType;
+ land: boolean;
+ border: boolean;
+ cells: number;
+ firstCell: number;
+ vertices: number[];
+ area: number;
+ shoreline: number[];
+ height: number;
+ group: string;
+ temp: number;
+ flux: number;
+ evaporation: number;
+ name: string;
+
+ // River related
+ inlets?: number[];
+ outlet?: number;
+ river?: number;
+ enteringFlux?: number;
+ closed?: boolean;
+ outCell?: number;
+}
+
+export interface GridFeature {
+ i: number;
+ land: boolean;
+ border: boolean;
+ type: FeatureType;
+}
+
+class FeatureModule {
+ private DEEPER_LAND = 3;
+ private LANDLOCKED = 2;
+ private LAND_COAST = 1;
+ private UNMARKED = 0;
+ private WATER_COAST = -1;
+ private DEEP_WATER = -2;
+
+ /**
+ * calculate distance to coast for every cell
+ */
+ private markup({ distanceField, neighbors, start, increment, limit = TYPED_ARRAY_MAX_VALUES.INT8_MAX }: {
+ distanceField: Int8Array;
+ neighbors: number[][];
+ start: number;
+ increment: number;
+ limit?: number;
+ }) {
+ for (let distance = start, marked = Infinity; marked > 0 && distance !== limit; distance += increment) {
+ marked = 0;
+ const prevDistance = distance - increment;
+ for (let cellId = 0; cellId < neighbors.length; cellId++) {
+ if (distanceField[cellId] !== prevDistance) continue;
+
+ for (const neighborId of neighbors[cellId]) {
+ if (distanceField[neighborId] !== this.UNMARKED) continue;
+ distanceField[neighborId] = distance;
+ marked++;
+ }
+ }
+ }
+ }
+
+ /**
+ * mark Grid features (ocean, lakes, islands) and calculate distance field
+ */
+ markupGrid() {
+ TIME && console.time("markupGrid");
+ Math.random = Alea(seed); // get the same result on heightmap edit in Erase mode
+
+ const { h: heights, c: neighbors, b: borderCells, i } = grid.cells;
+ const cellsNumber = i.length;
+ const distanceField = new Int8Array(cellsNumber); // gird.cells.t
+ const featureIds = new Uint16Array(cellsNumber); // gird.cells.f
+ const features: GridFeature[] = [];
+
+ const queue = [0];
+ for (let featureId = 1; queue[0] !== -1; featureId++) {
+ const firstCell = queue[0];
+ featureIds[firstCell] = featureId;
+
+ const land = heights[firstCell] >= 20;
+ let border = false; // set true if feature touches map edge
+
+ while (queue.length) {
+ const cellId = queue.pop() as number;
+ if (!border && borderCells[cellId]) border = true;
+
+ for (const neighborId of neighbors[cellId]) {
+ const isNeibLand = heights[neighborId] >= 20;
+
+ if (land === isNeibLand && featureIds[neighborId] === this.UNMARKED) {
+ featureIds[neighborId] = featureId;
+ queue.push(neighborId);
+ } else if (land && !isNeibLand) {
+ distanceField[cellId] = this.LAND_COAST;
+ distanceField[neighborId] = this.WATER_COAST;
+ }
+ }
+ }
+
+ const type = land ? "island" : border ? "ocean" : "lake";
+ features.push({ i: featureId, land, border, type });
+
+ queue[0] = featureIds.findIndex(f => f === this.UNMARKED); // find unmarked cell
+ }
+
+ // markup deep ocean cells
+ this.markup({ distanceField, neighbors, start: this.DEEP_WATER, increment: -1, limit: -10 });
+ grid.cells.t = distanceField;
+ grid.cells.f = featureIds;
+ grid.features = [0, ...features];
+
+ TIME && console.timeEnd("markupGrid");
+ }
+
+ /**
+ * mark PackedGraph features (oceans, lakes, islands) and calculate distance field
+ */
+ markupPack() {
+ const defineHaven = (cellId: number) => {
+ const waterCells = neighbors[cellId].filter((index: number) => isWater(index, pack));
+ const distances = waterCells.map((neibCellId: number) => distanceSquared(cells.p[cellId], cells.p[neibCellId]));
+ const closest = distances.indexOf(Math.min.apply(Math, distances));
+
+ haven[cellId] = waterCells[closest];
+ harbor[cellId] = waterCells.length;
+ }
+
+ const getCellsData = (featureType: string, firstCell: number): [number, number[]] => {
+ if (featureType === "ocean") return [firstCell, []];
+
+ const getType = (cellId: number) => featureIds[cellId];
+ const type = getType(firstCell);
+ const ofSameType = (cellId: number) => getType(cellId) === type;
+ const ofDifferentType = (cellId: number) => getType(cellId) !== type;
+
+ const startCell = findOnBorderCell(firstCell);
+ const featureVertices = getFeatureVertices(startCell);
+ return [startCell, featureVertices];
+
+ function findOnBorderCell(firstCell: number) {
+ const isOnBorder = (cellId: number) => borderCells[cellId] || neighbors[cellId].some(ofDifferentType);
+ if (isOnBorder(firstCell)) return firstCell;
+
+ const startCell = cells.i.filter(ofSameType).find(isOnBorder);
+ if (startCell === undefined)
+ throw new Error(`Markup: firstCell ${firstCell} is not on the feature or map border`);
+
+ return startCell;
+ }
+
+ function getFeatureVertices(startCell: number) {
+ const startingVertex = cells.v[startCell].find((v: number) => vertices.c[v].some(ofDifferentType));
+ if (startingVertex === undefined)
+ throw new Error(`Markup: startingVertex for cell ${startCell} is not found`);
+
+ return connectVertices({ vertices, startingVertex, ofSameType, closeRing: false });
+ }
+ }
+
+ const addFeature = ({ firstCell, land, border, featureId, totalCells }: { firstCell: number; land: boolean; border: boolean; featureId: number; totalCells: number }): PackedGraphFeature => {
+ const type = land ? "island" : border ? "ocean" : "lake";
+ const [startCell, featureVertices] = getCellsData(type, firstCell);
+ const points = clipPoly(featureVertices.map((vertex: number) => vertices.p[vertex]));
+ const area = polygonArea(points); // feature perimiter area
+ const absArea = Math.abs(rn(area));
+
+ const feature: Partial = {
+ i: featureId,
+ type,
+ land,
+ border,
+ cells: totalCells,
+ firstCell: startCell,
+ vertices: featureVertices,
+ area: absArea,
+ shoreline: [],
+ height: 0,
+ };
+
+ if (type === "lake") {
+ if (area > 0) feature.vertices = (feature.vertices as number[]).reverse();
+ feature.shoreline = unique(
+ (feature.vertices as number[])
+ .flatMap(
+ vertexIndex => vertices.c[vertexIndex].filter((index) => isLand(index, pack))
+ )
+ );
+ feature.height = Lakes.getHeight(feature as PackedGraphFeature);
+ }
+
+ return {
+ ...feature
+ } as PackedGraphFeature;
+ }
+
+ TIME && console.time("markupPack");
+
+ const { cells, vertices } = pack;
+ const { c: neighbors, b: borderCells, i } = cells;
+ const packCellsNumber = i.length;
+ if (!packCellsNumber) return; // no cells -> there is nothing to do
+
+ const distanceField = new Int8Array(packCellsNumber); // pack.cells.t
+ const featureIds = new Uint16Array(packCellsNumber); // pack.cells.f
+ const haven = createTypedArray({ maxValue: packCellsNumber, length: packCellsNumber }); // haven: opposite water cell
+ const harbor = new Uint8Array(packCellsNumber); // harbor: number of adjacent water cells
+ const features: PackedGraphFeature[] = [];
+
+ const queue = [0];
+ for (let featureId = 1; queue[0] !== -1; featureId++) {
+ const firstCell = queue[0];
+ featureIds[firstCell] = featureId;
+
+ const land = isLand(firstCell, pack);
+ let border = Boolean(borderCells[firstCell]); // true if feature touches map border
+ let totalCells = 1; // count cells in a feature
+
+ while (queue.length) {
+ const cellId = queue.pop() as number;
+ if (borderCells[cellId]) border = true;
+
+ for (const neighborId of neighbors[cellId]) {
+ const isNeibLand = isLand(neighborId, pack);
+
+ if (land && !isNeibLand) {
+ distanceField[cellId] = this.LAND_COAST;
+ distanceField[neighborId] = this.WATER_COAST;
+ if (!haven[cellId]) defineHaven(cellId);
+ } else if (land && isNeibLand) {
+ if (distanceField[neighborId] === this.UNMARKED && distanceField[cellId] === this.LAND_COAST)
+ distanceField[neighborId] = this.LANDLOCKED;
+ else if (distanceField[cellId] === this.UNMARKED && distanceField[neighborId] === this.LAND_COAST)
+ distanceField[cellId] = this.LANDLOCKED;
+ }
+
+ if (!featureIds[neighborId] && land === isNeibLand) {
+ queue.push(neighborId);
+ featureIds[neighborId] = featureId;
+ totalCells++;
+ }
+ }
+ }
+
+ features.push(addFeature({ firstCell, land, border, featureId, totalCells }));
+ queue[0] = featureIds.findIndex(f => f === this.UNMARKED); // find unmarked cell
+ }
+
+ this.markup({ distanceField, neighbors, start: this.DEEPER_LAND, increment: 1 }); // markup pack land
+ this.markup({ distanceField, neighbors, start: this.DEEP_WATER, increment: -1, limit: -10 }); // markup pack water
+
+ pack.cells.t = distanceField;
+ pack.cells.f = featureIds;
+ pack.cells.haven = haven;
+ pack.cells.harbor = harbor;
+ pack.features = [0 as unknown as PackedGraphFeature, ...features];
+ TIME && console.timeEnd("markupPack");
+ }
+
+ /**
+ * define feature groups (ocean, sea, gulf, continent, island, isle, freshwater lake, salt lake, etc.)
+ */
+ defineGroups() {
+ const gridCellsNumber = grid.cells.i.length;
+ 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 defineIslandGroup = (feature: PackedGraphFeature) => {
+ const prevFeature = pack.features[pack.cells.f[feature.firstCell - 1]];
+ if (prevFeature && prevFeature.type === "lake") return "lake_island";
+ if (feature.cells > CONTINENT_MIN_SIZE) return "continent";
+ if (feature.cells > ISLAND_MIN_SIZE) return "island";
+ return "isle";
+ }
+
+ const defineOceanGroup = (feature: PackedGraphFeature) => {
+ if (feature.cells > OCEAN_MIN_SIZE) return "ocean";
+ if (feature.cells > SEA_MIN_SIZE) return "sea";
+ return "gulf";
+ }
+
+ const defineLakeGroup = (feature: PackedGraphFeature) => {
+ if (feature.temp < -3) return "frozen";
+ if (feature.height > 60 && feature.cells < 10 && feature.firstCell % 10 === 0) return "lava";
+
+ if (!feature.inlets && !feature.outlet) {
+ if (feature.evaporation > feature.flux * 4) return "dry";
+ if (feature.cells < 3 && feature.firstCell % 10 === 0) return "sinkhole";
+ }
+
+ if (!feature.outlet && feature.evaporation > feature.flux) return "salt";
+
+ return "freshwater";
+ }
+
+ const defineGroup = (feature: PackedGraphFeature) => {
+ if (feature.type === "island") return defineIslandGroup(feature);
+ if (feature.type === "ocean") return defineOceanGroup(feature);
+ if (feature.type === "lake") return defineLakeGroup(feature);
+ throw new Error(`Markup: unknown feature type ${feature.type}`);
+ }
+
+ for (const feature of pack.features) {
+ if (!feature || feature.type === "ocean") continue;
+
+ if (feature.type === "lake") feature.height = Lakes.getHeight(feature);
+ feature.group = defineGroup(feature);
+ }
+ }
+}
+
+window.Features = new FeatureModule();
diff --git a/src/modules/heightmap-generator.ts b/src/modules/heightmap-generator.ts
index 27fb063b..a060ecdc 100644
--- a/src/modules/heightmap-generator.ts
+++ b/src/modules/heightmap-generator.ts
@@ -3,12 +3,7 @@ import { range as d3Range, leastIndex, mean } from "d3";
import { createTypedArray, byId, findGridCell, getNumberInRange, lim, minmax, P, rand } from "../utils";
declare global {
- interface Window {
- HeightmapGenerator: HeightmapGenerator;
- }
- var heightmapTemplates: any;
- var TIME: boolean;
- var ERROR: boolean;
+ var HeightmapGenerator: HeightmapGenerator;
}
type Tool = "Hill" | "Pit" | "Range" | "Trough" | "Strait" | "Mask" | "Invert" | "Add" | "Multiply" | "Smooth";
@@ -19,21 +14,6 @@ class HeightmapGenerator {
blobPower: number = 0;
linePower: number = 0;
- // TODO: remove after migration to TS and use param in constructor
- get seed() {
- return (window as any).seed;
- }
- get graphWidth() {
- return (window as any).graphWidth;
- }
- get graphHeight() {
- return (window as any).graphHeight;
- }
-
- constructor() {
-
- }
-
private clearData() {
this.heights = null;
this.grid = null;
@@ -107,8 +87,8 @@ class HeightmapGenerator {
let h = lim(getNumberInRange(height));
do {
- const x = this.getPointInRange(rangeX, this.graphWidth);
- const y = this.getPointInRange(rangeY, this.graphHeight);
+ const x = this.getPointInRange(rangeX, graphWidth);
+ const y = this.getPointInRange(rangeY, graphHeight);
if (x === undefined || y === undefined) return;
start = findGridCell(x, y, this.grid);
limit++;
@@ -143,8 +123,8 @@ class HeightmapGenerator {
let h = lim(getNumberInRange(height));
do {
- const x = this.getPointInRange(rangeX, this.graphWidth);
- const y = this.getPointInRange(rangeY, this.graphHeight);
+ const x = this.getPointInRange(rangeX, graphWidth);
+ const y = this.getPointInRange(rangeY, graphHeight);
if (x === undefined || y === undefined) return;
start = findGridCell(x, y, this.grid);
limit++;
@@ -207,8 +187,8 @@ class HeightmapGenerator {
if (rangeX && rangeY) {
// find start and end points
- const startX = this.getPointInRange(rangeX, this.graphWidth) as number;
- const startY = this.getPointInRange(rangeY, this.graphHeight) as number;
+ const startX = this.getPointInRange(rangeX, graphWidth) as number;
+ const startY = this.getPointInRange(rangeY, graphHeight) as number;
let dist = 0;
let limit = 0;
@@ -216,11 +196,11 @@ class HeightmapGenerator {
let endX;
do {
- endX = Math.random() * this.graphWidth * 0.8 + this.graphWidth * 0.1;
- endY = Math.random() * this.graphHeight * 0.7 + this.graphHeight * 0.15;
+ endX = Math.random() * graphWidth * 0.8 + graphWidth * 0.1;
+ endY = Math.random() * graphHeight * 0.7 + graphHeight * 0.15;
dist = Math.abs(endY - startY) + Math.abs(endX - startX);
limit++;
- } while ((dist < this.graphWidth / 8 || dist > this.graphWidth / 3) && limit < 50);
+ } while ((dist < graphWidth / 8 || dist > graphWidth / 3) && limit < 50);
startCellId = findGridCell(startX, startY, this.grid);
endCellId = findGridCell(endX, endY, this.grid);
@@ -311,19 +291,19 @@ class HeightmapGenerator {
let endX: number;
let endY: number;
do {
- startX = this.getPointInRange(rangeX, this.graphWidth) as number;
- startY = this.getPointInRange(rangeY, this.graphHeight) as number;
+ startX = this.getPointInRange(rangeX, graphWidth) as number;
+ startY = this.getPointInRange(rangeY, graphHeight) as number;
startCellId = findGridCell(startX, startY, this.grid);
limit++;
} while (this.heights[startCellId] < 20 && limit < 50);
limit = 0;
do {
- endX = Math.random() * this.graphWidth * 0.8 + this.graphWidth * 0.1;
- endY = Math.random() * this.graphHeight * 0.7 + this.graphHeight * 0.15;
+ endX = Math.random() * graphWidth * 0.8 + graphWidth * 0.1;
+ endY = Math.random() * graphHeight * 0.7 + graphHeight * 0.15;
dist = Math.abs(endY - startY) + Math.abs(endX - startX);
limit++;
- } while ((dist < this.graphWidth / 8 || dist > this.graphWidth / 2) && limit < 50);
+ } while ((dist < graphWidth / 8 || dist > graphWidth / 2) && limit < 50);
endCellId = findGridCell(endX, endY, this.grid);
}
@@ -378,14 +358,14 @@ class HeightmapGenerator {
if (desiredWidth < 1 && P(desiredWidth)) return;
const used = new Uint8Array(this.heights.length);
const vert = direction === "vertical";
- const startX = vert ? Math.floor(Math.random() * this.graphWidth * 0.4 + this.graphWidth * 0.3) : 5;
- const startY = vert ? 5 : Math.floor(Math.random() * this.graphHeight * 0.4 + this.graphHeight * 0.3);
+ const startX = vert ? Math.floor(Math.random() * graphWidth * 0.4 + graphWidth * 0.3) : 5;
+ const startY = vert ? 5 : Math.floor(Math.random() * graphHeight * 0.4 + graphHeight * 0.3);
const endX = vert
- ? Math.floor(this.graphWidth - startX - this.graphWidth * 0.1 + Math.random() * this.graphWidth * 0.2)
- : this.graphWidth - 5;
+ ? Math.floor(graphWidth - startX - graphWidth * 0.1 + Math.random() * graphWidth * 0.2)
+ : graphWidth - 5;
const endY = vert
- ? this.graphHeight - 5
- : Math.floor(this.graphHeight - startY - this.graphHeight * 0.1 + Math.random() * this.graphHeight * 0.2);
+ ? graphHeight - 5
+ : Math.floor(graphHeight - startY - graphHeight * 0.1 + Math.random() * graphHeight * 0.2);
const start = findGridCell(startX, startY, this.grid);
const end = findGridCell(endX, endY, this.grid);
@@ -462,8 +442,8 @@ class HeightmapGenerator {
this.heights = this.heights.map((h, i) => {
const [x, y] = this.grid.points[i];
- const nx = (2 * x) / this.graphWidth - 1; // [-1, 1], 0 is center
- const ny = (2 * y) / this.graphHeight - 1; // [-1, 1], 0 is center
+ const nx = (2 * x) / graphWidth - 1; // [-1, 1], 0 is center
+ const ny = (2 * y) / graphHeight - 1; // [-1, 1], 0 is center
let distance = (1 - nx ** 2) * (1 - ny ** 2); // 1 is center, 0 is edge
if (power < 0) distance = 1 - distance; // inverted, 0 is center, 1 is edge
const masked = h * distance;
@@ -509,7 +489,7 @@ class HeightmapGenerator {
TIME && console.time("defineHeightmap");
const id = (byId("templateInput")! as HTMLInputElement).value;
- Math.random = Alea(this.seed);
+ Math.random = Alea(seed);
const isTemplate = id in heightmapTemplates;
const heights = isTemplate ? this.fromTemplate(graph, id) : await this.fromPrecreated(graph, id);
diff --git a/src/modules/index.ts b/src/modules/index.ts
index fe1135c0..41beaabd 100644
--- a/src/modules/index.ts
+++ b/src/modules/index.ts
@@ -1,2 +1,7 @@
import "./voronoi";
-import "./heightmap-generator";
\ No newline at end of file
+import "./heightmap-generator";
+import "./features";
+import "./lakes";
+import "./ocean-layers";
+import "./river-generator";
+import "./biomes"
diff --git a/public/modules/lakes.js b/src/modules/lakes.ts
similarity index 67%
rename from public/modules/lakes.js
rename to src/modules/lakes.ts
index 8ce18793..6fa381ac 100644
--- a/public/modules/lakes.js
+++ b/src/modules/lakes.ts
@@ -1,12 +1,93 @@
-"use strict";
+import { PackedGraphFeature } from "./features";
+import { min, mean } from "d3";
+import { byId,
+rn } from "../utils";
-window.Lakes = (function () {
- const LAKE_ELEVATION_DELTA = 0.1;
+declare global {
+ var Lakes: LakesModule;
+}
+
+export class LakesModule {
+ private LAKE_ELEVATION_DELTA = 0.1;
+
+ getHeight(feature: PackedGraphFeature) {
+ const heights = pack.cells.h;
+ const minShoreHeight = min(feature.shoreline.map(cellId => heights[cellId])) || 20;
+ return rn(minShoreHeight - this.LAKE_ELEVATION_DELTA, 2);
+ };
+
+ defineNames() {
+ pack.features.forEach((feature: PackedGraphFeature) => {
+ if (feature.type !== "lake") return;
+ feature.name = this.getName(feature);
+ });
+ };
+
+ getName(feature: PackedGraphFeature): string {
+ const landCell = feature.shoreline[0];
+ const culture = pack.cells.culture[landCell];
+ return Names.getCulture(culture);
+ };
+
+ cleanupLakeData = function () {
+ 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;
+ }
+ };
+
+ defineClimateData(heights: number[] | Uint8Array) {
+ const {cells, features} = pack;
+ const lakeOutCells = new Uint16Array(cells.i.length);
+
+ const getFlux = (lake: PackedGraphFeature) => {
+ return lake.shoreline.reduce((acc, c) => acc + grid.cells.prec[cells.g[c]], 0);
+ }
+
+ const getLakeTemp = (lake: PackedGraphFeature) => {
+ if (lake.cells < 6) return grid.cells.temp[cells.g[lake.firstCell]];
+ return rn(mean(lake.shoreline.map(c => grid.cells.temp[cells.g[c]])) as number, 1);
+ }
+
+ const getLakeEvaporation = (lake: PackedGraphFeature) => {
+ const height = (lake.height - 18) ** Number(heightExponentInput.value); // height in meters
+ const evaporation = ((700 * (lake.temp + 0.006 * height)) / 50 + 75) / (80 - lake.temp); // based on Penman formula, [1-11]
+ return rn(evaporation * lake.cells);
+ }
+
+ const getLowestShoreCell = (lake: PackedGraphFeature) => {
+ return lake.shoreline.sort((a, b) => heights[a] - heights[b])[0];
+ }
+
+ features.forEach(feature => {
+ if (feature.type !== "lake") return;
+ feature.flux = getFlux(feature);
+ feature.temp = getLakeTemp(feature);
+ feature.evaporation = getLakeEvaporation(feature);
+ if (feature.closed) return; // no outlet for lakes in depressed areas
+
+ feature.outCell = getLowestShoreCell(feature);
+ lakeOutCells[feature.outCell as number] = feature.i;
+ });
+
+ return lakeOutCells;
+ };
// check if lake can be potentially open (not in deep depression)
- const detectCloseLakes = h => {
+ detectCloseLakes(h: number[] | Uint8Array) {
const {cells} = pack;
- const ELEVATION_LIMIT = +byId("lakeElevationLimitOutput").value;
+ const ELEVATION_LIMIT = +(byId("lakeElevationLimitOutput") as HTMLInputElement)?.value;
pack.features.forEach(feature => {
if (feature.type !== "lake") return;
@@ -25,7 +106,7 @@ window.Lakes = (function () {
checked[lowestShorelineCell] = true;
while (queue.length && isDeep) {
- const cellId = queue.pop();
+ const cellId: number = queue.pop() as number;
for (const neibCellId of cells.c[cellId]) {
if (checked[neibCellId]) continue;
@@ -44,80 +125,6 @@ window.Lakes = (function () {
feature.closed = isDeep;
});
};
+}
- const defineClimateData = function (heights) {
- const {cells, features} = pack;
- const lakeOutCells = new Uint16Array(cells.i.length);
-
- features.forEach(feature => {
- if (feature.type !== "lake") return;
- feature.flux = getFlux(feature);
- feature.temp = getLakeTemp(feature);
- feature.evaporation = getLakeEvaporation(feature);
- if (feature.closed) return; // no outlet for lakes in depressed areas
-
- feature.outCell = getLowestShoreCell(feature);
- lakeOutCells[feature.outCell] = feature.i;
- });
-
- return lakeOutCells;
-
- function getFlux(lake) {
- return lake.shoreline.reduce((acc, c) => acc + grid.cells.prec[cells.g[c]], 0);
- }
-
- function getLakeTemp(lake) {
- if (lake.cells < 6) return grid.cells.temp[cells.g[lake.firstCell]];
- return rn(d3.mean(lake.shoreline.map(c => grid.cells.temp[cells.g[c]])), 1);
- }
-
- function getLakeEvaporation(lake) {
- const height = (lake.height - 18) ** heightExponentInput.value; // height in meters
- const evaporation = ((700 * (lake.temp + 0.006 * height)) / 50 + 75) / (80 - lake.temp); // based on Penman formula, [1-11]
- return rn(evaporation * lake.cells);
- }
-
- function getLowestShoreCell(lake) {
- return lake.shoreline.sort((a, b) => heights[a] - heights[b])[0];
- }
- };
-
- const cleanupLakeData = function () {
- 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 getHeight = function (feature) {
- const heights = pack.cells.h;
- const minShoreHeight = d3.min(feature.shoreline.map(cellId => heights[cellId])) || 20;
- return rn(minShoreHeight - LAKE_ELEVATION_DELTA, 2);
- };
-
- const defineNames = function () {
- pack.features.forEach(feature => {
- if (feature.type !== "lake") return;
- feature.name = getName(feature);
- });
- };
-
- const getName = function (feature) {
- const landCell = feature.shoreline[0];
- const culture = pack.cells.culture[landCell];
- return Names.getCulture(culture);
- };
-
- return {defineClimateData, cleanupLakeData, detectCloseLakes, getHeight, defineNames, getName};
-})();
+window.Lakes = new LakesModule();
\ No newline at end of file
diff --git a/src/modules/ocean-layers.ts b/src/modules/ocean-layers.ts
new file mode 100644
index 00000000..11467ea5
--- /dev/null
+++ b/src/modules/ocean-layers.ts
@@ -0,0 +1,110 @@
+import { line, curveBasisClosed } from 'd3';
+import type { Selection } from 'd3';
+import { clipPoly,P,rn,round } from '../utils';
+
+declare global {
+ var OceanLayers: typeof OceanModule.prototype.draw;
+}
+class OceanModule {
+ private cells: any;
+ private vertices: any;
+ private pointsN: any;
+ private used: any;
+ private lineGen = line().curve(curveBasisClosed);
+ private oceanLayers: Selection;
+
+
+ constructor(oceanLayers: Selection) {
+ this.oceanLayers = oceanLayers;
+ }
+
+ randomizeOutline() {
+ const limits = [];
+ let odd = 0.2;
+ for (let l = -9; l < 0; l++) {
+ if (P(odd)) {
+ odd = 0.2;
+ limits.push(l);
+ } else {
+ odd *= 2;
+ }
+ }
+ return limits;
+ }
+
+ // connect vertices to chain
+ connectVertices(start: number, t: number) {
+ const chain = []; // vertices chain to form a path
+ for (let i = 0, current = start; i === 0 || (current !== start && i < 10000); i++) {
+ const prev = chain[chain.length - 1]; // previous vertex in chain
+ chain.push(current); // add current vertex to sequence
+ const c = this.vertices.c[current]; // cells adjacent to vertex
+ c.filter((c: number) => this.cells.t[c] === t).forEach((c: number) => (this.used[c] = 1));
+ const v = this.vertices.v[current]; // neighboring vertices
+ const c0 = !this.cells.t[c[0]] || this.cells.t[c[0]] === t - 1;
+ const c1 = !this.cells.t[c[1]] || this.cells.t[c[1]] === t - 1;
+ const c2 = !this.cells.t[c[2]] || this.cells.t[c[2]] === t - 1;
+ if (v[0] !== undefined && v[0] !== prev && c0 !== c1) current = v[0];
+ else if (v[1] !== undefined && v[1] !== prev && c1 !== c2) current = v[1];
+ else if (v[2] !== undefined && v[2] !== prev && c0 !== c2) current = v[2];
+ if (current === chain[chain.length - 1]) {
+ ERROR && console.error("Next vertex is not found");
+ break;
+ }
+ }
+ chain.push(chain[0]); // push first vertex as the last one
+ return chain;
+ }
+
+ // find eligible cell vertex to start path detection
+ findStart(i: number, t: number) {
+ if (this.cells.b[i]) return this.cells.v[i].find((v: number) => this.vertices.c[v].some((c: number) => c >= this.pointsN)); // map border cell
+ return this.cells.v[i][this.cells.c[i].findIndex((c: number)=> this.cells.t[c] < t || !this.cells.t[c])];
+ }
+
+ draw() {
+ const outline = this.oceanLayers.attr("layers");
+ if (outline === "none") return;
+ TIME && console.time("drawOceanLayers");
+ this.cells = grid.cells;
+ this.pointsN = grid.cells.i.length;
+ this.vertices = grid.vertices;
+ const limits = outline === "random" ? this.randomizeOutline() : outline.split(",").map((s: string) => +s);
+
+ const chains: [number, any[]][] = [];
+ const opacity = rn(0.4 / limits.length, 2);
+ this.used = new Uint8Array(this.pointsN); // to detect already passed cells
+
+ for (const i of this.cells.i) {
+ const t = this.cells.t[i];
+ if (t > 0) continue;
+ if (this.used[i] || !limits.includes(t)) continue;
+ const start = this.findStart(i, t);
+ if (!start) continue;
+ this.used[i] = 1;
+ const chain = this.connectVertices(start, t); // vertices chain to form a path
+ if (chain.length < 4) continue;
+ const relax = 1 + t * -2; // select only n-th point
+ const relaxed = chain.filter((v, i) => !(i % relax) || this.vertices.c[v].some((c: number) => c >= this.pointsN));
+ if (relaxed.length < 4) continue;
+
+ const points = clipPoly(
+ relaxed.map(v => this.vertices.p[v]),
+ graphWidth,
+ graphHeight,
+ 1
+ );
+ chains.push([t, points]);
+ }
+
+ for (const t of limits) {
+ const layer = chains.filter((c: [number, any[]]) => c[0] === t);
+ let path = layer.map((c: [number, any[]]) => round(this.lineGen(c[1]) || "")).join("");
+ if (path) this.oceanLayers.append("path").attr("d", path).attr("fill", "#ecf2f9").attr("fill-opacity", opacity);
+ }
+
+ TIME && console.timeEnd("drawOceanLayers");
+ }
+}
+
+window.OceanLayers = () => new OceanModule(oceanLayers).draw();
diff --git a/public/modules/river-generator.js b/src/modules/river-generator.ts
similarity index 63%
rename from public/modules/river-generator.js
rename to src/modules/river-generator.ts
index 254e1af8..55cedaa1 100644
--- a/public/modules/river-generator.js
+++ b/src/modules/river-generator.ts
@@ -1,66 +1,89 @@
-"use strict";
+import Alea from "alea";
+import { each, rn, round, rw} from "../utils";
+import { curveBasis, line, mean, min, sum, curveCatmullRom } from "d3";
-window.Rivers = (function () {
- const generate = function (allowErosion = true) {
+
+
+declare global {
+ var Rivers: RiverModule;
+}
+
+export interface River {
+ i: number; // river id
+ source: number; // source cell index
+ mouth: number; // mouth cell index
+ parent: number; // parent river id
+ basin: number; // basin river id
+ length: number; // river length
+ discharge: number; // river discharge in m3/s
+ width: number; // mouth width in km
+ widthFactor: number; // width scaling factor
+ sourceWidth: number; // source width in km
+ name: string; // river name
+ type: string; // river type
+ cells: number[]; // cells forming the river path
+}
+
+class RiverModule {
+ private FLUX_FACTOR = 500;
+ private MAX_FLUX_WIDTH = 1;
+ private LENGTH_FACTOR = 200;
+ private LENGTH_STEP_WIDTH = 1 / this.LENGTH_FACTOR;
+ private LENGTH_PROGRESSION = [1, 1, 2, 3, 5, 8, 13, 21, 34].map(n => n / this.LENGTH_FACTOR);
+ private lineGen = line().curve(curveBasis)
+
+ riverTypes = {
+ main: {
+ big: {River: 1},
+ small: {Creek: 9, River: 3, Brook: 3, Stream: 1}
+ },
+ fork: {
+ big: {Fork: 1},
+ small: {Branch: 1}
+ }
+ };
+
+ smallLength: number | null = null;
+
+ generate(allowErosion = true) {
TIME && console.time("generateRivers");
- Math.random = aleaPRNG(seed);
+ Math.random = Alea(seed);
const {cells, features} = pack;
- const riversData = {}; // rivers data
- const riverParents = {};
+ const riversData: {[riverId: number]: number[]} = {};
+ const riverParents: {[key: number]: number} = {};
- const addCellToRiver = function (cell, river) {
- if (!riversData[river]) riversData[river] = [cell];
- else riversData[river].push(cell);
+ const addCellToRiver = (cellId: number, riverId: number) => {
+ if (!riversData[riverId]) riversData[riverId] = [cellId];
+ else riversData[riverId].push(cellId);
};
- 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 h = alterHeights();
- Lakes.detectCloseLakes(h);
- resolveDepressions(h);
- drainWater();
- defineRivers();
-
- calculateConfluenceFlux();
- Lakes.cleanupLakeData();
-
- if (allowErosion) {
- cells.h = Uint8Array.from(h); // apply gradient
- downcutRivers(); // downcut river beds
- }
-
- TIME && console.timeEnd("generateRivers");
-
- function drainWater() {
+ const drainWater = () => {
const MIN_FLUX_TO_FORM_RIVER = 30;
- const cellsNumberModifier = (pointsInput.dataset.cells / 10000) ** 0.25;
+ const cellsNumberModifier = ((pointsInput.dataset.cells as any) / 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 land = cells.i.filter((i: number) => h[i] >= 20).sort((a: number, b: number) => h[b] - h[a]);
const lakeOutCells = Lakes.defineClimateData(h);
- land.forEach(function (i) {
+ land.forEach(function (i: number) {
cells.fl[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)
+ ? features.filter((feature: any) => 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);
+ const lakeCell = cells.c[i].find((c: number) => 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
// allow chain lakes to retain identity
if (cells.r[lakeCell] !== lake.river) {
- const sameRiver = cells.c[lakeCell].some(c => cells.r[c] === lake.river);
+ const sameRiver = cells.c[lakeCell].some((c: number) => cells.r[c] === lake.river);
if (sameRiver) {
- cells.r[lakeCell] = lake.river;
- addCellToRiver(lakeCell, lake.river);
+ cells.r[lakeCell] = lake.river as number;
+ addCellToRiver(lakeCell, lake.river as number);
} else {
cells.r[lakeCell] = riverNext;
addCellToRiver(lakeCell, riverNext);
@@ -77,7 +100,7 @@ window.Rivers = (function () {
for (const lake of lakes) {
if (!Array.isArray(lake.inlets)) continue;
for (const inlet of lake.inlets) {
- riverParents[inlet] = outlet;
+ riverParents[inlet] = outlet as number;
}
}
@@ -87,12 +110,12 @@ window.Rivers = (function () {
// 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];
+ const filtered = cells.c[i].filter((c: number) => !lakes.map((lake: any) => lake.i).includes(cells.f[c]));
+ min = filtered.sort((a: number, b: number) => h[a] - h[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: number, b: number) => h[a] - h[b])[0];
}
// cells is depressed
@@ -124,7 +147,7 @@ window.Rivers = (function () {
});
}
- function flowDown(toCell, fromFlux, river) {
+ const flowDown = (toCell: number, fromFlux: number, river: number) => {
const toFlux = cells.fl[toCell] - cells.conf[toCell];
const toRiver = cells.r[toCell];
@@ -144,7 +167,7 @@ window.Rivers = (function () {
// pour water to the water body
const waterBody = features[cells.f[toCell]];
if (waterBody.type === "lake") {
- if (!waterBody.river || fromFlux > waterBody.enteringFlux) {
+ if (!waterBody.river || fromFlux > (waterBody.enteringFlux as number)) {
waterBody.river = river;
waterBody.enteringFlux = fromFlux;
}
@@ -160,13 +183,13 @@ window.Rivers = (function () {
addCellToRiver(toCell, river);
}
- function defineRivers() {
+ const defineRivers = () => {
// re-initialize rivers and confluence arrays
cells.r = new Uint16Array(cells.i.length);
cells.conf = new Uint16Array(cells.i.length);
pack.rivers = [];
- const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2);
+ const defaultWidthFactor = rn(1 / ((pointsInput.dataset.cells as any) / 10000) ** 0.25, 2);
const mainStemWidthFactor = defaultWidthFactor * 1.2;
for (const key in riversData) {
@@ -187,12 +210,12 @@ window.Rivers = (function () {
const parent = riverParents[key] || 0;
const widthFactor = !parent || parent === riverId ? mainStemWidthFactor : defaultWidthFactor;
- const meanderedPoints = addMeandering(riverCells);
+ const meanderedPoints = this.addMeandering(riverCells);
const discharge = cells.fl[mouth]; // m3 in second
- const length = getApproximateLength(meanderedPoints);
- const sourceWidth = getSourceWidth(cells.fl[source]);
- const width = getWidth(
- getOffset({
+ const length = this.getApproximateLength(meanderedPoints);
+ const sourceWidth = this.getSourceWidth(cells.fl[source]);
+ const width = this.getWidth(
+ this.getOffset({
flux: discharge,
pointIndex: meanderedPoints.length,
widthFactor,
@@ -211,19 +234,19 @@ window.Rivers = (function () {
sourceWidth,
parent,
cells: riverCells
- });
+ } as River);
}
}
- function downcutRivers() {
+ const downcutRivers = () => {
const MAX_DOWNCUT = 5;
for (const i of pack.cells.i) {
if (cells.h[i] < 35) continue; // don't donwcut lowlands
if (!cells.fl[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 higherCells = cells.c[i].filter((c: number) => cells.h[c] > cells.h[i]);
+ const higherFlux = higherCells.reduce((acc: number, c: number) => acc + cells.fl[c], 0) / higherCells.length;
if (!higherFlux) continue;
const downcut = Math.floor(cells.fl[i] / higherFlux);
@@ -231,48 +254,68 @@ window.Rivers = (function () {
}
}
- function calculateConfluenceFlux() {
+ const calculateConfluenceFlux = () => {
for (const i of cells.i) {
if (!cells.conf[i]) continue;
const sortedInflux = cells.c[i]
- .filter(c => cells.r[c] && h[c] > h[i])
- .map(c => cells.fl[c])
- .sort((a, b) => b - a);
- cells.conf[i] = sortedInflux.reduce((acc, flux, index) => (index ? acc + flux : acc), 0);
+ .filter((c: number) => cells.r[c] && h[c] > h[i])
+ .map((c: number) => cells.fl[c])
+ .sort((a: number, b: number) => b - a);
+ cells.conf[i] = sortedInflux.reduce((acc: number, flux: number, index: number) => (index ? acc + flux : acc), 0);
}
}
+
+ 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 h = this.alterHeights();
+ Lakes.detectCloseLakes(h);
+ this.resolveDepressions(h);
+ drainWater();
+ defineRivers();
+
+ calculateConfluenceFlux();
+ Lakes.cleanupLakeData();
+
+ if (allowErosion) {
+ cells.h = Uint8Array.from(h); // apply gradient
+ downcutRivers(); // downcut river beds
+ }
+
+ TIME && console.timeEnd("generateRivers");
};
- // add distance to water value to land cells to make map less depressed
- const alterHeights = () => {
- const {h, c, t} = pack.cells;
+ alterHeights(): number[] {
+ const {h, c, t} = pack.cells as {h: Uint8Array, c: number[][], t: Uint8Array};
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;
+ return h + t[i] / 100 + (mean(c[i].map(c => t[c])) as number) / 10000;
});
};
// depression filling algorithm (for a correct water flux modeling)
- const resolveDepressions = function (h) {
+ resolveDepressions(h: number[]) {
const {cells, features} = pack;
- const maxIterations = +document.getElementById("resolveDepressionsStepsOutput").value;
+ const maxIterations = +(document.getElementById("resolveDepressionsStepsOutput") as HTMLInputElement)?.value;
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 height = (i: number) => features[cells.f[i]].height || h[i]; // height of lake or specific cell
- const lakes = features.filter(f => f.type === "lake");
- 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
+ const lakes = features.filter((feature) => feature.type === "lake");
+ const land = cells.i.filter((i: number) => h[i] >= 20 && !cells.b[i]); // exclude near-border cells
+ land.sort((a: number, b: number) => h[a] - h[b]); // lowest cells go first
const progress = [];
let depressions = Infinity;
let prevDepressions = null;
for (let iteration = 0; depressions && iteration < maxIterations; iteration++) {
- if (progress.length > 5 && d3.sum(progress) > 0) {
+ if (progress.length > 5 && sum(progress) > 0) {
// bad progress, abort and set heights back
- h = alterHeights();
+ h = this.alterHeights();
depressions = progress[0];
break;
}
@@ -282,23 +325,23 @@ window.Rivers = (function () {
if (iteration < checkLakeMaxIteration) {
for (const l of lakes) {
if (l.closed) continue;
- const minHeight = d3.min(l.shoreline.map(s => h[s]));
+ const minHeight = min(l.shoreline.map((s: number) => h[s])) as number;
if (minHeight >= 100 || l.height > minHeight) continue;
if (iteration > elevateLakeMaxIteration) {
- l.shoreline.forEach(i => (h[i] = cells.h[i]));
- l.height = d3.min(l.shoreline.map(s => h[s])) - 1;
+ l.shoreline.forEach((i: number) => (h[i] = cells.h[i]));
+ l.height = (min(l.shoreline.map((s: number) => h[s])) as number) - 1;
l.closed = true;
continue;
}
depressions++;
- l.height = minHeight + 0.2;
+ l.height = (minHeight as number) + 0.2;
}
}
for (const i of land) {
- const minHeight = d3.min(cells.c[i].map(c => height(c)));
+ const minHeight = min(cells.c[i].map((c: number) => height(c))) as number;
if (minHeight >= 100 || h[i] > minHeight) continue;
depressions++;
@@ -312,12 +355,11 @@ window.Rivers = (function () {
depressions && WARN && console.warn(`Unresolved depressions: ${depressions}. Edit heightmap to fix`);
};
- // add points at 1/3 and 2/3 of a line between adjacents river cells
- const addMeandering = function (riverCells, riverPoints = null, meandering = 0.5) {
+ addMeandering(riverCells: number[], riverPoints = null, meandering = 0.5): [number, number, number][] {
const {fl, h} = pack.cells;
const meandered = [];
const lastStep = riverCells.length - 1;
- const points = getRiverPoints(riverCells, riverPoints);
+ const points = this.getRiverPoints(riverCells, riverPoints);
let step = h[riverCells[0]] < 20 ? 1 : 10;
for (let i = 0; i <= lastStep; i++, step++) {
@@ -360,20 +402,20 @@ window.Rivers = (function () {
}
}
- return meandered;
+ return meandered as [number, number, number][];
};
- const getRiverPoints = (riverCells, riverPoints) => {
+ getRiverPoints(riverCells: number[], riverPoints: [number, number][] | null) {
if (riverPoints) return riverPoints;
const {p} = pack.cells;
return riverCells.map((cell, i) => {
- if (cell === -1) return getBorderPoint(riverCells[i - 1]);
+ if (cell === -1) return this.getBorderPoint(riverCells[i - 1]);
return p[cell];
});
};
- const getBorderPoint = i => {
+ getBorderPoint(i: number) {
const [x, y] = pack.cells.p[i];
const min = Math.min(y, graphHeight - y, x, graphWidth - x);
if (min === y) return [x, 0];
@@ -382,27 +424,23 @@ window.Rivers = (function () {
return [graphWidth, y];
};
- const FLUX_FACTOR = 500;
- const MAX_FLUX_WIDTH = 1;
- const LENGTH_FACTOR = 200;
- const LENGTH_STEP_WIDTH = 1 / LENGTH_FACTOR;
- const LENGTH_PROGRESSION = [1, 1, 2, 3, 5, 8, 13, 21, 34].map(n => n / LENGTH_FACTOR);
-
- const getOffset = ({flux, pointIndex, widthFactor, startingWidth}) => {
+ getOffset({flux, pointIndex, widthFactor, startingWidth}: {flux: number, pointIndex: number, widthFactor: number, startingWidth: number}) {
if (pointIndex === 0) return startingWidth;
- const fluxWidth = Math.min(flux ** 0.7 / FLUX_FACTOR, MAX_FLUX_WIDTH);
- const lengthWidth = pointIndex * LENGTH_STEP_WIDTH + (LENGTH_PROGRESSION[pointIndex] || LENGTH_PROGRESSION.at(-1));
+ const fluxWidth = Math.min(flux ** 0.7 / this.FLUX_FACTOR, this.MAX_FLUX_WIDTH);
+ const lengthWidth = pointIndex * this.LENGTH_STEP_WIDTH + (this.LENGTH_PROGRESSION[pointIndex] || this.LENGTH_PROGRESSION.at(-1) as number);
return widthFactor * (lengthWidth + fluxWidth) + startingWidth;
};
- const getSourceWidth = flux => rn(Math.min(flux ** 0.9 / FLUX_FACTOR, MAX_FLUX_WIDTH), 2);
+ getSourceWidth(flux: number) {
+ return rn(Math.min(flux ** 0.9 / this.FLUX_FACTOR, this.MAX_FLUX_WIDTH), 2);
+ }
// build polygon from a list of points and calculated offset (width)
- const getRiverPath = (points, widthFactor, startingWidth) => {
- lineGen.curve(d3.curveCatmullRom.alpha(0.1));
- const riverPointsLeft = [];
- const riverPointsRight = [];
+ getRiverPath(points: [number, number, number][], widthFactor: number, startingWidth: number) {
+ this.lineGen.curve(curveCatmullRom.alpha(0.1));
+ const riverPointsLeft: [number, number][] = [];
+ const riverPointsRight: [number, number][] = [];
let flux = 0;
for (let pointIndex = 0; pointIndex < points.length; pointIndex++) {
@@ -411,7 +449,7 @@ window.Rivers = (function () {
const [x2, y2] = points[pointIndex + 1] || points[pointIndex];
if (pointFlux > flux) flux = pointFlux;
- const offset = getOffset({flux, pointIndex, widthFactor, startingWidth});
+ const offset = this.getOffset({flux, pointIndex, widthFactor, startingWidth});
const angle = Math.atan2(y0 - y2, x0 - x2);
const sinOffset = Math.sin(angle) * offset;
const cosOffset = Math.cos(angle) * offset;
@@ -420,63 +458,52 @@ window.Rivers = (function () {
riverPointsRight.push([x1 + sinOffset, y1 - cosOffset]);
}
- const right = lineGen(riverPointsRight.reverse());
- let left = lineGen(riverPointsLeft);
+ const right = this.lineGen(riverPointsRight.reverse());
+ let left = this.lineGen(riverPointsLeft) || "";
left = left.substring(left.indexOf("C"));
return round(right + left, 1);
};
- const specify = function () {
+ specify() {
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);
+ river.basin = this.getBasin(river.i);
+ river.name = this.getName(river.mouth);
+ river.type = this.getType(river);
}
};
- const getName = function (cell) {
+ getName(cell: number) {
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) {
+ getType({i, length, parent}: River) {
+ if (this.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];
+ this.smallLength = pack.rivers.map(r => r.length || 0).sort((a: number, b: number) => a - b)[threshold];
}
- const isSmall = length < smallLength;
+ const isSmall: boolean = length < (this.smallLength as number);
const isFork = each(3)(i) && parent && parent !== i;
- return rw(riverTypes[isFork ? "fork" : "main"][isSmall ? "small" : "big"]);
+ return rw(this.riverTypes[isFork ? "fork" : "main"][isSmall ? "small" : "big"]);
};
- const getApproximateLength = points => {
+ getApproximateLength(points: [number, number, number][]) {
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
+ getWidth(offset: number) {
+ return rn((offset / 1.5) ** 1.8, 2); // mouth width in km
+ };
// remove river and all its tributaries
- const remove = function (id) {
+ remove(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());
@@ -489,32 +516,15 @@ window.Rivers = (function () {
pack.rivers = pack.rivers.filter(r => !riversToRemove.includes(r.i));
};
- const getBasin = function (r) {
+ getBasin(r: number): number {
const parent = pack.rivers.find(river => river.i === r)?.parent;
if (!parent || r === parent) return r;
- return getBasin(parent);
+ return this.getBasin(parent);
};
- const getNextId = function (rivers) {
+ getNextId(rivers: {i: number}[]) {
return rivers.length ? Math.max(...rivers.map(r => r.i)) + 1 : 1;
};
+}
- return {
- generate,
- alterHeights,
- resolveDepressions,
- addMeandering,
- getRiverPath,
- specify,
- getName,
- getType,
- getBasin,
- getWidth,
- getOffset,
- getSourceWidth,
- getApproximateLength,
- getRiverPoints,
- remove,
- getNextId
- };
-})();
+window.Rivers = new RiverModule()
\ No newline at end of file
diff --git a/src/types/PackedGraph.ts b/src/types/PackedGraph.ts
new file mode 100644
index 00000000..23f464df
--- /dev/null
+++ b/src/types/PackedGraph.ts
@@ -0,0 +1,36 @@
+import type { PackedGraphFeature } from "../modules/features";
+import type { River } from "../modules/river-generator";
+
+
+type TypedArray = Uint8Array | Uint16Array | Uint32Array | Int8Array | Int16Array | Float32Array | Float64Array;
+
+export interface PackedGraph {
+ cells: {
+ i: number[]; // cell indices
+ c: number[][]; // neighboring cells
+ v: number[][]; // neighboring vertices
+ p: [number, number][]; // cell polygon points
+ b: boolean[]; // cell is on border
+ h: TypedArray; // cell heights
+ t: TypedArray; // cell terrain types
+ r: Uint16Array; // river id passing through cell
+ f: Uint16Array; // feature id occupying cell
+ fl: TypedArray; // flux presence in cell
+ conf: TypedArray; // cell water confidence
+ haven: TypedArray; // cell is a haven
+ g: number[]; // cell ground type
+ culture: number[]; // cell culture id
+ biome: TypedArray; // cell biome id
+ harbor: TypedArray; // cell harbour presence
+ };
+ vertices: {
+ i: number[]; // vertex indices
+ c: [number, number, number][]; // neighboring cells
+ v: number[][]; // neighboring vertices
+ x: number[]; // x coordinates
+ y: number[]; // y coordinates
+ p: [number, number][]; // vertex points
+ };
+ rivers: River[];
+ features: PackedGraphFeature[];
+}
\ No newline at end of file
diff --git a/src/types/global.ts b/src/types/global.ts
new file mode 100644
index 00000000..1f37d64e
--- /dev/null
+++ b/src/types/global.ts
@@ -0,0 +1,33 @@
+import type { Selection } from 'd3';
+import { PackedGraph } from "./PackedGraph";
+
+declare global {
+ var seed: string;
+ var pack: PackedGraph;
+ var grid: any;
+ var graphHeight: number;
+ var graphWidth: number;
+
+ var TIME: boolean;
+ var WARN: boolean;
+ var ERROR: boolean;
+
+ var heightmapTemplates: any;
+ var Names: any;
+
+ var pointsInput: HTMLInputElement;
+ var heightExponentInput: HTMLInputElement;
+
+ var rivers: Selection;
+ var oceanLayers: Selection;
+ var biomesData: {
+ i: number[];
+ name: string[];
+ color: string[];
+ biomesMatrix: Uint8Array[];
+ habitability: number[];
+ iconsDensity: number[];
+ icons: string[][];
+ cost: number[];
+ };
+}
\ No newline at end of file
diff --git a/src/utils/commonUtils.ts b/src/utils/commonUtils.ts
index d5f2fc9a..24f7501c 100644
--- a/src/utils/commonUtils.ts
+++ b/src/utils/commonUtils.ts
@@ -11,7 +11,7 @@ import { last } from "./arrayUtils";
* @param secure - Secure clipping to avoid edge artifacts
* @returns Clipped polygon points
*/
-export const clipPoly = (points: [number, number][], graphWidth: number, graphHeight: number, secure: number = 0) => {
+export const clipPoly = (points: [number, number][], graphWidth?: number, graphHeight?: number, secure: number = 0) => {
if (points.length < 2) return points;
if (points.some(point => point === undefined)) {
window.ERROR && console.error("Undefined point in clipPoly", points);
diff --git a/tests/e2e/layers.spec.ts-snapshots/rivers.html b/tests/e2e/layers.spec.ts-snapshots/rivers.html
index 087b4d8d..81b2fcf9 100644
--- a/tests/e2e/layers.spec.ts-snapshots/rivers.html
+++ b/tests/e2e/layers.spec.ts-snapshots/rivers.html
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file