Merge branch 'master' into dev-submaps

This commit is contained in:
Mészáros Gergely 2021-09-08 12:43:23 +02:00
commit 0a7218db99
24 changed files with 904 additions and 515 deletions

209
main.js
View file

@ -2,7 +2,7 @@
// https://github.com/Azgaar/Fantasy-Map-Generator
"use strict";
const version = "1.652"; // generator version1
const version = "1.66"; // generator version1
document.title += " v" + version;
// Switches to disable/enable logging features
@ -220,37 +220,6 @@ void (function checkLoadParameters() {
generateMapOnLoad();
})();
function loadMapFromURL(maplink, random) {
const URL = decodeURIComponent(maplink);
fetch(URL, {method: "GET", mode: "cors"})
.then(response => {
if (response.ok) return response.blob();
throw new Error("Cannot load map from URL");
})
.then(blob => uploadMap(blob))
.catch(error => {
showUploadErrorMessage(error.message, URL, random);
if (random) generateMapOnLoad();
});
}
function showUploadErrorMessage(error, URL, random) {
ERROR && console.error(error);
alertMessage.innerHTML = `Cannot load map from the ${link(URL, "link provided")}.
${random ? `A new random map is generated. ` : ""}
Please ensure the linked file is reachable and CORS is allowed on server side`;
$("#alert").dialog({
title: "Loading error",
width: "32em",
buttons: {
OK: function () {
$(this).dialog("close");
}
}
});
}
function generateMapOnLoad() {
applyStyleOnLoad(); // apply default of previously selected style
generate(); // generate map
@ -263,10 +232,12 @@ function focusOn() {
const url = new URL(window.location.href);
const params = url.searchParams;
if (params.get("from") === "MFCG" && document.referrer) {
const fromMGCG = params.get("from") === "MFCG" && document.referrer;
if (fromMGCG) {
if (params.get("seed").length === 13) {
// show back burg from MFCG
params.set("burg", params.get("seed").slice(-4));
const burgSeed = params.get("seed").slice(-4);
params.set("burg", burgSeed);
} else {
// select burg for MFCG
findBurgForMFCG(params);
@ -274,23 +245,33 @@ function focusOn() {
}
}
const s = +params.get("scale") || 8;
let x = +params.get("x");
let y = +params.get("y");
const scaleParam = params.get("scale");
const cellParam = params.get("cell");
const burgParam = params.get("burg");
const c = +params.get("cell");
if (c) {
x = pack.cells.p[c][0];
y = pack.cells.p[c][1];
if (scaleParam || cellParam || burgParam) {
const scale = +scaleParam || 8;
if (cellParam) {
const cell = +params.get("cell");
const [x, y] = pack.cells.p[cell];
zoomTo(x, y, scale, 1600);
return;
}
if (burgParam) {
const burg = isNaN(+burgParam) ? pack.burgs.find(burg => burg.name === burgParam) : pack.burgs[+burgParam];
if (!burg) return;
const {x, y} = burg;
zoomTo(x, y, scale, 1600);
return;
}
const x = +params.get("x") || graphWidth / 2;
const y = +params.get("y") || graphHeight / 2;
zoomTo(x, y, scale, 1600);
}
const b = +params.get("burg");
if (b && pack.burgs[b]) {
x = pack.burgs[b].x;
y = pack.burgs[b].y;
}
if (x && y) zoomTo(x, y, s, 1600);
}
// find burg for MFCG and focus on it
@ -390,7 +371,7 @@ function applyDefaultBiomesSystem() {
}
function showWelcomeMessage() {
const changelog = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "previous version");
const changelog = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "previous versions");
const reddit = link("https://www.reddit.com/r/FantasyMapGenerator", "Reddit community");
const discord = link("https://discordapp.com/invite/X7E84HU", "Discord server");
const patreon = link("https://www.patreon.com/azgaar", "Patreon");
@ -398,9 +379,10 @@ function showWelcomeMessage() {
alertMessage.innerHTML = `The Fantasy Map Generator is updated up to version <b>${version}</b>.
This version is compatible with ${changelog}, loaded <i>.map</i> files will be auto-updated.
<ul>Main changes:
<li>Ability to add river selecting its cells</li>
<li>Keep river course on edit</li>
<li>Refactor river rendering code</li>
<li>Save and load <i>.map</i> files to Dropbox</li>
<li>Ability to add control points on river edit</li>
<li>New heightmap template: Taklamakan</li>
<li>Option to not scale labels on zoom</li>
</ul>
<p>Join our ${discord} and ${reddit} to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.</p>
@ -882,8 +864,8 @@ function openNearSeaLakes() {
function defineMapSize() {
const [size, latitude] = getSizeAndLatitude();
const randomize = new URL(window.location.href).searchParams.get("options") === "default"; // ignore stored options
if (randomize || !locked("mapSize")) mapSizeOutput.value = mapSizeInput.value = size;
if (randomize || !locked("latitude")) latitudeOutput.value = latitudeInput.value = latitude;
if (randomize || !locked("mapSize")) mapSizeOutput.value = mapSizeInput.value = rn(size);
if (randomize || !locked("latitude")) latitudeOutput.value = latitudeInput.value = rn(latitude);
function getSizeAndLatitude() {
const template = document.getElementById("templateInput").value; // heightmap template
@ -916,11 +898,11 @@ function calculateMapCoordinates() {
const size = +document.getElementById("mapSizeOutput").value;
const latShift = +document.getElementById("latitudeOutput").value;
const latT = (size / 100) * 180;
const latN = 90 - ((180 - latT) * latShift) / 100;
const latS = latN - latT;
const latT = rn((size / 100) * 180, 1);
const latN = rn(90 - ((180 - latT) * latShift) / 100, 1);
const latS = rn(latN - latT, 1);
const lon = Math.min(((graphWidth / graphHeight) * latT) / 2, 180);
const lon = rn(Math.min(((graphWidth / graphHeight) * latT) / 2, 180));
mapCoordinates = {latT, latN, latS, lonT: lon * 2, lonW: -lon, lonE: lon};
}
@ -959,43 +941,45 @@ function calculateTemperatures() {
function generatePrecipitation() {
TIME && console.time("generatePrecipitation");
prec.selectAll("*").remove();
const cells = grid.cells;
const {cells, cellsX, cellsY} = grid;
cells.prec = new Uint8Array(cells.i.length); // precipitation array
const modifier = precInput.value / 100; // user's input
const cellsX = grid.cellsX,
cellsY = grid.cellsY;
let westerly = [],
easterly = [],
southerly = 0,
northerly = 0;
{
// latitude bands
// x4 = 0-5 latitude: wet through the year (rising zone)
// x2 = 5-20 latitude: wet summer (rising zone), dry winter (sinking zone)
// x1 = 20-30 latitude: dry all year (sinking zone)
// x2 = 30-50 latitude: wet winter (rising zone), dry summer (sinking zone)
// x3 = 50-60 latitude: wet all year (rising zone)
// x2 = 60-70 latitude: wet summer (rising zone), dry winter (sinking zone)
// x1 = 70-90 latitude: dry all year (sinking zone)
}
const lalitudeModifier = [4, 2, 2, 2, 1, 1, 2, 2, 2, 2, 3, 3, 2, 2, 1, 1, 1, 0.5]; // by 5d step
const westerly = [];
const easterly = [];
let southerly = 0;
let northerly = 0;
// difine wind directions based on cells latitude and prevailing winds there
// precipitation modifier per latitude band
// x4 = 0-5 latitude: wet through the year (rising zone)
// x2 = 5-20 latitude: wet summer (rising zone), dry winter (sinking zone)
// x1 = 20-30 latitude: dry all year (sinking zone)
// x2 = 30-50 latitude: wet winter (rising zone), dry summer (sinking zone)
// x3 = 50-60 latitude: wet all year (rising zone)
// x2 = 60-70 latitude: wet summer (rising zone), dry winter (sinking zone)
// x1 = 70-85 latitude: dry all year (sinking zone)
// x0.5 = 85-90 latitude: dry all year (sinking zone)
const lalitudeModifier = [4, 2, 2, 2, 1, 1, 2, 2, 2, 2, 3, 3, 2, 2, 1, 1, 1, 0.5];
const MAX_PASSABLE_ELEVATION = 85;
// define wind directions based on cells latitude and prevailing winds there
d3.range(0, cells.i.length, cellsX).forEach(function (c, i) {
const lat = mapCoordinates.latN - (i / cellsY) * mapCoordinates.latT;
const band = ((Math.abs(lat) - 1) / 5) | 0;
const latMod = lalitudeModifier[band];
const tier = (Math.abs(lat - 89) / 30) | 0; // 30d tiers from 0 to 5 from N to S
if (options.winds[tier] > 40 && options.winds[tier] < 140) westerly.push([c, latMod, tier]);
else if (options.winds[tier] > 220 && options.winds[tier] < 320) easterly.push([c + cellsX - 1, latMod, tier]);
if (options.winds[tier] > 100 && options.winds[tier] < 260) northerly++;
else if (options.winds[tier] > 280 || options.winds[tier] < 80) southerly++;
const latBand = ((Math.abs(lat) - 1) / 5) | 0;
const latMod = lalitudeModifier[latBand];
const windTier = (Math.abs(lat - 89) / 30) | 0; // 30d tiers from 0 to 5 from N to S
const {isWest, isEast, isNorth, isSouth} = getWindDirections(windTier);
if (isWest) westerly.push([c, latMod, windTier]);
if (isEast) easterly.push([c + cellsX - 1, latMod, windTier]);
if (isNorth) northerly++;
if (isSouth) southerly++;
});
// distribute winds by direction
if (westerly.length) passWind(westerly, 120 * modifier, 1, cellsX);
if (easterly.length) passWind(easterly, 120 * modifier, -1, cellsX);
const vertT = southerly + northerly;
if (northerly) {
const bandN = ((Math.abs(mapCoordinates.latN) - 1) / 5) | 0;
@ -1003,6 +987,7 @@ function generatePrecipitation() {
const maxPrecN = (northerly / vertT) * 60 * modifier * latModN;
passWind(d3.range(0, cellsX, 1), maxPrecN, cellsX, cellsY);
}
if (southerly) {
const bandS = ((Math.abs(mapCoordinates.latS) - 1) / 5) | 0;
const latModS = mapCoordinates.latT > 60 ? d3.mean(lalitudeModifier) : lalitudeModifier[bandS];
@ -1010,20 +995,34 @@ function generatePrecipitation() {
passWind(d3.range(cells.i.length - cellsX, cells.i.length, 1), maxPrecS, -cellsX, cellsY);
}
function getWindDirections(tier) {
const angle = options.winds[tier];
const isWest = angle > 40 && angle < 140;
const isEast = angle > 220 && angle < 320;
const isNorth = angle > 100 && angle < 260;
const isSouth = angle > 280 || angle < 80;
return {isWest, isEast, isNorth, isSouth};
}
function passWind(source, maxPrec, next, steps) {
const maxPrecInit = maxPrec;
for (let first of source) {
if (first[0]) {
maxPrec = Math.min(maxPrecInit * first[1], 255);
first = first[0];
}
let humidity = maxPrec - cells.h[first]; // initial water amount
if (humidity <= 0) continue; // if first cell in row is too elevated cosdired wind dry
for (let s = 0, current = first; s < steps; s++, current += next) {
// no flux on permafrost
if (cells.temp[current] < -5) continue;
// water cell
if (cells.temp[current] < -5) continue; // no flux in permafrost
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
} else {
@ -1034,16 +1033,16 @@ function generatePrecipitation() {
}
// land cell
const precipitation = getPrecipitation(humidity, current, next);
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 = Math.min(Math.max(humidity - precipitation + evaporation, 0), maxPrec);
humidity = isPassable ? Math.min(Math.max(humidity - precipitation + evaporation, 0), maxPrec) : 0;
}
}
}
function getPrecipitation(humidity, i, n) {
if (cells.h[i + n] > 85) return humidity; // 85 is max passable height
const normalLoss = Math.max(humidity / (10 * modifier), 1); // precipitation in normal conditions
const diff = Math.max(cells.h[i + n] - cells.h[i], 0); // difference in height
const mod = (cells.h[i + n] / 70) ** 2; // 50 stands for hills, 70 for mountains
@ -1361,22 +1360,21 @@ function reMarkFeatures() {
// assign biome id for each cell
function defineBiomes() {
TIME && console.time("defineBiomes");
const cells = pack.cells,
f = pack.features,
temp = grid.cells.temp,
prec = grid.cells.prec;
const {cells} = pack;
const {temp, prec} = grid.cells;
cells.biome = new Uint8Array(cells.i.length); // biomes array
for (const i of cells.i) {
const t = temp[cells.g[i]]; // cell temperature
const h = cells.h[i]; // cell height
const m = h < 20 ? 0 : calculateMoisture(i); // cell moisture
cells.biome[i] = getBiomeId(m, t, h);
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]])
@ -1389,12 +1387,13 @@ function defineBiomes() {
// assign biome id to a cell
function getBiomeId(moisture, temperature, height) {
if (temperature < -5) return 11; // permafrost biome, including sea ice
if (height < 20) return 0; // marine biome: liquid water cells
if (moisture > 40 && temperature > -2 && (height < 25 || (moisture > 24 && height > 24))) return 12; // wetland biome
const m = Math.min((moisture / 5) | 0, 4); // moisture band from 0 to 4
const t = Math.min(Math.max(20 - temperature, 0), 25); // temparature band from 0 to 25
return biomesData.biomesMartix[m][t];
if (height < 20) return 0; // marine biome: all water cells
if (temperature < -5) return 11; // permafrost biome
if (moisture > 40 && temperature > -2 && (height < 25 || (moisture > 24 && height > 24 && height < 60))) 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