diff --git a/src/config/generation.ts b/src/config/generation.ts index ff17126d..85fdf67e 100644 --- a/src/config/generation.ts +++ b/src/config/generation.ts @@ -3,8 +3,9 @@ export const MIN_LAND_HEIGHT = 20; export const MAX_HEIGHT = 100; export enum DISTANCE_FIELD { - LAND_COAST = 1, UNMARKED = 0, WATER_COAST = -1, - DEEPER_WATER = -2 + LAND_COAST = 1, + DEEPER_WATER = -2, + LANDLOCKED = 2 } diff --git a/src/dialogs/dialogs/heightmap-editor.js b/src/dialogs/dialogs/heightmap-editor.js index b9272c42..34b8de58 100644 --- a/src/dialogs/dialogs/heightmap-editor.js +++ b/src/dialogs/dialogs/heightmap-editor.js @@ -17,7 +17,7 @@ import {setDefaultEventHandlers} from "scripts/events"; import {undraw} from "scripts/generation/generation"; import {prompt} from "scripts/prompt"; import {rankCells} from "scripts/rankCells"; -import {reGraph} from "scripts/reGraph"; +import {repackGrid, markupPackFeatures} from "scripts/reGraph"; import {clearMainTip, showMainTip, tip} from "scripts/tooltips"; import {createTypedArray, last} from "utils/arrayUtils"; import {getColorScheme, getHeightColor} from "utils/colorUtils"; @@ -223,8 +223,8 @@ export function open(options) { OceanLayers(grid); calculateTemperatures(grid); generatePrecipitation(grid); - reGraph(grid); - reMarkFeatures(pack, newGrid); + repackGrid(grid); + markupPackFeatures(pack, newGrid); drawCoastline(pack); Rivers.generate(pack, grid, erosionAllowed); diff --git a/src/modules/io/load.js b/src/modules/io/load.js index 54fa8787..a51de935 100644 --- a/src/modules/io/load.js +++ b/src/modules/io/load.js @@ -3,7 +3,6 @@ import * as d3 from "d3"; import {INFO} from "config/logging"; import {closeDialogs} from "dialogs/utils"; import {updatePresetInput} from "layers"; -import {reMarkFeatures} from "modules/markup"; import {setDefaultEventHandlers} from "scripts/events"; import {regenerateMap} from "scripts/generation/generation"; import {calculateVoronoi} from "scripts/generation/graph"; diff --git a/src/modules/markup.ts b/src/modules/markup.ts index 48cca00c..beea8674 100644 --- a/src/modules/markup.ts +++ b/src/modules/markup.ts @@ -3,10 +3,12 @@ import {TIME} from "config/logging"; import {INT8_MAX} from "constants"; // @ts-expect-error js module import {aleaPRNG} from "scripts/aleaPRNG"; +import {createTypedArray} from "utils/arrayUtils"; +import {dist2} from "utils/functionUtils"; -const {UNMARKED, LAND_COAST, WATER_COAST} = DISTANCE_FIELD; +const {UNMARKED, LAND_COAST, WATER_COAST, LANDLOCKED, DEEPER_WATER} = DISTANCE_FIELD; -// define features (grid.features: ocean, lakes, islands) and calculate distance field (cells.t) +// define features (oceans, lakes, islands) export function markupGridFeatures(grid: IGridWithHeights) { TIME && console.time("markupGridFeatures"); Math.random = aleaPRNG(seed); // get the same result on heightmap edit in Erase mode @@ -20,7 +22,7 @@ export function markupGridFeatures(grid: IGridWithHeights) { const n = cells.i.length; const featureIds = new Uint16Array(n); // starts from 1 - let distanceField = new Int8Array(n); + const distanceField = new Int8Array(n); const features: TGridFeatures = [0]; const queue = [0]; @@ -55,33 +57,166 @@ export function markupGridFeatures(grid: IGridWithHeights) { } // markup deep ocean cells - distanceField = markup({graph: grid, distanceField, start: -2, increment: -1, limit: -10}); + const dfOceanMarked = markup({ + distanceField, + neighbors: grid.cells.c, + start: DEEPER_WATER, + increment: -1, + limit: -10 + }); TIME && console.timeEnd("markupGridFeatures"); - return {featureIds, distanceField, features}; + return {featureIds, distanceField: dfOceanMarked, features}; +} + +// define features (oceans, lakes, islands) add related details +export function markupPackFeatures(grid: IGrid, cells: Pick) { + TIME && console.time("markupPackFeatures"); + + const packCellsNumber = cells.h.length; + const gridCellsNumber = grid.cells.h.length; + + const features: TPackFeatures = [0]; + const featureIds = new Uint16Array(packCellsNumber); // ids of features, starts from 1 + const distanceField = new Int8Array(packCellsNumber); // distance from coast; 1 = land along coast; -1 = water along coast + const haven = createTypedArray({maxValue: packCellsNumber, length: packCellsNumber}); // haven (opposite water cell) + const harbor = new Uint8Array(packCellsNumber); // harbor (number of adjacent water cells) + + const defineHaven = (cellId: number) => { + const waterCells = cells.c[cellId].filter(c => cells.h[c] < MIN_LAND_HEIGHT); + const distances = waterCells.map(c => dist2(cells.p[cellId], cells.p[c])); + const closest = distances.indexOf(Math.min.apply(Math, distances)); + + haven[cellId] = waterCells[closest]; + harbor[cellId] = waterCells.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; + + function defineOceanGroup(cellsNumber: number) { + if (cellsNumber > OCEAN_MIN_SIZE) return "ocean"; + if (cellsNumber > SEA_MIN_SIZE) return "sea"; + return "gulf"; + } + + function defineIslandGroup(firstCell: number, cellsNumber: number) { + const prevCellFeature = features[featureIds[firstCell - 1]]; + + if (prevCellFeature && prevCellFeature.type === "lake") return "lake_island"; + if (cellsNumber > CONTINENT_MIN_SIZE) return "continent"; + if (cellsNumber > ISLAND_MIN_SIZE) return "island"; + return "isle"; + } + + function addIsland(featureId: number, border: boolean, firstCell: number, cells: number) { + const group = defineIslandGroup(firstCell, cells); + const feature: IPackFeatureIsland = {i: featureId, type: "island", group, land: true, border, cells, firstCell}; + features.push(feature); + } + + function addOcean(featureId: number, firstCell: number, cells: number) { + const group = defineOceanGroup(cells); + const feature: IPackFeatureOcean = { + i: featureId, + type: "ocean", + group, + land: false, + border: false, + cells, + firstCell + }; + features.push(feature); + } + + function addLake(featureId: number, firstCell: number, cells: number) { + const group = "freshwater"; // temp, to be defined later + const name = ""; // temp, to be defined later + const feature: IPackFeatureLake = { + i: featureId, + type: "lake", + group, + name, + land: false, + border: false, + cells, + firstCell + }; + features.push(feature); + } + + const queue = [0]; + for (let featureId = 1; queue[0] !== -1; featureId++) { + const firstCell = queue[0]; + featureIds[firstCell] = featureId; // assign feature number + + const land = cells.h[firstCell] >= MIN_LAND_HEIGHT; + let border = false; // true if feature touches map border + let cellNumber = 1; // count cells in a feature + + while (queue.length) { + const cellId = queue.pop()!; + if (cells.b[cellId]) border = true; + + for (const neighborId of cells.c[cellId]) { + const isNeibLand = cells.h[neighborId] >= MIN_LAND_HEIGHT; + + 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; + cellNumber++; + } + } + } + + // const vertices = detectFeatureVertices(cells, firstCell); + + if (land) addIsland(featureId, border, firstCell, cellNumber); + else if (border) addOcean(featureId, firstCell, cellNumber); + else addLake(featureId, firstCell, cellNumber); + + queue[0] = featureIds.findIndex(f => f === UNMARKED); // find unmarked cell + } + + // markup pack land cells + const dfLandMarked = markup({distanceField, neighbors: cells.c, start: LANDLOCKED + 1, increment: 1}); + + TIME && console.timeEnd("markupPackFeatures"); + + return {features, featureIds, distanceField: dfLandMarked, haven, harbor}; } // calculate distance to coast for every cell function markup({ - graph, distanceField, + neighbors, start, increment, - limit + limit = INT8_MAX }: { - graph: IGraph; distanceField: Int8Array; + neighbors: number[][]; start: number; increment: number; - limit: 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++) { + for (let cellId = 0; cellId < neighbors.length; cellId++) { if (distanceField[cellId] !== prevDistance) continue; for (const neighborId of neighbors[cellId]) { @@ -95,94 +230,24 @@ function markup({ return distanceField; } -// Re-mark features (ocean, lakes, islands) -export function reMarkFeatures(pack: IPackBase, grid: IGrid) { - 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++; - } - }); +// 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 < 50000); 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 + const v = vertices.v[current]; // neighboring vertices + const c0 = c[0] >= n || cells.t[c[0]] === t; + const c1 = c[1] >= n || cells.t[c[1]] === t; + const c2 = c[2] >= n || cells.t[c[2]] === t; + if (v[0] !== prev && c0 !== c1) current = v[0]; + else if (v[1] !== prev && c1 !== c2) current = v[1]; + else if (v[2] !== prev && c0 !== c2) current = v[2]; + if (current === chain[chain.length - 1]) { + ERROR && console.error("Next vertex is not found"); + break; } - - 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: 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(cellId: number, number: number) { - const prevCellFeature = features[cells.f[cellId - 1]]; - - if (cellId && prevCellFeature && prevCellFeature.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"); + return chain; } diff --git a/src/scripts/generation/generation.ts b/src/scripts/generation/generation.ts index 6359772a..21bc9745 100644 --- a/src/scripts/generation/generation.ts +++ b/src/scripts/generation/generation.ts @@ -3,10 +3,9 @@ import * as d3 from "d3"; import {ERROR, INFO, WARN} from "config/logging"; import {closeDialogs} from "dialogs/utils"; import {openDialog} from "dialogs"; -import {initLayers, renderLayer, restoreLayers} from "layers"; +import {initLayers, restoreLayers} from "layers"; // @ts-expect-error js module import {drawCoastline} from "modules/coastline"; -import {reMarkFeatures} from "modules/markup"; // @ts-expect-error js module import {drawScaleBar, Rulers} from "modules/measurers"; // @ts-expect-error js module @@ -26,26 +25,12 @@ import {debounce} from "utils/functionUtils"; import {rn} from "utils/numberUtils"; import {generateSeed} from "utils/probabilityUtils"; import {byId} from "utils/shorthands"; -import {rankCells} from "../rankCells"; import {showStatistics} from "../statistics"; import {createGrid} from "./grid"; -import {reGraph} from "./reGraph"; +import {createPack} from "./pack"; import {getInputValue, setInputValue} from "utils/nodeUtils"; -const { - Zoom, - Lakes, - OceanLayers, - Rivers, - Biomes, - Cultures, - BurgsAndStates, - Religions, - Military, - Markers, - Names, - ThreeD -} = window; +const {Zoom, ThreeD} = window; interface IGenerationOptions { seed: string; @@ -66,42 +51,10 @@ async function generate(options?: IGenerationOptions) { randomizeOptions(); const newGrid = await createGrid(grid, precreatedGraph); - const newPack = reGraph(newGrid); + const newPack = createPack(newGrid); - reMarkFeatures(newPack, newGrid); - drawCoastline(newPack); - - Rivers.generate(newPack, newGrid); - renderLayer("rivers", newPack); - Lakes.defineGroup(newPack); - Biomes.define(newPack, newGrid); - - rankCells(newPack); - Cultures.generate(); - Cultures.expand(); - BurgsAndStates.generate(); - Religions.generate(); - BurgsAndStates.defineStateForms(); - BurgsAndStates.generateProvinces(); - BurgsAndStates.defineBurgFeatures(); - - renderLayer("states"); - renderLayer("borders"); - BurgsAndStates.drawStateLabels(); - - Rivers.specify(); - Lakes.generateName(); - - Military.generate(); - Markers.generate(); - addZones(); - - OceanLayers(newGrid); - - drawScaleBar(window.scale); - Names.getMapName(); - - // @ts-expect-error redefine global + // redefine global grid and pack + grid = newGrid; pack = newPack; WARN && console.warn(`TOTAL: ${rn((performance.now() - timeStart) / 1000, 2)}s`); diff --git a/src/scripts/generation/reGraph.ts b/src/scripts/generation/pack.ts similarity index 57% rename from src/scripts/generation/reGraph.ts rename to src/scripts/generation/pack.ts index 8f8f5d4c..bc80cab1 100644 --- a/src/scripts/generation/reGraph.ts +++ b/src/scripts/generation/pack.ts @@ -1,17 +1,75 @@ import * as d3 from "d3"; +import {renderLayer} from "layers"; +// @ts-expect-error js module +import {drawCoastline} from "modules/coastline"; +import {markupPackFeatures} from "modules/markup"; +// @ts-expect-error js module +import {drawScaleBar} from "modules/measurers"; +// @ts-expect-error js module +import {addZones} from "modules/zones"; +import {DISTANCE_FIELD, MIN_LAND_HEIGHT} from "config/generation"; import {TIME} from "config/logging"; import {UINT16_MAX} from "constants"; -import {createTypedArray} from "utils/arrayUtils"; import {calculateVoronoi} from "scripts/generation/graph"; +import {createTypedArray} from "utils/arrayUtils"; +import {pick} from "utils/functionUtils"; import {rn} from "utils/numberUtils"; -import {DISTANCE_FIELD, MIN_LAND_HEIGHT} from "config/generation"; +import {rankCells} from "../rankCells"; const {LAND_COAST, WATER_COAST, DEEPER_WATER} = DISTANCE_FIELD; +const {Lakes, OceanLayers, Rivers, Biomes, Cultures, BurgsAndStates, Religions, Military, Markers, Names} = window; -// recalculate Voronoi Graph to pack cells -export function reGraph(grid: IGrid): IPackBase { - TIME && console.time("reGraph"); +export function createPack(grid: IGrid): IPack { + const {vertices, cells} = repackGrid(grid); + + const markup = markupPackFeatures(grid, pick(cells, "c", "b", "p", "h")); + + drawCoastline({vertices, cells}); // split into vertices definition and rendering + + // Rivers.generate(newPack, grid); + // renderLayer("rivers", newPack); + // Lakes.defineGroup(newPack); + // Biomes.define(newPack, grid); + + // const rankCellsData = pick(newPack.cells, "i", "f", "fl", "conf", "r", "h", "area", "biome", "haven", "harbor"); + // rankCells(newPack.features!, rankCellsData); + + Cultures.generate(); + Cultures.expand(); + BurgsAndStates.generate(); + Religions.generate(); + BurgsAndStates.defineStateForms(); + BurgsAndStates.generateProvinces(); + BurgsAndStates.defineBurgFeatures(); + + renderLayer("states"); + renderLayer("borders"); + BurgsAndStates.drawStateLabels(); + + Rivers.specify(); + Lakes.generateName(); + + Military.generate(); + Markers.generate(); + addZones(); + + OceanLayers(newGrid); + + drawScaleBar(window.scale); + Names.getMapName(); + + const pack = { + vertices, + cells + }; + + return pack as IPack; +} + +// repack grid cells: discart deep water cells, add land cells along the coast +function repackGrid(grid: IGrid) { + TIME && console.time("repackGrid"); const {cells: gridCells, points, features} = grid; const newCells: {p: TPoints; g: number[]; h: number[]} = {p: [], g: [], h: []}; // store new data const spacing2 = grid.spacing ** 2; @@ -59,7 +117,7 @@ export function reGraph(grid: IGrid): IPackBase { return Math.min(area, UINT16_MAX); } - const pack: IPackBase = { + const pack = { vertices, cells: { ...cells, @@ -71,6 +129,6 @@ export function reGraph(grid: IGrid): IPackBase { } }; - TIME && console.timeEnd("reGraph"); + TIME && console.timeEnd("repackGrid"); return pack; } diff --git a/src/scripts/rankCells.ts b/src/scripts/rankCells.ts index 447d3aad..39875d71 100644 --- a/src/scripts/rankCells.ts +++ b/src/scripts/rankCells.ts @@ -8,12 +8,14 @@ const FLUX_MAX_BONUS = 250; const SUITABILITY_FACTOR = 5; // assess cells suitability for population and rank cells for culture centers and burgs placement -export function rankCells(pack: IPack) { +export function rankCells( + features: TPackFeatures, + cells: Pick +) { TIME && console.time("rankCells"); - const {cells, features} = pack; - cells.s = new Int16Array(cells.i.length); // cell suitability array - cells.pop = new Float32Array(cells.i.length); // cell population array + const suitability = new Int16Array(cells.i.length); // cell suitability array + const population = new Float32Array(cells.i.length); // cell population array const meanFlux = d3.median(cells.fl.filter(f => f)) || 0; const maxFlux = (d3.max(cells.fl) || 0) + (d3.max(cells.conf) || 0); // to normalize flux @@ -30,14 +32,12 @@ export function rankCells(pack: IPack) { const coastBonus = getCoastBonus(cellId); // [-30, 30] const estuaryBonus = getEstuaryBonus(cellId); // [0, 15] - const suitability = - (habitabilityBonus + riverBonus + elevationBonus + coastBonus + estuaryBonus) / SUITABILITY_FACTOR; // [-30, 311] + const bonuses = [habitabilityBonus, riverBonus, elevationBonus, coastBonus, estuaryBonus]; + const total = d3.sum(bonuses) / SUITABILITY_FACTOR; // [-30, 311] + suitability[cellId] = total; // cell rural population is suitability adjusted by cell area - const population = suitability > 0 ? suitability * (cells.area[cellId] / meanArea) : 0; - - cells.pop[cellId] = population; - cells.s[cellId] = suitability; + population[cellId] = total > 0 ? total * (cells.area[cellId] / meanArea) : 0; } function getHabitabilityBonus(cellId: number) { @@ -57,7 +57,10 @@ export function rankCells(pack: IPack) { if (!isCoastal(cellId)) return 0; const havenCell = cells.haven[cellId]; - const {group} = features[cells.f[havenCell]]; + const feature = features[cells.f[havenCell]]; + if (!feature) return 0; + + const {group} = feature; // lake coast if (group === "freshwater") return 30; @@ -78,4 +81,6 @@ export function rankCells(pack: IPack) { } TIME && console.timeEnd("rankCells"); + + return {s: suitability, pop: population}; } diff --git a/src/utils/functionUtils.ts b/src/utils/functionUtils.ts index 6a5e6b0a..ad7c1544 100644 --- a/src/utils/functionUtils.ts +++ b/src/utils/functionUtils.ts @@ -84,3 +84,15 @@ export function getBase64(url: string, callback: (base64: string | ArrayBuffer | xhr.responseType = "blob"; xhr.send(); } + +export function pick(obj: T, ...keys: K[]): Pick { + const ret = {} as Pick; + keys.forEach(key => { + ret[key] = obj[key]; + }); + return ret; +} + +export function dist2([x1, y1]: [number, number], [x2, y2]: [number, number]) { + return (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2); +}