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