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]}
>