diff --git a/index.html b/index.html
index ec71f62b..d85ab08d 100644
--- a/index.html
+++ b/index.html
@@ -7647,7 +7647,7 @@
-
+
diff --git a/src/config/generation.ts b/src/config/generation.ts
new file mode 100644
index 00000000..95ee79f2
--- /dev/null
+++ b/src/config/generation.ts
@@ -0,0 +1,9 @@
+export const MIN_LAND_HEIGHT = 20;
+
+export const MAX_HEIGHT = 100;
+
+export enum DISTANCE_FIELD {
+ LAND_COAST = 1,
+ UNMARKED = 0,
+ WATER_COAST = -1
+}
diff --git a/src/constants/index.ts b/src/constants/index.ts
index 6b18a663..d8f61cd6 100644
--- a/src/constants/index.ts
+++ b/src/constants/index.ts
@@ -2,6 +2,7 @@
export const MOBILE = window.innerWidth < 600 || window.navigator.userAgentData?.mobile;
// typed arrays max values
+export const INT8_MAX = 127;
export const UINT8_MAX = 255;
export const UINT16_MAX = 65535;
export const UINT32_MAX = 4294967295;
diff --git a/src/dialogs/dialogs/heightmap-editor.js b/src/dialogs/dialogs/heightmap-editor.js
index db47b427..6aeffb5d 100644
--- a/src/dialogs/dialogs/heightmap-editor.js
+++ b/src/dialogs/dialogs/heightmap-editor.js
@@ -6,7 +6,7 @@ import {ERROR, INFO, TIME} from "config/logging";
import {closeDialogs} from "dialogs/utils";
import {layerIsOn, turnLayerButtonOff, turnLayerButtonOn, updatePresetInput, renderLayer} from "layers";
import {drawCoastline} from "modules/coastline";
-import {markFeatures, markupGridOcean} from "modules/markup";
+import {markupGridFeatures} from "modules/markup";
import {generatePrecipitation} from "modules/precipitation";
import {calculateTemperatures} from "modules/temperature";
import {moveCircle, removeCircle} from "modules/ui/editors";
@@ -214,11 +214,11 @@ export function open(options) {
TIME && console.time("regenerateErasedData");
const erosionAllowed = allowErosion.checked;
- markFeatures();
- markupGridOcean();
+ markupGridFeatures();
+
if (erosionAllowed) {
- Lakes.addLakesInDeepDepressions();
- Lakes.openNearSeaLakes();
+ Lakes.addLakesInDeepDepressions(grid);
+ Lakes.openNearSeaLakes(grid);
}
OceanLayers();
calculateTemperatures();
@@ -336,8 +336,8 @@ export function open(options) {
zone.selectAll("*").remove();
});
- markFeatures();
- markupGridOcean();
+ markupGridFeatures();
+
if (erosionAllowed) addLakesInDeepDepressions();
OceanLayers();
calculateTemperatures();
diff --git a/src/dialogs/dialogs/heightmap-selection.js b/src/dialogs/dialogs/heightmap-selection.js
index 5bdaf8f3..25717190 100644
--- a/src/dialogs/dialogs/heightmap-selection.js
+++ b/src/dialogs/dialogs/heightmap-selection.js
@@ -2,7 +2,7 @@ import * as d3 from "d3";
import {heightmapTemplates} from "config/heightmap-templates";
import {precreatedHeightmaps} from "config/precreated-heightmaps";
-import {shouldRegenerateGrid, generateGrid} from "utils/graphUtils";
+import {shouldRegenerateGridPoints, generateGrid} from "utils/graphUtils";
import {byId} from "utils/shorthands";
import {generateSeed} from "utils/probabilityUtils";
import {getColorScheme} from "utils/colorUtils";
@@ -274,7 +274,7 @@ function getName(id) {
}
function getGraph(currentGraph) {
- const newGraph = shouldRegenerateGrid(currentGraph) ? generateGrid() : structuredClone(currentGraph);
+ const newGraph = shouldRegenerateGridPoints(currentGraph) ? generateGrid() : structuredClone(currentGraph);
delete newGraph.cells.h;
return newGraph;
}
diff --git a/src/modules/io/load.js b/src/modules/io/load.js
index b8a87337..98c12374 100644
--- a/src/modules/io/load.js
+++ b/src/modules/io/load.js
@@ -151,7 +151,7 @@ export function loadMapFromURL(maplink, random) {
});
}
-function showUploadErrorMessage(error, URL, random) {
+export function showUploadErrorMessage(error, URL, random) {
ERROR && console.error(error);
alertMessage.innerHTML = /* html */ `Cannot load map from the ${link(URL, "link provided")}. ${
random ? `A new random map is generated. ` : ""
@@ -168,7 +168,7 @@ function showUploadErrorMessage(error, URL, random) {
});
}
-function uploadMap(file, callback) {
+export function uploadMap(file, callback) {
uploadMap.timeStart = performance.now();
const OLDEST_SUPPORTED_VERSION = 0.7;
const currentVersion = parseFloat(version);
diff --git a/src/modules/lakes.js b/src/modules/lakes.ts
similarity index 70%
rename from src/modules/lakes.js
rename to src/modules/lakes.ts
index ab8f3fb3..bd7064e3 100644
--- a/src/modules/lakes.js
+++ b/src/modules/lakes.ts
@@ -4,6 +4,8 @@ import {TIME} from "config/logging";
import {rn} from "utils/numberUtils";
import {aleaPRNG} from "scripts/aleaPRNG";
import {byId} from "utils/shorthands";
+import {getInputNumber, getInputValue} from "utils/nodeUtils";
+import {DISTANCE_FIELD, MIN_LAND_HEIGHT} from "config/generation";
window.Lakes = (function () {
const setClimateData = function (h) {
@@ -154,17 +156,21 @@ window.Lakes = (function () {
return "freshwater";
}
- function addLakesInDeepDepressions() {
- TIME && console.time("addLakesInDeepDepressions");
- const {cells, features} = grid;
- const {c, h, b} = cells;
- const ELEVATION_LIMIT = +byId("lakeElevationLimitOutput").value;
+ const {LAND_COAST, WATER_COAST} = DISTANCE_FIELD;
+
+ function addLakesInDeepDepressions(grid: IGraph & Partial) {
+ const ELEVATION_LIMIT = getInputNumber("lakeElevationLimitOutput");
if (ELEVATION_LIMIT === 80) return;
- for (const i of cells.i) {
- if (b[i] || h[i] < 20) continue;
+ TIME && console.time("addLakesInDeepDepressions");
+ const {cells, features} = grid;
+ if (!features) throw new Error("addLakesInDeepDepressions: features are not defined");
+ const {c, h, b} = cells;
- const minHeight = d3.min(c[i].map(c => h[c]));
+ for (const i of cells.i) {
+ if (b[i] || h[i] < MIN_LAND_HEIGHT) continue;
+
+ const minHeight = d3.min(c[i].map(c => h[c])) || 0;
if (h[i] > minHeight) continue;
let deep = true;
@@ -175,12 +181,12 @@ window.Lakes = (function () {
// check if elevated cell can potentially pour to water
while (deep && queue.length) {
- const q = queue.pop();
+ const q = queue.pop()!;
for (const n of c[q]) {
if (checked[n]) continue;
if (h[n] >= threshold) continue;
- if (h[n] < 20) {
+ if (h[n] < MIN_LAND_HEIGHT) {
deep = false;
break;
}
@@ -197,56 +203,68 @@ window.Lakes = (function () {
}
}
- function addLake(lakeCells) {
- const f = features.length;
+ function addLake(lakeCells: number[]) {
+ const featureId = features!.length;
- lakeCells.forEach(i => {
- cells.h[i] = 19;
- cells.t[i] = -1;
- cells.f[i] = f;
- c[i].forEach(n => !lakeCells.includes(n) && (cells.t[c] = 1));
- });
+ for (const lakeCellId of lakeCells) {
+ cells.h[lakeCellId] = MIN_LAND_HEIGHT - 1;
+ cells.t[lakeCellId] = WATER_COAST;
+ cells.f[lakeCellId] = featureId;
- features.push({i: f, land: false, border: false, type: "lake"});
+ for (const neibCellId of c[lakeCellId]) {
+ if (!lakeCells.includes(neibCellId)) cells.t[neibCellId] = LAND_COAST;
+ }
+ }
+
+ features!.push({i: featureId, land: false, border: false, type: "lake"});
}
TIME && console.timeEnd("addLakesInDeepDepressions");
}
// near sea lakes usually get a lot of water inflow, most of them should brake threshold and flow out to sea (see Ancylus Lake)
- function openNearSeaLakes() {
- if (byId("templateInput").value === "Atoll") return; // no need for Atolls
+ function openNearSeaLakes(grid: IGraph & Partial) {
+ if (getInputValue("templateInput") === "Atoll") return; // no need for Atolls
+
+ const {cells, features} = grid;
+ if (!features?.find(f => f && f.type === "lake")) return; // no lakes
- const cells = grid.cells;
- const features = grid.features;
- if (!features.find(f => f.type === "lake")) return; // no lakes
TIME && console.time("openLakes");
const LIMIT = 22; // max height that can be breached by water
- for (const i of cells.i) {
- const lake = cells.f[i];
- if (features[lake].type !== "lake") continue; // not a lake cell
+ const isLake = (featureId: number) => featureId && (features[featureId] as IGridFeature).type === "lake";
+ const isOcean = (featureId: number) => featureId && (features[featureId] as IGridFeature).type === "ocean";
- check_neighbours: for (const c of cells.c[i]) {
- if (cells.t[c] !== 1 || cells.h[c] > LIMIT) continue; // water cannot brake this
+ for (const cellId of cells.i) {
+ const featureId = cells.f[cellId];
+ if (!isLake(featureId)) continue; // not a lake cell
- for (const n of cells.c[c]) {
- const ocean = cells.f[n];
- if (features[ocean].type !== "ocean") continue; // not an ocean
- removeLake(c, lake, ocean);
+ check_neighbours: for (const neibCellId of cells.c[cellId]) {
+ // water cannot brake the barrier
+ if (cells.t[neibCellId] !== WATER_COAST || cells.h[neibCellId] > LIMIT) continue;
+
+ for (const neibOfNeibCellId of cells.c[neibCellId]) {
+ const neibOfNeibFeatureId = cells.f[neibOfNeibCellId];
+ if (!isOcean(neibOfNeibFeatureId)) continue; // not an ocean
+ removeLake(neibCellId, featureId, neibOfNeibFeatureId);
break check_neighbours;
}
}
}
- function removeLake(threshold, lake, ocean) {
- cells.h[threshold] = 19;
- cells.t[threshold] = -1;
- cells.f[threshold] = ocean;
- cells.c[threshold].forEach(function (c) {
- if (cells.h[c] >= 20) cells.t[c] = 1; // mark as coastline
- });
- features[lake].type = "ocean"; // mark former lake as ocean
+ function removeLake(barrierCellId: number, lakeFeatureId: number, oceanFeatureId: number) {
+ cells.h[barrierCellId] = MIN_LAND_HEIGHT - 1;
+ cells.t[barrierCellId] = WATER_COAST;
+ cells.f[barrierCellId] = oceanFeatureId;
+
+ for (const neibCellId of cells.c[barrierCellId]) {
+ if (cells.h[neibCellId] >= MIN_LAND_HEIGHT) cells.t[neibCellId] = LAND_COAST;
+ }
+
+ if (features && lakeFeatureId) {
+ // mark former lake as ocean
+ (features[lakeFeatureId] as IGridFeature).type = "ocean";
+ }
}
TIME && console.timeEnd("openLakes");
diff --git a/src/modules/markup.js b/src/modules/markup.js
deleted file mode 100644
index 994c0ea0..00000000
--- a/src/modules/markup.js
+++ /dev/null
@@ -1,145 +0,0 @@
-import {TIME} from "config/logging";
-import {aleaPRNG} from "scripts/aleaPRNG";
-
-// Mark features (ocean, lakes, islands) and calculate distance field
-export function markFeatures() {
- TIME && console.time("markFeatures");
- Math.random = aleaPRNG(seed); // get the same result on heightmap edit in Erase mode
-
- const cells = grid.cells;
- const heights = grid.cells.h;
-
- cells.f = new Uint16Array(cells.i.length); // cell feature number
- cells.t = new Int8Array(cells.i.length); // cell type: 1 = land coast; -1 = water near coast
-
- grid.features = [0];
-
- for (let i = 1, queue = [0]; queue[0] !== -1; i++) {
- cells.f[queue[0]] = i; // feature number
- const land = heights[queue[0]] >= 20;
- let border = false; // true if feature touches map border
-
- while (queue.length) {
- const q = queue.pop();
- if (cells.b[q]) border = true;
-
- cells.c[q].forEach(c => {
- const cLand = heights[c] >= 20;
- if (land === cLand && !cells.f[c]) {
- cells.f[c] = i;
- queue.push(c);
- } else if (land && !cLand) {
- cells.t[q] = 1;
- cells.t[c] = -1;
- }
- });
- }
- const type = land ? "island" : border ? "ocean" : "lake";
- grid.features.push({i, land, border, type});
-
- queue[0] = cells.f.findIndex(f => !f); // find unmarked cell
- }
-
- TIME && console.timeEnd("markFeatures");
-}
-
-export function markupGridOcean() {
- TIME && console.time("markupGridOcean");
- markup(grid.cells, -2, -1, -10);
- TIME && console.timeEnd("markupGridOcean");
-}
-
-// Calculate cell-distance to coast for every cell
-export function markup(cells, start, increment, limit) {
- for (let t = start, count = Infinity; count > 0 && t > limit; t += increment) {
- count = 0;
- const prevT = t - increment;
- for (let i = 0; i < cells.i.length; i++) {
- if (cells.t[i] !== prevT) continue;
-
- for (const c of cells.c[i]) {
- if (cells.t[c]) continue;
- cells.t[c] = t;
- count++;
- }
- }
- }
-}
-
-// Re-mark features (ocean, lakes, islands)
-export function reMarkFeatures() {
- TIME && console.time("reMarkFeatures");
- const {cells} = pack;
- const features = [0];
-
- cells.f = new Uint16Array(cells.i.length); // cell feature number
- cells.t = new Int8Array(cells.i.length); // cell type: 1 = land along coast; -1 = water along coast;
- cells.haven = cells.i.length < 65535 ? new Uint16Array(cells.i.length) : new Uint32Array(cells.i.length); // cell haven (opposite water cell);
- cells.harbor = new Uint8Array(cells.i.length); // cell harbor (number of adjacent water cells);
-
- const defineHaven = i => {
- const water = cells.c[i].filter(c => cells.h[c] < 20);
- const dist2 = water.map(c => (cells.p[i][0] - cells.p[c][0]) ** 2 + (cells.p[i][1] - cells.p[c][1]) ** 2);
- const closest = water[dist2.indexOf(Math.min.apply(Math, dist2))];
-
- cells.haven[i] = closest;
- cells.harbor[i] = water.length;
- };
-
- if (!cells.i.length) return; // no cells -> there is nothing to do
- for (let i = 1, queue = [0]; queue[0] !== -1; i++) {
- const start = queue[0]; // first cell
- cells.f[start] = i; // assign feature number
- const land = cells.h[start] >= 20;
- let border = false; // true if feature touches map border
- let cellNumber = 1; // to count cells number in a feature
-
- while (queue.length) {
- const q = queue.pop();
- if (cells.b[q]) border = true;
- cells.c[q].forEach(function (e) {
- const eLand = cells.h[e] >= 20;
- if (land && !eLand) {
- cells.t[q] = 1;
- cells.t[e] = -1;
- if (!cells.haven[q]) defineHaven(q);
- } else if (land && eLand) {
- if (!cells.t[e] && cells.t[q] === 1) cells.t[e] = 2;
- else if (!cells.t[q] && cells.t[e] === 1) cells.t[q] = 2;
- }
- if (!cells.f[e] && land === eLand) {
- queue.push(e);
- cells.f[e] = i;
- cellNumber++;
- }
- });
- }
-
- const type = land ? "island" : border ? "ocean" : "lake";
- let group;
- if (type === "ocean") group = defineOceanGroup(cellNumber);
- else if (type === "island") group = defineIslandGroup(start, cellNumber);
- features.push({i, land, border, type, cells: cellNumber, firstCell: start, group});
- queue[0] = cells.f.findIndex(f => !f); // find unmarked cell
- }
-
- // markupPackLand
- markup(pack.cells, 3, 1, 0);
-
- function defineOceanGroup(number) {
- if (number > grid.cells.i.length / 25) return "ocean";
- if (number > grid.cells.i.length / 100) return "sea";
- return "gulf";
- }
-
- function defineIslandGroup(cell, number) {
- if (cell && features[cells.f[cell - 1]].type === "lake") return "lake_island";
- if (number > grid.cells.i.length / 10) return "continent";
- if (number > grid.cells.i.length / 1000) return "island";
- return "isle";
- }
-
- pack.features = features;
-
- TIME && console.timeEnd("reMarkFeatures");
-}
diff --git a/src/modules/markup.ts b/src/modules/markup.ts
new file mode 100644
index 00000000..57af2bcd
--- /dev/null
+++ b/src/modules/markup.ts
@@ -0,0 +1,186 @@
+import {MIN_LAND_HEIGHT, DISTANCE_FIELD} from "config/generation";
+import {TIME} from "config/logging";
+import {INT8_MAX} from "constants";
+// @ts-expect-error js module
+import {aleaPRNG} from "scripts/aleaPRNG";
+
+const {UNMARKED, LAND_COAST, WATER_COAST} = DISTANCE_FIELD;
+
+// define features (grid.features: ocean, lakes, islands) and calculate distance field (cells.t)
+export function markupGridFeatures(grid: IGraph & {cells: {h: UintArray}}) {
+ TIME && console.time("markupGridFeatures");
+ Math.random = aleaPRNG(seed); // get the same result on heightmap edit in Erase mode
+
+ if (!grid.cells || !grid.cells.h) {
+ throw new Error("markupGridFeatures: grid.cells.h is required");
+ }
+
+ const cells = grid.cells;
+ const heights = cells.h;
+ const n = cells.i.length;
+
+ const featureIds = new Uint16Array(n); // starts from 1
+ let distanceField = new Int8Array(n);
+ const features: TGridFeatures = [0];
+
+ const queue = [0];
+ for (let featureId = 1; queue[0] !== -1; featureId++) {
+ const firstCell = queue[0];
+ featureIds[firstCell] = featureId;
+
+ const land = heights[firstCell] >= MIN_LAND_HEIGHT;
+ let border = false; // set true if feature touches map edge
+
+ while (queue.length) {
+ const cellId = queue.pop()!;
+ if (cells.b[cellId]) border = true;
+
+ for (const neighborId of cells.c[cellId]) {
+ const isNeibLand = heights[neighborId] >= MIN_LAND_HEIGHT;
+
+ 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
+ distanceField = markup({graph: grid, distanceField, start: -2, increment: -1, limit: -10});
+
+ TIME && console.timeEnd("markupGridFeatures");
+ return {featureIds, distanceField, features};
+}
+
+// calculate distance to coast for every cell
+function markup({
+ graph,
+ distanceField,
+ start,
+ increment,
+ limit
+}: {
+ graph: IGraph;
+ distanceField: Int8Array;
+ start: number;
+ increment: number;
+ limit: number;
+}) {
+ const cellsLength = graph.cells.i.length;
+ const neighbors = graph.cells.c;
+
+ for (let distance = start, marked = Infinity; marked > 0 && distance > limit; distance += increment) {
+ marked = 0;
+ const prevDistance = distance - increment;
+ for (let cellId = 0; cellId < cellsLength; cellId++) {
+ if (distanceField[cellId] !== prevDistance) continue;
+
+ for (const neighborId of neighbors[cellId]) {
+ if (distanceField[neighborId] !== UNMARKED) continue;
+ distanceField[neighborId] = distance;
+ marked++;
+ }
+ }
+ }
+
+ return distanceField;
+}
+
+// Re-mark features (ocean, lakes, islands)
+export function reMarkFeatures() {
+ TIME && console.time("reMarkFeatures");
+ const {cells} = pack;
+ const features: TPackFeatures = [0];
+ const n = cells.i.length;
+
+ cells.f = new Uint16Array(n); // cell feature number
+ cells.t = new Int8Array(n); // cell type: 1 = land along coast; -1 = water along coast;
+ cells.haven = n < 65535 ? new Uint16Array(n) : new Uint32Array(n); // cell haven (opposite water cell);
+ cells.harbor = new Uint8Array(n); // cell harbor (number of adjacent water cells);
+
+ const defineHaven = (i: number) => {
+ const water = cells.c[i].filter(c => cells.h[c] < 20);
+ const dist2 = water.map(c => (cells.p[i][0] - cells.p[c][0]) ** 2 + (cells.p[i][1] - cells.p[c][1]) ** 2);
+ const closest = water[dist2.indexOf(Math.min.apply(Math, dist2))];
+
+ cells.haven[i] = closest;
+ cells.harbor[i] = water.length;
+ };
+
+ for (let i = 1, queue = [0]; queue[0] !== -1; i++) {
+ const start = queue[0]; // first cell
+ cells.f[start] = i; // assign feature number
+ const land = cells.h[start] >= 20;
+ let border = false; // true if feature touches map border
+ let cellNumber = 1; // to count cells number in a feature
+
+ while (queue.length) {
+ const firstCellId = queue.pop()!;
+
+ if (cells.b[firstCellId]) border = true;
+ cells.c[firstCellId].forEach(function (e) {
+ const eLand = cells.h[e] >= 20;
+ if (land && !eLand) {
+ cells.t[firstCellId] = 1;
+ cells.t[e] = -1;
+ if (!cells.haven[firstCellId]) defineHaven(firstCellId);
+ } else if (land && eLand) {
+ if (!cells.t[e] && cells.t[firstCellId] === 1) cells.t[e] = 2;
+ else if (!cells.t[firstCellId] && cells.t[e] === 1) cells.t[firstCellId] = 2;
+ }
+ if (!cells.f[e] && land === eLand) {
+ queue.push(e);
+ cells.f[e] = i;
+ cellNumber++;
+ }
+ });
+ }
+
+ if (land) {
+ const group = defineIslandGroup(start, cellNumber);
+ const feature: IPackFeatureIsland = {i, type: "island", group, land, border, cells: cellNumber, firstCell: start};
+ features.push(feature);
+ } else if (border) {
+ const group = defineOceanGroup(cellNumber);
+ const feature: IPackFeatureOcean = {i, type: "ocean", group, land, border, cells: cellNumber, firstCell: start};
+ features.push(feature);
+ } else {
+ const group = "freshwater"; // temp, to be defined later
+ const name = ""; // temp, to be defined later
+ const cells = cellNumber;
+ const feature: IPackFeatureLake = {i, type: "lake", group, name, land, border, cells, firstCell: start};
+ features.push(feature);
+ }
+
+ queue[0] = cells.f.findIndex(f => f === UNMARKED); // find unmarked cell
+ }
+
+ // markupPackLand
+ markup({graph: pack, distanceField: pack.cells.t, start: 3, increment: 1, limit: INT8_MAX});
+
+ function defineOceanGroup(number: number) {
+ if (number > grid.cells.i.length / 25) return "ocean";
+ if (number > grid.cells.i.length / 100) return "sea";
+ return "gulf";
+ }
+
+ function defineIslandGroup(cell, number) {
+ if (cell && features[cells.f[cell - 1]].type === "lake") return "lake_island";
+ if (number > grid.cells.i.length / 10) return "continent";
+ if (number > grid.cells.i.length / 1000) return "island";
+ return "isle";
+ }
+
+ pack.features = features;
+
+ TIME && console.timeEnd("reMarkFeatures");
+}
diff --git a/src/modules/submap.js b/src/modules/submap.js
index 88ef0384..c258e89d 100644
--- a/src/modules/submap.js
+++ b/src/modules/submap.js
@@ -6,6 +6,7 @@ import {getMiddlePoint} from "utils/lineUtils";
import {rn} from "utils/numberUtils";
import {aleaPRNG} from "scripts/aleaPRNG";
import {renderLayer} from "layers";
+import {markupGridFeatures} from "modules/markup";
window.Submap = (function () {
const isWater = (pack, id) => pack.cells.h[id] < 20;
@@ -113,13 +114,12 @@ window.Submap = (function () {
}
stage("Detect features, ocean and generating lakes.");
- markFeatures();
- markupGridOcean();
+ markupGridFeatures();
// Warning: addLakesInDeepDepressions can be very slow!
if (options.addLakesInDepressions) {
- Lakes.addLakesInDeepDepressions();
- Lakes.openNearSeaLakes();
+ Lakes.addLakesInDeepDepressions(grid);
+ Lakes.openNearSeaLakes(grid);
}
OceanLayers();
diff --git a/src/scripts/generation.js b/src/scripts/generation.ts
similarity index 85%
rename from src/scripts/generation.js
rename to src/scripts/generation.ts
index fcd8727d..007be01e 100644
--- a/src/scripts/generation.js
+++ b/src/scripts/generation.ts
@@ -5,7 +5,7 @@ import {closeDialogs} from "dialogs/utils";
import {initLayers, renderLayer, restoreLayers} from "layers";
import {drawCoastline} from "modules/coastline";
import {calculateMapCoordinates, defineMapSize} from "modules/coordinates";
-import {markFeatures, markupGridOcean} from "modules/markup";
+import {markupGridFeatures, markupGridOcean} from "modules/markup";
import {drawScaleBar, Rulers} from "modules/measurers";
import {generatePrecipitation} from "modules/precipitation";
import {calculateTemperatures} from "modules/temperature";
@@ -18,7 +18,7 @@ import {hideLoading, showLoading} from "scripts/loading";
import {clearMainTip, tip} from "scripts/tooltips";
import {parseError} from "utils/errorUtils";
import {debounce} from "utils/functionUtils";
-import {generateGrid, shouldRegenerateGrid} from "utils/graphUtils";
+import {generateGrid, shouldRegenerateGridPoints} from "utils/graphUtils";
import {rn} from "utils/numberUtils";
import {generateSeed} from "utils/probabilityUtils";
import {byId} from "utils/shorthands";
@@ -26,33 +26,26 @@ import {rankCells} from "./rankCells";
import {reGraph} from "./reGraph";
import {showStatistics} from "./statistics";
-export async function generate(options) {
+const {Zoom, Lakes, HeightmapGenerator, OceanLayers} = window;
+
+interface IGenerationOptions {
+ seed: string;
+ graph: IGrid;
+}
+
+export async function generate(options?: IGenerationOptions) {
try {
const timeStart = performance.now();
const {seed: precreatedSeed, graph: precreatedGraph} = options || {};
- Zoom.invoke();
+ Zoom?.invoke();
setSeed(precreatedSeed);
INFO && console.group("Generated Map " + seed);
applyMapSize();
randomizeOptions();
- if (shouldRegenerateGrid(grid)) grid = precreatedGraph || generateGrid();
- else delete grid.cells.h;
- grid.cells.h = await HeightmapGenerator.generate(grid);
-
- markFeatures();
- markupGridOcean();
-
- Lakes.addLakesInDeepDepressions();
- Lakes.openNearSeaLakes();
-
- OceanLayers();
- defineMapSize();
- window.mapCoordinates = calculateMapCoordinates();
- calculateTemperatures();
- generatePrecipitation();
+ const updatedGrid = await updateGrid(precreatedGraph);
reGraph();
drawCoastline();
@@ -117,6 +110,37 @@ export async function generate(options) {
}
}
+async function updateGrid(precreatedGraph?: IGrid) {
+ const globalGrid = grid;
+
+ const updatedGrid: IGraph & Partial = shouldRegenerateGridPoints(globalGrid)
+ ? (precreatedGraph && undressGrid(precreatedGraph)) || generateGrid()
+ : undressGrid(globalGrid);
+
+ const heights = await HeightmapGenerator.generate(updatedGrid);
+ updatedGrid.cells.h = heights;
+
+ const {featureIds, distanceField, features} = markupGridFeatures(updatedGrid);
+ updatedGrid.cells.f = featureIds;
+ updatedGrid.cells.t = distanceField;
+ updatedGrid.features = features;
+
+ Lakes.addLakesInDeepDepressions(updatedGrid);
+ Lakes.openNearSeaLakes(updatedGrid);
+
+ OceanLayers();
+ defineMapSize();
+ window.mapCoordinates = calculateMapCoordinates();
+ calculateTemperatures();
+ generatePrecipitation();
+}
+
+function undressGrid(extendedGrid: IGrid) {
+ const {spacing, cellsDesired, boundary, points, cellsX, cellsY, cells, vertices} = extendedGrid;
+ const {i, b, c, v} = cells;
+ return {spacing, cellsDesired, boundary, points, cellsX, cellsY, cells: {i, b, c, v}, vertices};
+}
+
export async function generateMapOnLoad() {
await applyStyleOnLoad(); // apply previously selected default or custom style
await generate(); // generate map
@@ -127,8 +151,7 @@ export async function generateMapOnLoad() {
// clear the map
export function undraw() {
viewbox.selectAll("path, circle, polygon, line, text, use, #zones > g, #armies > g, #ruler > g").remove();
- document
- .getElementById("deftemp")
+ byId("deftemp")
.querySelectorAll("path, clipPath, svg")
.forEach(el => el.remove());
byId("coas").innerHTML = ""; // remove auto-generated emblems
diff --git a/src/scripts/loading.js b/src/scripts/loading.ts
similarity index 91%
rename from src/scripts/loading.js
rename to src/scripts/loading.ts
index 42353b23..1601bdff 100644
--- a/src/scripts/loading.js
+++ b/src/scripts/loading.ts
@@ -5,7 +5,8 @@ import {loadMapFromURL} from "modules/io/load";
import {setDefaultEventHandlers} from "scripts/events";
import {ldb} from "scripts/indexedDB";
import {getInputValue} from "utils/nodeUtils";
-import {generateMapOnLoad} from "./generation";
+import {generateMapOnLoad} from "./generation.ts";
+import {showUploadErrorMessage, uploadMap} from "modules/io/load";
export function addOnLoadListener() {
document.on("DOMContentLoaded", async () => {
@@ -73,9 +74,9 @@ function loadLastMap() {
if (blob) {
WARN && console.warn("Load last saved map");
try {
- uploadMap(blob);
- resolve();
+ uploadMap(blob, resolve);
} catch (error) {
+ ERROR && console.error("Cannot load last saved map", error);
reject(error);
}
} else {
diff --git a/src/scripts/rankCells.ts b/src/scripts/rankCells.ts
index 8b581870..b2ee91fa 100644
--- a/src/scripts/rankCells.ts
+++ b/src/scripts/rankCells.ts
@@ -1,7 +1,7 @@
import * as d3 from "d3";
import {TIME} from "config/logging";
-import {normalize, rn} from "utils/numberUtils";
+import {normalize} from "utils/numberUtils";
import {isWater, isCoastal} from "utils/graphUtils";
const FLUX_MAX_BONUS = 250;
diff --git a/src/types/globals.d.ts b/src/types/globals.d.ts
index 30d5093c..f9888c46 100644
--- a/src/types/globals.d.ts
+++ b/src/types/globals.d.ts
@@ -1,5 +1,5 @@
-declare const grid: IGrid;
-declare const pack: IPack;
+declare let grid: IGrid;
+declare let pack: IPack;
declare let seed: string;
declare let mapId: number;
diff --git a/src/types/graph.d.ts b/src/types/graph.d.ts
new file mode 100644
index 00000000..251f5b05
--- /dev/null
+++ b/src/types/graph.d.ts
@@ -0,0 +1,18 @@
+// generic part of any graph, simplest verstion of IGrid and IGraph
+interface IGraph {
+ vertices: IGraphVertices;
+ cells: IGraphCells;
+}
+
+interface IGraphVertices {
+ p: TPoints;
+ v: number[][];
+ c: number[][];
+}
+
+interface IGraphCells {
+ i: UintArray;
+ b: UintArray;
+ c: number[][];
+ v: number[][];
+}
diff --git a/src/types/grid.d.ts b/src/types/grid.d.ts
index 82fd30f5..6dcf39fd 100644
--- a/src/types/grid.d.ts
+++ b/src/types/grid.d.ts
@@ -1,28 +1,27 @@
-interface IGrid {
+interface IGrid extends IGraph {
+ cellsDesired: number;
+ cellsX: number;
+ cellsY: number;
spacing: number;
boundary: TPoints;
points: TPoints;
- vertices: {
- p: TPoints;
- v: number[][];
- c: number[][];
- };
- cells: {
- i: UintArray;
- b: UintArray;
- c: number[][];
- v: number[][];
- h: UintArray;
- t: UintArray;
- f: UintArray;
- temp: UintArray;
- prec: UintArray;
- };
- features: IGridFeature[];
+ cells: IGridCells;
+ features: TGridFeatures;
}
+
+interface IGridCells extends IGraphCells {
+ h: UintArray; // heights, [0, 100], see MIN_LAND_HEIGHT constant
+ t: Int8Array; // see DISTANCE_FIELD enum
+ f: Uint16Array; // feature id, see IGridFeature
+ temp: UintArray; // temparature in Celsius
+ prec: UintArray; // precipitation in inner units
+}
+
+type TGridFeatures = [0, ...IGridFeature[]];
+
interface IGridFeature {
- i: number;
+ i: number; // starts from 1, not 0
land: boolean;
- border: boolean;
+ border: boolean; // if touches map edge
type: "ocean" | "lake" | "island";
}
diff --git a/src/types/overrides.d.ts b/src/types/overrides.d.ts
index 2559b1f4..c7458943 100644
--- a/src/types/overrides.d.ts
+++ b/src/types/overrides.d.ts
@@ -6,14 +6,16 @@ interface Navigator {
interface Window {
mapCoordinates: IMapCoordinates;
- // untyped IIFE modules
$: typeof $;
- d3: typeof d3;
+ // untyped IIFE modules
Biomes: typeof Biomes;
Names: typeof Names;
ThreeD: typeof ThreeD;
ReliefIcons: typeof ReliefIcons;
Zoom: typeof Zoom;
+ Lakes: typeof Lakes;
+ HeightmapGenerator: typeof HeightmapGenerator;
+ OceanLayers: typeof OceanLayers;
}
interface Node {
diff --git a/src/types/pack.d.ts b/src/types/pack.d.ts
index 4470f3b7..b3728fd9 100644
--- a/src/types/pack.d.ts
+++ b/src/types/pack.d.ts
@@ -1,35 +1,6 @@
-interface IPack {
- vertices: {
- p: TPoints;
- v: number[][];
- c: number[][];
- };
- features: TPackFeature[];
- cells: {
- i: UintArray;
- p: TPoints;
- v: number[][];
- c: number[][];
- g: UintArray;
- h: UintArray;
- t: UintArray;
- f: UintArray;
- s: IntArray;
- pop: Float32Array;
- fl: UintArray;
- conf: UintArray;
- r: UintArray;
- biome: UintArray;
- area: UintArray;
- state: UintArray;
- culture: UintArray;
- religion: UintArray;
- province: UintArray;
- burg: UintArray;
- haven: UintArray;
- harbor: UintArray;
- q: d3.Quadtree;
- };
+interface IPack extends IGraph {
+ cells: IPackCells;
+ features: TPackFeatures;
states: IState[];
cultures: ICulture[];
provinces: IProvince[];
@@ -38,18 +9,41 @@ interface IPack {
religions: IReligion[];
}
+interface IPackCells extends IGraphCells {
+ p: TPoints; // cell center points
+ h: UintArray; // heights, [0, 100], see MIN_LAND_HEIGHT constant
+ t: Int8Array; // see DISTANCE_FIELD enum
+ f: Uint16Array; // feature id, see TPackFeature
+ g: UintArray;
+ s: IntArray;
+ pop: Float32Array;
+ fl: UintArray;
+ conf: UintArray;
+ r: UintArray;
+ biome: UintArray;
+ area: UintArray;
+ state: UintArray;
+ culture: UintArray;
+ religion: UintArray;
+ province: UintArray;
+ burg: UintArray;
+ haven: UintArray;
+ harbor: UintArray;
+ q: d3.Quadtree;
+}
+
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
+ vertices?: number[]; // indexes of perimetric vertices
}
interface IPackFeatureOcean extends IPackFeatureBase {
land: false;
type: "ocean";
- group: "ocean";
+ group: "ocean" | "sea" | "gulf";
}
interface IPackFeatureIsland extends IPackFeatureBase {
@@ -67,6 +61,8 @@ interface IPackFeatureLake extends IPackFeatureBase {
type TPackFeature = IPackFeatureOcean | IPackFeatureIsland | IPackFeatureLake;
+type TPackFeatures = [0, ...TPackFeature[]];
+
interface IState {
i: number;
name: string;
diff --git a/src/utils/graphUtils.ts b/src/utils/graphUtils.ts
index 2cf36b05..c485ffbf 100644
--- a/src/utils/graphUtils.ts
+++ b/src/utils/graphUtils.ts
@@ -2,15 +2,16 @@
import * as d3 from "d3";
import Delaunator from "delaunator";
+import {aleaPRNG} from "scripts/aleaPRNG";
import {TIME} from "../config/logging";
import {createTypedArray} from "./arrayUtils";
import {rn} from "./numberUtils";
import {byId} from "./shorthands";
import {Voronoi} from "/src/modules/voronoi";
-import {aleaPRNG} from "scripts/aleaPRNG";
+import {MIN_LAND_HEIGHT, DISTANCE_FIELD} from "config/generation";
// check if new grid graph should be generated or we can use the existing one
-export function shouldRegenerateGrid(grid) {
+export function shouldRegenerateGridPoints(grid: IGrid) {
const cellsDesired = Number(byId("pointsInput")?.dataset.cells);
if (cellsDesired !== grid.cellsDesired) return true;
@@ -69,7 +70,7 @@ function getBoundaryPoints(width: number, height: number, spacing: number) {
const h = height - offset * 2;
const numberX = Math.ceil(w / bSpacing) - 1;
const numberY = Math.ceil(h / bSpacing) - 1;
- const points = [];
+ const points: TPoints = [];
for (let i = 0.5; i < numberX; i++) {
let x = Math.ceil((w * i) / numberX + offset);
@@ -91,7 +92,7 @@ function getJitteredGrid(width: number, height: number, spacing: number) {
const doubleJittering = jittering * 2;
const jitter = () => Math.random() * doubleJittering - jittering;
- let points = [];
+ const points: TPoints = [];
for (let y = radius; y < height; y += spacing) {
for (let x = radius; x < width; x += spacing) {
const xj = Math.min(rn(x + jitter(), 2), width);
@@ -161,15 +162,15 @@ export function getGridPolygon(i: number): TPoints {
}
export function isLand(cellId: number) {
- return pack.cells.h[cellId] >= 20;
+ return pack.cells.h[cellId] >= MIN_LAND_HEIGHT;
}
export function isWater(cellId: number) {
- return pack.cells.h[cellId] < 20;
+ return pack.cells.h[cellId] < MIN_LAND_HEIGHT;
}
export function isCoastal(i: number) {
- return pack.cells.t[i] === 1;
+ return pack.cells.t[i] === DISTANCE_FIELD.LAND_COAST;
}
// findAll d3.quandtree search from https://bl.ocks.org/lwthatcher/b41479725e0ff2277c7ac90df2de2b5e