diff --git a/src/config/generation.ts b/src/config/generation.ts index 95ee79f2..ff17126d 100644 --- a/src/config/generation.ts +++ b/src/config/generation.ts @@ -5,5 +5,6 @@ export const MAX_HEIGHT = 100; export enum DISTANCE_FIELD { LAND_COAST = 1, UNMARKED = 0, - WATER_COAST = -1 + WATER_COAST = -1, + DEEPER_WATER = -2 } diff --git a/src/dialogs/dialogs/heightmap-editor.js b/src/dialogs/dialogs/heightmap-editor.js index 0ecfdd2e..097a0a74 100644 --- a/src/dialogs/dialogs/heightmap-editor.js +++ b/src/dialogs/dialogs/heightmap-editor.js @@ -221,9 +221,9 @@ export function open(options) { Lakes.openNearSeaLakes(grid); } OceanLayers(grid); - calculateTemperatures(); - generatePrecipitation(); - reGraph(); + calculateTemperatures(grid); + generatePrecipitation(grid); + reGraph(grid); drawCoastline(); Rivers.generate(erosionAllowed); @@ -340,9 +340,9 @@ export function open(options) { if (erosionAllowed) addLakesInDeepDepressions(); OceanLayers(grid); - calculateTemperatures(); - generatePrecipitation(); - reGraph(); + calculateTemperatures(grid); + generatePrecipitation(grid); + reGraph(grid); drawCoastline(); if (erosionAllowed) Rivers.generate(true); diff --git a/src/dialogs/dialogs/units-editor.js b/src/dialogs/dialogs/units-editor.js index 2dcea13c..43b6eb48 100644 --- a/src/dialogs/dialogs/units-editor.js +++ b/src/dialogs/dialogs/units-editor.js @@ -83,7 +83,7 @@ export function open() { } function changeHeightExponent() { - calculateTemperatures(); + calculateTemperatures(grid); if (layerIsOn("toggleTemp")) drawTemp(); } @@ -134,7 +134,7 @@ export function open() { // height exponent heightExponentInput.value = heightExponentOutput.value = 1.8; localStorage.removeItem("heightExponent"); - calculateTemperatures(); + calculateTemperatures(grid); // scale bar barSizeOutput.value = barSizeInput.value = 2; diff --git a/src/modules/io/load.js b/src/modules/io/load.js index 98c12374..d63414bc 100644 --- a/src/modules/io/load.js +++ b/src/modules/io/load.js @@ -399,7 +399,7 @@ async function parseLoadedData(data) { })(); void (function parsePackData() { - reGraph(); + reGraph(grid); reMarkFeatures(); pack.features = JSON.parse(data[12]); pack.cultures = JSON.parse(data[13]); diff --git a/src/modules/markup.ts b/src/modules/markup.ts index 57af2bcd..b3b9054d 100644 --- a/src/modules/markup.ts +++ b/src/modules/markup.ts @@ -7,7 +7,7 @@ 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}}) { +export function markupGridFeatures(grid: IGridWithHeights) { TIME && console.time("markupGridFeatures"); Math.random = aleaPRNG(seed); // get the same result on heightmap edit in Erase mode @@ -173,8 +173,10 @@ export function reMarkFeatures() { return "gulf"; } - function defineIslandGroup(cell, number) { - if (cell && features[cells.f[cell - 1]].type === "lake") return "lake_island"; + 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"; diff --git a/src/modules/precipitation.js b/src/modules/precipitation.js index e77c8053..aea8c8fe 100644 --- a/src/modules/precipitation.js +++ b/src/modules/precipitation.js @@ -5,11 +5,12 @@ import {minmax} from "utils/numberUtils"; import {rand} from "utils/probabilityUtils"; // simplest precipitation model -export function generatePrecipitation() { +export function generatePrecipitation(grid) { TIME && console.time("generatePrecipitation"); prec.selectAll("*").remove(); + const {cells, cellsX, cellsY} = grid; - cells.prec = new Uint8Array(cells.i.length); // precipitation array + const precipitation = new Uint8Array(cells.i.length); // precipitation array const cellsNumberModifier = (pointsInput.dataset.cells / 10000) ** 0.25; const precInputModifier = precInput.value / 100; @@ -94,20 +95,20 @@ export function generatePrecipitation() { if (cells.h[current] < 20) { // water cell if (cells.h[current + next] >= 20) { - cells.prec[current + next] += Math.max(humidity / rand(10, 20), 1); // coastal precipitation + precipitation[current + next] += Math.max(humidity / rand(10, 20), 1); // coastal precipitation } else { humidity = Math.min(humidity + 5 * modifier, maxPrec); // wind gets more humidity passing water cell - cells.prec[current] += 5 * modifier; // water cells precipitation (need to correctly pour water through lakes) + precipitation[current] += 5 * modifier; // water cells precipitation (need to correctly pour water through lakes) } continue; } // land cell const isPassable = cells.h[current + next] <= MAX_PASSABLE_ELEVATION; - const precipitation = isPassable ? getPrecipitation(humidity, current, next) : humidity; - cells.prec[current] += precipitation; - const evaporation = precipitation > 1.5 ? 1 : 0; // some humidity evaporates back to the atmosphere - humidity = isPassable ? minmax(humidity - precipitation + evaporation, 0, maxPrec) : 0; + const cellPrec = isPassable ? getPrecipitation(humidity, current, next) : humidity; + precipitation[current] += cellPrec; + const evaporation = cellPrec > 1.5 ? 1 : 0; // some humidity evaporates back to the atmosphere + humidity = isPassable ? minmax(humidity - cellPrec + evaporation, 0, maxPrec) : 0; } } } @@ -162,4 +163,5 @@ export function generatePrecipitation() { })(); TIME && console.timeEnd("generatePrecipitation"); + return precipitation; } diff --git a/src/modules/submap.js b/src/modules/submap.js index b67bb90e..e7ff76f9 100644 --- a/src/modules/submap.js +++ b/src/modules/submap.js @@ -125,10 +125,8 @@ window.Submap = (function () { OceanLayers(grid); calculateMapCoordinates(); - // calculateTemperatures(); - // generatePrecipitation(); stage("Cell cleanup."); - reGraph(); + reGraph(grid); // remove misclassified cells stage("Define coastline."); diff --git a/src/modules/temperature.js b/src/modules/temperature.js deleted file mode 100644 index c1a3c3b2..00000000 --- a/src/modules/temperature.js +++ /dev/null @@ -1,35 +0,0 @@ -import * as d3 from "d3"; - -import {TIME} from "config/logging"; -import {minmax, rn} from "utils/numberUtils"; - -// temperature model -export function calculateTemperatures() { - TIME && console.time("calculateTemperatures"); - const cells = grid.cells; - cells.temp = new Int8Array(cells.i.length); // temperature array - - const tEq = +temperatureEquatorInput.value; - const tPole = +temperaturePoleInput.value; - const tDelta = tEq - tPole; - const int = d3.easePolyInOut.exponent(0.5); // interpolation function - - d3.range(0, cells.i.length, grid.cellsX).forEach(function (r) { - const y = grid.points[r][1]; - const lat = Math.abs(mapCoordinates.latN - (y / graphHeight) * mapCoordinates.latT); // [0; 90] - const initTemp = tEq - int(lat / 90) * tDelta; - for (let i = r; i < r + grid.cellsX; i++) { - cells.temp[i] = minmax(initTemp - convertToFriendly(cells.h[i]), -128, 127); - } - }); - - // temperature decreases by 6.5 degree C per 1km - function convertToFriendly(h) { - if (h < 20) return 0; - const exponent = +heightExponentInput.value; - const height = Math.pow(h - 18, exponent); - return rn((height / 1000) * 6.5); - } - - TIME && console.timeEnd("calculateTemperatures"); -} diff --git a/src/modules/temperature.ts b/src/modules/temperature.ts new file mode 100644 index 00000000..5a5d3c41 --- /dev/null +++ b/src/modules/temperature.ts @@ -0,0 +1,47 @@ +import * as d3 from "d3"; + +import {TIME} from "config/logging"; +import {minmax} from "utils/numberUtils"; +import {getInputNumber} from "utils/nodeUtils"; +import {MIN_LAND_HEIGHT} from "config/generation"; + +const interpolate = d3.easePolyInOut.exponent(0.5); // interpolation function + +export function calculateTemperatures(grid: IGridWithHeights) { + TIME && console.time("calculateTemperatures"); + + const {cells, cellsX, points} = grid; + const heights = cells.h; + + const temperatures = new Int8Array(heights.length); // temperature array + + // temperature decreases by 6.5 Celsius per kilometer + const heightExponent = getInputNumber("heightExponentInput"); + function decreaseTempFromElevation(height: number) { + if (height < MIN_LAND_HEIGHT) return 0; + + const realHeight = Math.pow(height - 18, heightExponent); + return (realHeight / 1000) * 6.5; + } + + const tEq = getInputNumber("temperatureEquatorInput"); + const tPole = getInputNumber("temperaturePoleInput"); + const tDelta = tEq - tPole; + + const {latN, latT} = window.mapCoordinates; + + d3.range(0, heights.length, cellsX).forEach(rowStart => { + const y = points[rowStart][1]; + const lat = Math.abs(latN - (y / graphHeight) * latT); // [0; 90] + + const initTemp = tEq - interpolate(lat / 90) * tDelta; + for (let i = rowStart; i < rowStart + cellsX; i++) { + const elevationDecrease = decreaseTempFromElevation(heights[i]); + temperatures[i] = minmax(initTemp - elevationDecrease, -128, 127); + } + }); + + TIME && console.timeEnd("calculateTemperatures"); + + return temperatures; +} diff --git a/src/modules/ui/world-configurator.js b/src/modules/ui/world-configurator.js index 50146498..ba4d42ac 100644 --- a/src/modules/ui/world-configurator.js +++ b/src/modules/ui/world-configurator.js @@ -60,8 +60,8 @@ export function editWorld() { updateGlobeTemperature(); updateGlobePosition(); - calculateTemperatures(); - generatePrecipitation(); + calculateTemperatures(grid); + generatePrecipitation(grid); const heights = new Uint8Array(pack.cells.h); Rivers.generate(); Lakes.defineGroup(); diff --git a/src/scripts/generation.ts b/src/scripts/generation.ts index 9cd9ab15..c3c26e9c 100644 --- a/src/scripts/generation.ts +++ b/src/scripts/generation.ts @@ -3,16 +3,24 @@ import * as d3 from "d3"; import {ERROR, INFO, WARN} from "config/logging"; import {closeDialogs} from "dialogs/utils"; import {initLayers, renderLayer, restoreLayers} from "layers"; +// @ts-expect-error js module import {drawCoastline} from "modules/coastline"; import {calculateMapCoordinates, defineMapSize} from "modules/coordinates"; -import {markupGridFeatures, markupGridOcean} from "modules/markup"; +import {markupGridFeatures} from "modules/markup"; +// @ts-expect-error js module import {drawScaleBar, Rulers} from "modules/measurers"; +// @ts-expect-error js module import {generatePrecipitation} from "modules/precipitation"; import {calculateTemperatures} from "modules/temperature"; +// @ts-expect-error js module import {unfog} from "modules/ui/editors"; +// @ts-expect-error js module import {applyMapSize, randomizeOptions} from "modules/ui/options"; +// @ts-expect-error js module import {applyStyleOnLoad} from "modules/ui/stylePresets"; +// @ts-expect-error js module import {addZones} from "modules/zones"; +// @ts-expect-error js module import {aleaPRNG} from "scripts/aleaPRNG"; import {hideLoading, showLoading} from "scripts/loading"; import {clearMainTip, tip} from "scripts/tooltips"; @@ -45,9 +53,9 @@ export async function generate(options?: IGenerationOptions) { applyMapSize(); randomizeOptions(); - const updatedGrid = await updateGrid(precreatedGraph); + const updatedGrid = await updateGrid(grid, precreatedGraph); - reGraph(); + reGraph(updatedGrid); drawCoastline(); Rivers.generate(); @@ -75,6 +83,8 @@ export async function generate(options?: IGenerationOptions) { Markers.generate(); addZones(); + OceanLayers(updatedGrid); + drawScaleBar(scale); Names.getMapName(); @@ -110,35 +120,33 @@ export async function generate(options?: IGenerationOptions) { } } -async function updateGrid(precreatedGraph?: IGrid) { - const globalGrid = grid; - - const updatedGrid: IGraph & Partial = shouldRegenerateGridPoints(globalGrid) +async function updateGrid(globalGrid: IGrid, precreatedGraph?: IGrid): Promise { + const baseGrid: IGridBase = shouldRegenerateGridPoints(globalGrid) ? (precreatedGraph && undressGrid(precreatedGraph)) || generateGrid() : undressGrid(globalGrid); - const heights = await HeightmapGenerator.generate(updatedGrid); - updatedGrid.cells.h = heights; + const heights: Uint8Array = await HeightmapGenerator.generate(baseGrid); + if (!heights) throw new Error("Heightmap generation failed"); + const heightsGrid = {...baseGrid, cells: {...baseGrid.cells, h: heights}}; - const {featureIds, distanceField, features} = markupGridFeatures(updatedGrid); - updatedGrid.cells.f = featureIds; - updatedGrid.cells.t = distanceField; - updatedGrid.features = features; + const {featureIds, distanceField, features} = markupGridFeatures(heightsGrid); + const markedGrid = {...heightsGrid, features, cells: {...heightsGrid.cells, f: featureIds, t: distanceField}}; const touchesEdges = features.some(feature => feature && feature.land && feature.border); defineMapSize(touchesEdges); - - Lakes.addLakesInDeepDepressions(updatedGrid); - Lakes.openNearSeaLakes(updatedGrid); - - OceanLayers(updatedGrid); - window.mapCoordinates = calculateMapCoordinates(); - calculateTemperatures(); - generatePrecipitation(); + + Lakes.addLakesInDeepDepressions(markedGrid); + Lakes.openNearSeaLakes(markedGrid); + + const temperature = calculateTemperatures(markedGrid); + const temperatureGrid = {...markedGrid, cells: {...markedGrid.cells, temp: temperature}}; + + const prec = generatePrecipitation(temperatureGrid); + return {...temperatureGrid, cells: {...temperatureGrid.cells, prec}}; } -function undressGrid(extendedGrid: IGrid) { +function undressGrid(extendedGrid: IGrid): IGridBase { 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}; @@ -154,12 +162,17 @@ 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(); + byId("deftemp") - .querySelectorAll("path, clipPath, svg") + ?.querySelectorAll("path, clipPath, svg") .forEach(el => el.remove()); - byId("coas").innerHTML = ""; // remove auto-generated emblems + + // remove auto-generated emblems + if (byId("coas")) byId("coas")!.innerHTML = ""; + notes = []; rulers = new Rulers(); + unfog(); } diff --git a/src/scripts/reGraph.ts b/src/scripts/reGraph.ts index a1546487..25701084 100644 --- a/src/scripts/reGraph.ts +++ b/src/scripts/reGraph.ts @@ -5,9 +5,12 @@ import {UINT16_MAX} from "constants"; import {createTypedArray} from "utils/arrayUtils"; import {calculateVoronoi, getPackPolygon} from "utils/graphUtils"; import {rn} from "utils/numberUtils"; +import {DISTANCE_FIELD, MIN_LAND_HEIGHT} from "config/generation"; + +const {LAND_COAST, WATER_COAST, DEEPER_WATER} = DISTANCE_FIELD; // recalculate Voronoi Graph to pack cells -export function reGraph() { +export function reGraph(grid: IGrid) { TIME && console.time("reGraph"); const {cells: gridCells, points, features} = grid; const newCells: {p: TPoints; g: number[]; h: number[]} = {p: [], g: [], h: []}; // store new data @@ -16,14 +19,18 @@ export function reGraph() { for (const i of gridCells.i) { const height = gridCells.h[i]; const type = gridCells.t[i]; - if (height < 20 && type !== -1 && type !== -2) continue; // exclude all deep ocean points - if (type === -2 && (i % 4 === 0 || features[gridCells.f[i]].type === "lake")) continue; // exclude non-coastal lake points + if (height < MIN_LAND_HEIGHT && type !== WATER_COAST && type !== DEEPER_WATER) continue; // exclude all deep ocean points + + const feature = features[gridCells.f[i]]; + const isLake = feature && feature.type === "lake"; + + if (type === DEEPER_WATER && (i % 4 === 0 || isLake)) continue; // exclude non-coastal lake points const [x, y] = points[i]; addNewPoint(i, x, y, height); // add additional points for cells along coast - if (type === 1 || type === -1) { + if (type === LAND_COAST || type === WATER_COAST) { if (gridCells.b[i]) continue; // not for near-border cells gridCells.c[i].forEach(e => { if (i > e) return; @@ -55,7 +62,7 @@ export function reGraph() { pack.cells.p = newCells.p; pack.cells.g = createTypedArray({maxValue: grid.points.length, from: newCells.g}); pack.cells.q = d3.quadtree(newCells.p.map(([x, y], i) => [x, y, i])); - pack.cells.h = createTypedArray({maxValue: 100, from: newCells.h}); + pack.cells.h = new Uint8Array(newCells.h); pack.cells.area = createTypedArray({maxValue: UINT16_MAX, from: pack.cells.i}).map(getCellArea); TIME && console.timeEnd("reGraph"); diff --git a/src/types/grid.d.ts b/src/types/grid.d.ts index 6dcf39fd..c6417078 100644 --- a/src/types/grid.d.ts +++ b/src/types/grid.d.ts @@ -5,16 +5,26 @@ interface IGrid extends IGraph { spacing: number; boundary: TPoints; points: TPoints; - cells: IGridCells; + cells: IGraphCells & IGridCells; features: TGridFeatures; } -interface IGridCells extends IGraphCells { - h: UintArray; // heights, [0, 100], see MIN_LAND_HEIGHT constant +interface IGridCells { + h: Uint8Array; // 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 + temp: Int8Array; // temparature in Celsius + prec: Uint8Array; // precipitation in inner units +} + +interface IGridBase extends IGrid { + cells: IGraphCells & Partial; + features?: TGridFeatures; +} + +interface IGridWithHeights extends IGrid { + cells: IGraphCells & Partial & {h: Uint8Array}; + features?: TGridFeatures; } type TGridFeatures = [0, ...IGridFeature[]]; diff --git a/src/types/overrides.d.ts b/src/types/overrides.d.ts index c7458943..b0b099c2 100644 --- a/src/types/overrides.d.ts +++ b/src/types/overrides.d.ts @@ -6,16 +6,23 @@ interface Navigator { interface Window { mapCoordinates: IMapCoordinates; - $: typeof $; + $: typeof $; // jQuery + // 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; + Biomes: any; + Names: any; + ThreeD: any; + ReliefIcons: any; + Zoom: any; + Lakes: any; + HeightmapGenerator: any; + OceanLayers: any; + Rivers: any; + Cultures: any; + BurgsAndStates: any; + Religions: any; + Military: any; + Markers: any; } interface Node { diff --git a/src/types/pack.d.ts b/src/types/pack.d.ts index b3728fd9..a80e7324 100644 --- a/src/types/pack.d.ts +++ b/src/types/pack.d.ts @@ -11,7 +11,7 @@ interface IPack extends IGraph { interface IPackCells extends IGraphCells { p: TPoints; // cell center points - h: UintArray; // heights, [0, 100], see MIN_LAND_HEIGHT constant + h: Uint8Array; // heights, [0, 100], see MIN_LAND_HEIGHT constant t: Int8Array; // see DISTANCE_FIELD enum f: Uint16Array; // feature id, see TPackFeature g: UintArray;