From 1bb90251cd16eb9ba484a42e23879e93fb90e020 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Fri, 11 Aug 2023 14:31:08 +0400 Subject: [PATCH] feat: move biomes code to a separate module, reduce deserts amount --- index.html | 13 +-- main.js | 116 +------------------------ modules/biomes.js | 144 +++++++++++++++++++++++++++++++ modules/io/load.js | 2 +- modules/submap.js | 2 +- modules/ui/biomes-editor.js | 27 ++++-- modules/ui/heightmap-editor.js | 12 +-- modules/ui/world-configurator.js | 2 +- versioning.js | 2 +- 9 files changed, 185 insertions(+), 135 deletions(-) create mode 100644 modules/biomes.js diff --git a/index.html b/index.html index f143a48a..ed6d14a4 100644 --- a/index.html +++ b/index.html @@ -7943,6 +7943,7 @@ + @@ -7951,7 +7952,7 @@ - + @@ -7962,16 +7963,16 @@ - + - - + + - + @@ -8005,7 +8006,7 @@ - + diff --git a/main.js b/main.js index 4996cf88..6967242d 100644 --- a/main.js +++ b/main.js @@ -159,7 +159,7 @@ let notes = []; let rulers = new Rulers(); let customization = 0; -let biomesData = applyDefaultBiomesSystem(); +let biomesData = Biomes.getDefault(); let nameBases = Names.getNameBases(); // cultures-related data let color = d3.scaleSequential(d3.interpolateSpectral); // default color scheme @@ -425,79 +425,6 @@ function findBurgForMFCG(params) { tip("Here stands the glorious city of " + b.name, true, "success", 15000); } -// apply default biomes data -function applyDefaultBiomesSystem() { - const name = [ - "Marine", - "Hot desert", - "Cold desert", - "Savanna", - "Grassland", - "Tropical seasonal forest", - "Temperate deciduous forest", - "Tropical rainforest", - "Temperate rainforest", - "Taiga", - "Tundra", - "Glacier", - "Wetland" - ]; - const color = [ - "#466eab", - "#fbe79f", - "#b5b887", - "#d2d082", - "#c8d68f", - "#b6d95d", - "#29bc56", - "#7dcb35", - "#409c43", - "#4b6b32", - "#96784b", - "#d5e7eb", - "#0b9131" - ]; - const habitability = [0, 4, 10, 22, 30, 50, 100, 80, 90, 12, 4, 0, 12]; - const iconsDensity = [0, 3, 2, 120, 120, 120, 120, 150, 150, 100, 5, 0, 150]; - const icons = [ - {}, - {dune: 3, cactus: 6, deadTree: 1}, - {dune: 9, deadTree: 1}, - {acacia: 1, grass: 9}, - {grass: 1}, - {acacia: 8, palm: 1}, - {deciduous: 1}, - {acacia: 5, palm: 3, deciduous: 1, swamp: 1}, - {deciduous: 6, swamp: 1}, - {conifer: 1}, - {grass: 1}, - {}, - {swamp: 1} - ]; - const cost = [10, 200, 150, 60, 50, 70, 70, 80, 90, 200, 1000, 5000, 150]; // biome movement cost - const biomesMartix = [ - // hot ↔ cold [>19°C; <-4°C]; dry ↕ wet - new Uint8Array([1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 10]), - new Uint8Array([1, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 9, 9, 9, 9, 10, 10, 10]), - new Uint8Array([5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 9, 9, 9, 9, 9, 10, 10, 10]), - new Uint8Array([5, 6, 6, 6, 6, 6, 6, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 10, 10, 10]), - new Uint8Array([7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 10, 10]) - ]; - - // parse icons weighted array into a simple array - for (let i = 0; i < icons.length; i++) { - const parsed = []; - for (const icon in icons[i]) { - for (let j = 0; j < icons[i][icon]; j++) { - parsed.push(icon); - } - } - icons[i] = parsed; - } - - return {i: d3.range(0, name.length), name, color, biomesMartix, habitability, iconsDensity, icons, cost}; -} - function handleZoom(isScaleChanged, isPositionChanged) { viewbox.attr("transform", `translate(${viewX} ${viewY}) scale(${scale})`); @@ -708,7 +635,7 @@ async function generate(options) { Rivers.generate(); drawRivers(); Lakes.defineGroup(); - defineBiomes(); + Biomes.define(); rankCells(); Cultures.generate(); @@ -1499,45 +1426,6 @@ function isWetLand(moisture, temperature, height) { return false; } -// assign biome id for each cell -function defineBiomes() { - TIME && console.time("defineBiomes"); - const {cells} = pack; - const {temp, prec} = grid.cells; - cells.biome = new Uint8Array(cells.i.length); // biomes array - - for (const i of cells.i) { - const temperature = temp[cells.g[i]]; - const height = cells.h[i]; - const moisture = height < 20 ? 0 : calculateMoisture(i); - cells.biome[i] = getBiomeId(moisture, temperature, height); - } - - function calculateMoisture(i) { - let moist = prec[cells.g[i]]; - if (cells.r[i]) moist += Math.max(cells.fl[i] / 20, 2); - - const n = cells.c[i] - .filter(isLand) - .map(c => prec[cells.g[c]]) - .concat([moist]); - return rn(4 + d3.mean(n)); - } - - TIME && console.timeEnd("defineBiomes"); -} - -// assign biome id to a cell -function getBiomeId(moisture, temperature, height) { - if (height < 20) return 0; // marine biome: all water cells - if (temperature < -5) return 11; // permafrost biome - if (isWetLand(moisture, temperature, height)) return 12; // wetland biome - - const moistureBand = Math.min((moisture / 5) | 0, 4); // [0-4] - const temperatureBand = Math.min(Math.max(20 - temperature, 0), 25); // [0-25] - return biomesData.biomesMartix[moistureBand][temperatureBand]; -} - // assess cells suitability to calculate population and rand cells for culture center and burgs placement function rankCells() { TIME && console.time("rankCells"); diff --git a/modules/biomes.js b/modules/biomes.js new file mode 100644 index 00000000..d7c95f77 --- /dev/null +++ b/modules/biomes.js @@ -0,0 +1,144 @@ +"use strict"; + +const MIN_LAND_HEIGHT = 20; + +const names = [ + "Marine", + "Hot desert", + "Cold desert", + "Savanna", + "Grassland", + "Tropical seasonal forest", + "Temperate deciduous forest", + "Tropical rainforest", + "Temperate rainforest", + "Taiga", + "Tundra", + "Glacier", + "Wetland" +]; + +window.Biomes = (function () { + const getDefault = () => { + const name = [ + "Marine", + "Hot desert", + "Cold desert", + "Savanna", + "Grassland", + "Tropical seasonal forest", + "Temperate deciduous forest", + "Tropical rainforest", + "Temperate rainforest", + "Taiga", + "Tundra", + "Glacier", + "Wetland" + ]; + + const color = [ + "#466eab", + "#fbe79f", + "#b5b887", + "#d2d082", + "#c8d68f", + "#b6d95d", + "#29bc56", + "#7dcb35", + "#409c43", + "#4b6b32", + "#96784b", + "#d5e7eb", + "#0b9131" + ]; + const habitability = [0, 4, 10, 22, 30, 50, 100, 80, 90, 12, 4, 0, 12]; + const iconsDensity = [0, 3, 2, 120, 120, 120, 120, 150, 150, 100, 5, 0, 150]; + const icons = [ + {}, + {dune: 3, cactus: 6, deadTree: 1}, + {dune: 9, deadTree: 1}, + {acacia: 1, grass: 9}, + {grass: 1}, + {acacia: 8, palm: 1}, + {deciduous: 1}, + {acacia: 5, palm: 3, deciduous: 1, swamp: 1}, + {deciduous: 6, swamp: 1}, + {conifer: 1}, + {grass: 1}, + {}, + {swamp: 1} + ]; + const cost = [10, 200, 150, 60, 50, 70, 70, 80, 90, 200, 1000, 5000, 150]; // biome movement cost + const biomesMartix = [ + // hot ↔ cold [>19°C; <-4°C]; dry ↕ wet + new Uint8Array([1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 10]), + new Uint8Array([3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 9, 9, 9, 9, 10, 10, 10]), + new Uint8Array([5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 9, 9, 9, 9, 9, 10, 10, 10]), + new Uint8Array([5, 6, 6, 6, 6, 6, 6, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 10, 10, 10]), + new Uint8Array([7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 10, 10]) + ]; + + // parse icons weighted array into a simple array + for (let i = 0; i < icons.length; i++) { + const parsed = []; + for (const icon in icons[i]) { + for (let j = 0; j < icons[i][icon]; j++) { + parsed.push(icon); + } + } + icons[i] = parsed; + } + + return {i: d3.range(0, name.length), name, color, biomesMartix, habitability, iconsDensity, icons, cost}; + }; + + // assign biome id for each cell + function define() { + TIME && console.time("defineBiomes"); + + const {fl: flux, r: riverIds, h: heights, c: neighbors, g: gridReference} = pack.cells; + const {temp, prec} = grid.cells; + pack.cells.biome = new Uint8Array(pack.cells.i.length); // biomes array + + for (let cellId = 0; cellId < heights.length; cellId++) { + const height = heights[cellId]; + const moisture = height < MIN_LAND_HEIGHT ? 0 : calculateMoisture(cellId); + const temperature = temp[gridReference[cellId]]; + pack.cells.biome[cellId] = getId(moisture, temperature, height, Boolean(riverIds[cellId])); + } + + function calculateMoisture(cellId) { + let moisture = prec[gridReference[cellId]]; + if (riverIds[cellId]) moisture += Math.max(flux[cellId] / 10, 2); + + const moistAround = neighbors[cellId] + .filter(neibCellId => heights[neibCellId] >= MIN_LAND_HEIGHT) + .map(c => prec[gridReference[c]]) + .concat([moisture]); + return rn(4 + d3.mean(moistAround)); + } + + TIME && console.timeEnd("defineBiomes"); + } + + function getId(moisture, temperature, height, hasRiver) { + if (height < 20) return 0; // all water cells: marine biome + if (temperature < -5) return 11; // too cold: permafrost biome + if (temperature >= 25 && !hasRiver && moisture < 8) return 1; // too hot and dry: hot desert biome + if (isWetland(moisture, temperature, height)) return 12; // too wet: wetland biome + + // in other cases use biome matrix + const moistureBand = Math.min((moisture / 5) | 0, 4); // [0-4] + const temperatureBand = Math.min(Math.max(20 - temperature, 0), 25); // [0-25] + return biomesData.biomesMartix[moistureBand][temperatureBand]; + } + + function isWetland(moisture, temperature, height) { + if (temperature <= -2) return false; // too cold + if (moisture > 40 && height < 25) return true; // near coast + if (moisture > 24 && height > 24 && height < 60) return true; // off coast + return false; + } + + return {getDefault, define, getId}; +})(); diff --git a/modules/io/load.js b/modules/io/load.js index 7b74da08..ff4792ad 100644 --- a/modules/io/load.js +++ b/modules/io/load.js @@ -257,7 +257,7 @@ async function parseLoadedData(data) { } const biomes = data[3].split("|"); - biomesData = applyDefaultBiomesSystem(); + biomesData = Biomes.getDefault(); biomesData.color = biomes[0].split(","); biomesData.habitability = biomes[1].split(",").map(h => +h); biomesData.name = biomes[2].split(","); diff --git a/modules/submap.js b/modules/submap.js index 41cdf74b..0544ba71 100644 --- a/modules/submap.js +++ b/modules/submap.js @@ -215,7 +215,7 @@ window.Submap = (function () { // biome calculation based on (resampled) grid.cells.temp and prec // it's safe to recalculate. stage("Regenerating Biome."); - defineBiomes(); + Biomes.define(); // recalculate suitability and population // TODO: normalize according to the base-map rankCells(); diff --git a/modules/ui/biomes-editor.js b/modules/ui/biomes-editor.js index 0cfc5ee2..bcb6c206 100644 --- a/modules/ui/biomes-editor.js +++ b/modules/ui/biomes-editor.js @@ -88,7 +88,9 @@ function editBiomes() { const rural = b.rural[i] * populationRate; const urban = b.urban[i] * populationRate * urbanization; const population = rn(rural + urban); - const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`; + const populationTip = `Total population: ${si(population)}; Rural population: ${si( + rural + )}; Urban population: ${si(urban)}`; totalArea += area; totalPopulation += population; @@ -104,7 +106,9 @@ function editBiomes() { data-color=${b.color[i]} > - + %
${si(population)}
- ${i > 12 && !b.cells[i] ? '' : ""} + ${ + i > 12 && !b.cells[i] + ? '' + : "" + } `; } @@ -403,7 +411,14 @@ function editBiomes() { // change of append new element if (exists.size()) exists.attr("data-biome", biomeNew).attr("fill", color).attr("stroke", color); - else temp.append("polygon").attr("data-cell", i).attr("data-biome", biomeNew).attr("points", getPackPolygon(i)).attr("fill", color).attr("stroke", color); + else + temp + .append("polygon") + .attr("data-cell", i) + .attr("data-biome", biomeNew) + .attr("points", getPackPolygon(i)) + .attr("fill", color) + .attr("stroke", color); }); } @@ -449,8 +464,8 @@ function editBiomes() { } function restoreInitialBiomes() { - biomesData = applyDefaultBiomesSystem(); - defineBiomes(); + biomesData = Biomes.getDefault(); + Biomes.define(); drawBiomes(); recalculatePopulation(); refreshBiomesEditor(); diff --git a/modules/ui/heightmap-editor.js b/modules/ui/heightmap-editor.js index 44be3fa0..bde67df7 100644 --- a/modules/ui/heightmap-editor.js +++ b/modules/ui/heightmap-editor.js @@ -239,7 +239,7 @@ function editHeightmap(options) { drawRivers(); Lakes.defineGroup(); - defineBiomes(); + Biomes.define(); rankCells(); Cultures.generate(); @@ -373,10 +373,6 @@ function editHeightmap(options) { const g = pack.cells.g[i]; const isLand = pack.cells.h[i] >= 20; - // check biome - pack.cells.biome[i] = - isLand && biome[g] ? biome[g] : getBiomeId(grid.cells.prec[g], grid.cells.temp[g], pack.cells.h[i]); - // rivers data if (!erosionAllowed) { pack.cells.r[i] = r[g]; @@ -384,6 +380,12 @@ function editHeightmap(options) { pack.cells.fl[i] = fl[g]; } + // check biome + pack.cells.biome[i] = + isLand && biome[g] + ? biome[g] + : Biomes.getId(grid.cells.prec[g], grid.cells.temp[g], pack.cells.h[i], Boolean(pack.cells.r[i])); + if (!isLand) continue; pack.cells.culture[i] = culture[g]; pack.cells.pop[i] = pop[g]; diff --git a/modules/ui/world-configurator.js b/modules/ui/world-configurator.js index ae222a59..c47beaca 100644 --- a/modules/ui/world-configurator.js +++ b/modules/ui/world-configurator.js @@ -85,7 +85,7 @@ function editWorld() { Lakes.defineGroup(); Rivers.specify(); pack.cells.h = new Float32Array(heights); - defineBiomes(); + Biomes.define(); if (layerIsOn("toggleTemp")) drawTemp(); if (layerIsOn("togglePrec")) drawPrec(); diff --git a/versioning.js b/versioning.js index d6b17ab3..ab4fb4e1 100644 --- a/versioning.js +++ b/versioning.js @@ -1,7 +1,7 @@ "use strict"; // version and caching control -const version = "1.91.04"; // generator version, update each time +const version = "1.91.05"; // generator version, update each time { document.title += " v" + version;