mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-23 12:31:24 +01:00
Merge branch 'master' into dev-submaps
This commit is contained in:
commit
0a7218db99
24 changed files with 904 additions and 515 deletions
209
main.js
209
main.js
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue