mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-24 13:01:24 +01:00
merge completed... now to fix all the bugs...
This commit is contained in:
commit
87c4d80fbc
3472 changed files with 466748 additions and 6517 deletions
602
main.js
602
main.js
|
|
@ -2,11 +2,11 @@
|
|||
// https://github.com/Azgaar/Fantasy-Map-Generator
|
||||
|
||||
'use strict';
|
||||
const version = '1.652'; // generator version
|
||||
const version = '1.71'; // generator version
|
||||
document.title += ' v' + version;
|
||||
|
||||
// Logging constants
|
||||
const PRODUCTION = window.location.host;
|
||||
const PRODUCTION = location.hostname && location.hostname !== "localhost" && location.hostname !== "127.0.0.1";
|
||||
const DEBUG = localStorage.getItem('debug');
|
||||
const INFO = DEBUG || !PRODUCTION;
|
||||
const TIME = DEBUG || !PRODUCTION;
|
||||
|
|
@ -112,18 +112,17 @@ legend.on('mousemove', () => tip('Drag to change the position. Click to hide the
|
|||
// main data variables
|
||||
let grid = {}; // initial grapg based on jittered square grid and data
|
||||
let pack = {}; // packed graph and data
|
||||
let seed,
|
||||
mapId,
|
||||
mapHistory = [],
|
||||
elSelected,
|
||||
modules = {},
|
||||
notes = [];
|
||||
let seed;
|
||||
let mapId;
|
||||
let mapHistory = [];
|
||||
let elSelected;
|
||||
let modules = {};
|
||||
let notes = [];
|
||||
let rulers = new Rulers();
|
||||
let customization = 0; // 0 - no; 1 = heightmap draw; 2 - states draw; 3 - add state/burg; 4 - cultures draw
|
||||
let customization = 0;
|
||||
|
||||
let biomesData = applyDefaultBiomesSystem();
|
||||
let nameBases = Names.getNameBases(); // cultures-related data
|
||||
const fonts = ['Almendra+SC', 'Georgia', 'Arial', 'Times+New+Roman', 'Comic+Sans+MS', 'Lucida+Sans+Unicode', 'Courier+New', 'Verdana', 'Arial', 'Impact']; // default fonts
|
||||
|
||||
let color = d3.scaleSequential(d3.interpolateSpectral); // default color scheme
|
||||
const lineGen = d3.line().curve(d3.curveBasis); // d3 line generator with default curve interpolation
|
||||
|
|
@ -149,28 +148,36 @@ function zoomed() {
|
|||
const zoom = d3.zoom().scaleExtent([1, 20]).on('zoom', zoomed);
|
||||
|
||||
// default options
|
||||
let options = {pinNotes: false}; // options object
|
||||
let options = {
|
||||
pinNotes: false,
|
||||
showMFCGMap: true,
|
||||
winds: [225, 45, 225, 315, 135, 315],
|
||||
stateLabelsMode: "auto"
|
||||
};
|
||||
let mapCoordinates = {}; // map coordinates on globe
|
||||
options.winds = [225, 45, 225, 315, 135, 315]; // default wind directions
|
||||
|
||||
let populationRate = +document.getElementById('populationRateInput').value;
|
||||
let urbanization = +document.getElementById('urbanizationInput').value;
|
||||
let urbanDensity = +document.getElementById("urbanDensityInput").value;
|
||||
|
||||
applyStoredOptions();
|
||||
let graphWidth = +mapWidthInput.value,
|
||||
graphHeight = +mapHeightInput.value; // voronoi graph extention, cannot be changed arter generation
|
||||
let svgWidth = graphWidth,
|
||||
svgHeight = graphHeight; // svg canvas resolution, can be changed
|
||||
|
||||
// voronoi graph extention, cannot be changed arter generation
|
||||
let graphWidth = +mapWidthInput.value;
|
||||
let graphHeight = +mapHeightInput.value;
|
||||
landmass.append('rect').attr('x', 0).attr('y', 0).attr('width', graphWidth).attr('height', graphHeight);
|
||||
oceanPattern.append('rect').attr('fill', 'url(#oceanic)').attr('x', 0).attr('y', 0).attr('width', graphWidth).attr('height', graphHeight);
|
||||
oceanLayers.append('rect').attr('id', 'oceanBase').attr('x', 0).attr('y', 0).attr('width', graphWidth).attr('height', graphHeight);
|
||||
|
||||
void (function removeLoading() {
|
||||
// svg canvas resolution, can be changed
|
||||
let svgWidth = graphWidth;
|
||||
let svgHeight = graphHeight;
|
||||
|
||||
|
||||
// remove loading screen
|
||||
d3.select('#loading').transition().duration(4000).style('opacity', 0).remove();
|
||||
d3.select('#initial').transition().duration(4000).attr('opacity', 0).remove();
|
||||
d3.select('#optionsContainer').transition().duration(3000).style('opacity', 1);
|
||||
d3.select('#tooltip').transition().duration(4000).style('opacity', 1);
|
||||
})();
|
||||
|
||||
// decide which map should be loaded or generated on page load
|
||||
void (function checkLoadParameters() {
|
||||
|
|
@ -219,39 +226,8 @@ 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
|
||||
applyStyleOnLoad(); // apply default or previously selected style
|
||||
generate(); // generate map
|
||||
focusOn(); // based on searchParams focus on point, cell or burg from MFCG
|
||||
applyPreset(); // apply saved layers preset
|
||||
|
|
@ -262,10 +238,11 @@ function focusOn() {
|
|||
const url = new URL(window.location.href);
|
||||
const params = url.searchParams;
|
||||
|
||||
if (params.get('from') === 'MFCG' && document.referrer) {
|
||||
if (params.get('seed').length === 13) {
|
||||
const fromMGCG = params.get("from") === "MFCG" && document.referrer;
|
||||
if (fromMGCG) {
|
||||
// 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);
|
||||
|
|
@ -273,23 +250,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
|
||||
|
|
@ -334,7 +321,6 @@ function findBurgForMFCG(params) {
|
|||
else if (p[0] === 'shantytown') b.shanty = +p[1];
|
||||
else b[p[0]] = +p[1]; // other parameters
|
||||
}
|
||||
b.MFCGlink = document.referrer; // set direct link to MFCG
|
||||
if (params.get('name') && params.get('name') != 'null') b.name = params.get('name');
|
||||
|
||||
const label = burgLabels.select("[data-id='" + burgId + "']");
|
||||
|
|
@ -421,13 +407,19 @@ 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>Ability to limit military units by biome, state, culture and religion</li>
|
||||
<li>New marker types</li>
|
||||
<li>New markers editor</li>
|
||||
<li>Markers overview screen</li>
|
||||
<li>Markers regeneration menu</li>
|
||||
<li>Burg editor update</li>
|
||||
<li>Editable theme color</li>
|
||||
<li>Add font dialog</li>
|
||||
<li>Save to Dropbox</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>
|
||||
<span>Thanks for all supporters on ${patreon}!</i></span>`;
|
||||
<span>Thanks for all supporters on <a href="https://www.patreon.com/azgaar" target="_blank">Patreon</a>!</i></span>`;
|
||||
|
||||
$('#alert').dialog({
|
||||
resizable: false,
|
||||
|
|
@ -479,10 +471,8 @@ function resetZoom(d = 1000) {
|
|||
svg.transition().duration(d).call(zoom.transform, d3.zoomIdentity);
|
||||
}
|
||||
|
||||
// calculate x,y extreme points of viewBox
|
||||
// calculate x y extreme points of viewBox
|
||||
function getViewBoxExtent() {
|
||||
// x = trX / scale * -1 + graphWidth / scale
|
||||
// y = trY / scale * -1 + graphHeight / scale
|
||||
return [
|
||||
[Math.abs(viewX / scale), Math.abs(viewY / scale)],
|
||||
[Math.abs(viewX / scale) + graphWidth / scale, Math.abs(viewY / scale) + graphHeight / scale]
|
||||
|
|
@ -536,19 +526,20 @@ function invokeActiveZooming() {
|
|||
}
|
||||
|
||||
// rescale map markers
|
||||
+markers.attr("rescale") &&
|
||||
|
||||
if (+markers.attr('rescale') && markers.style('display') !== 'none') {
|
||||
markers.selectAll('use').each(function () {
|
||||
const x = +this.dataset.x,
|
||||
y = +this.dataset.y,
|
||||
desired = +this.dataset.size;
|
||||
const size = Math.max(desired * 5 + 25 / scale, 1);
|
||||
d3.select(this)
|
||||
.attr('x', x - size / 2)
|
||||
.attr('y', y - size)
|
||||
.attr('width', size)
|
||||
.attr('height', size);
|
||||
pack.markers?.forEach(marker => {
|
||||
const {i, x, y, size = 30, hidden} = marker;
|
||||
const el = !hidden && document.getElementById(`marker${i}`);
|
||||
if (!el) return;
|
||||
|
||||
const zoomedSize = Math.max(rn(size / 5 + 24 / scale, 2), 1);
|
||||
el.setAttribute("width", zoomedSize);
|
||||
el.setAttribute("height", zoomedSize);
|
||||
el.setAttribute("x", rn(x - zoomedSize / 2, 1));
|
||||
el.setAttribute("y", rn(y - zoomedSize, 1));
|
||||
});
|
||||
}
|
||||
|
||||
// rescale rulers to have always the same size
|
||||
if (ruler.style('display') !== 'none') {
|
||||
|
|
@ -678,7 +669,7 @@ function generate() {
|
|||
Lakes.generateName();
|
||||
|
||||
Military.generate();
|
||||
addMarkers();
|
||||
Markers.generate();
|
||||
addZones();
|
||||
Names.getMapName();
|
||||
|
||||
|
|
@ -687,11 +678,12 @@ function generate() {
|
|||
INFO && console.groupEnd('Generated Map ' + seed);
|
||||
} catch (error) {
|
||||
ERROR && console.error(error);
|
||||
const parsedError = parseError(error);
|
||||
clearMainTip();
|
||||
|
||||
alertMessage.innerHTML = `An error is occured on map generation. Please retry.
|
||||
<br>If error is critical, clear the stored data and try again.
|
||||
<p id="errorBox">${parseError(error)}</p>`;
|
||||
<p id="errorBox">${parsedError}</p>`;
|
||||
$('#alert').dialog({
|
||||
resizable: false,
|
||||
title: 'Generation error',
|
||||
|
|
@ -702,7 +694,7 @@ function generate() {
|
|||
localStorage.setItem('version', version);
|
||||
},
|
||||
Regenerate: function () {
|
||||
regenerateMap();
|
||||
regenerateMap("generation error");
|
||||
$(this).dialog('close');
|
||||
},
|
||||
Ignore: function () {
|
||||
|
|
@ -731,6 +723,7 @@ function generateSeed() {
|
|||
// Place points to calculate Voronoi diagram
|
||||
function placePoints() {
|
||||
TIME && console.time('placePoints');
|
||||
Math.random = aleaPRNG(seed); // reset PRNG
|
||||
|
||||
const cellsDesired = +pointsInput.dataset.cells;
|
||||
const spacing = (grid.spacing = rn(Math.sqrt((graphWidth * graphHeight) / cellsDesired), 2)); // spacing between points before jirrering
|
||||
|
|
@ -955,11 +948,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};
|
||||
}
|
||||
|
||||
|
|
@ -979,7 +972,7 @@ function calculateTemperatures() {
|
|||
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] = Math.max(Math.min(initTemp - convertToFriendly(cells.h[i]), 127), -128);
|
||||
cells.temp[i] = minmax(initTemp - convertToFriendly(cells.h[i]), -128, 127);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -998,43 +991,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;
|
||||
|
|
@ -1042,6 +1037,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];
|
||||
|
|
@ -1049,20 +1045,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 {
|
||||
|
|
@ -1073,20 +1083,20 @@ 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 ? minmax(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
|
||||
return Math.min(Math.max(normalLoss + diff * mod, 1), humidity);
|
||||
return minmax(normalLoss + diff * mod, 1, humidity);
|
||||
}
|
||||
|
||||
void (function drawWindDirection() {
|
||||
|
|
@ -1400,22 +1410,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]])
|
||||
|
|
@ -1428,12 +1437,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 rang cells for culture center and burgs placement
|
||||
|
|
@ -1486,295 +1496,6 @@ function rankCells() {
|
|||
TIME && console.timeEnd('rankCells');
|
||||
}
|
||||
|
||||
// generate some markers
|
||||
function addMarkers(number = 1) {
|
||||
if (!number) return;
|
||||
TIME && console.time('addMarkers');
|
||||
const cells = pack.cells,
|
||||
states = pack.states;
|
||||
|
||||
void (function addVolcanoes() {
|
||||
let mounts = Array.from(cells.i)
|
||||
.filter((i) => cells.h[i] > 70)
|
||||
.sort((a, b) => cells.h[b] - cells.h[a]);
|
||||
let count = mounts.length < 10 ? 0 : Math.ceil((mounts.length / 300) * number);
|
||||
if (count) addMarker('volcano', '🌋', 52, 50, 13);
|
||||
|
||||
while (count && mounts.length) {
|
||||
const cell = mounts.splice(biased(0, mounts.length - 1, 5), 1);
|
||||
const x = cells.p[cell][0],
|
||||
y = cells.p[cell][1];
|
||||
const id = appendMarker(cell, 'volcano');
|
||||
const proper = Names.getCulture(cells.culture[cell]);
|
||||
const name = P(0.3) ? 'Mount ' + proper : Math.random() > 0.3 ? proper + ' Volcano' : proper;
|
||||
notes.push({id, name, legend: `Active volcano. Height: ${getFriendlyHeight([x, y])}`});
|
||||
count--;
|
||||
}
|
||||
})();
|
||||
|
||||
void (function addHotSprings() {
|
||||
let springs = Array.from(cells.i)
|
||||
.filter((i) => cells.h[i] > 50)
|
||||
.sort((a, b) => cells.h[b] - cells.h[a]);
|
||||
let count = springs.length < 30 ? 0 : Math.ceil((springs.length / 1000) * number);
|
||||
if (count) addMarker('hot_springs', '♨️', 50, 52, 12.5);
|
||||
|
||||
while (count && springs.length) {
|
||||
const cell = springs.splice(biased(1, springs.length - 1, 3), 1);
|
||||
const id = appendMarker(cell, 'hot_springs');
|
||||
const proper = Names.getCulture(cells.culture[cell]);
|
||||
const temp = convertTemperature(gauss(30, 15, 20, 100));
|
||||
notes.push({id, name: proper + ' Hot Springs', legend: `A hot springs area. Temperature: ${temp}`});
|
||||
count--;
|
||||
}
|
||||
})();
|
||||
|
||||
void (function addMines() {
|
||||
let hills = Array.from(cells.i).filter((i) => cells.h[i] > 47 && cells.burg[i]);
|
||||
let count = !hills.length ? 0 : Math.ceil((hills.length / 7) * number);
|
||||
if (!count) return;
|
||||
|
||||
addMarker('mine', '⛏️', 48, 50, 13.5);
|
||||
const resources = {salt: 5, gold: 2, silver: 4, copper: 2, iron: 3, lead: 1, tin: 1};
|
||||
|
||||
while (count && hills.length) {
|
||||
const cell = hills.splice(Math.floor(Math.random() * hills.length), 1);
|
||||
const id = appendMarker(cell, 'mine');
|
||||
const resource = rw(resources);
|
||||
const burg = pack.burgs[cells.burg[cell]];
|
||||
const name = `${burg.name} — ${resource} mining town`;
|
||||
const population = rn(burg.population * populationRate * urbanization);
|
||||
const legend = `${burg.name} is a mining town of ${population} people just nearby the ${resource} mine`;
|
||||
notes.push({id, name, legend});
|
||||
count--;
|
||||
}
|
||||
})();
|
||||
|
||||
void (function addBridges() {
|
||||
const meanRoad = d3.mean(cells.road.filter((r) => r));
|
||||
const meanFlux = d3.mean(cells.fl.filter((fl) => fl));
|
||||
|
||||
let bridges = Array.from(cells.i)
|
||||
.filter((i) => cells.burg[i] && cells.h[i] >= 20 && cells.r[i] && cells.fl[i] > meanFlux && cells.road[i] > meanRoad)
|
||||
.sort((a, b) => cells.road[b] + cells.fl[b] / 10 - (cells.road[a] + cells.fl[a] / 10));
|
||||
|
||||
let count = !bridges.length ? 0 : Math.ceil((bridges.length / 12) * number);
|
||||
if (count) addMarker('bridge', '🌉', 50, 50, 14);
|
||||
|
||||
while (count && bridges.length) {
|
||||
const cell = bridges.splice(0, 1);
|
||||
const id = appendMarker(cell, 'bridge');
|
||||
const burg = pack.burgs[cells.burg[cell]];
|
||||
const river = pack.rivers.find((r) => r.i === pack.cells.r[cell]);
|
||||
const riverName = river ? `${river.name} ${river.type}` : 'river';
|
||||
const name = river && P(0.2) ? river.name : burg.name;
|
||||
notes.push({id, name: `${name} Bridge`, legend: `A stone bridge over the ${riverName} near ${burg.name}`});
|
||||
count--;
|
||||
}
|
||||
})();
|
||||
|
||||
void (function addInns() {
|
||||
const maxRoad = d3.max(cells.road) * 0.9;
|
||||
let taverns = Array.from(cells.i).filter((i) => cells.crossroad[i] && cells.h[i] >= 20 && cells.road[i] > maxRoad);
|
||||
if (!taverns.length) return;
|
||||
const count = Math.ceil(4 * number);
|
||||
addMarker('inn', '🍻', 50, 50, 14.5);
|
||||
|
||||
const color = ['Dark', 'Light', 'Bright', 'Golden', 'White', 'Black', 'Red', 'Pink', 'Purple', 'Blue', 'Green', 'Yellow', 'Amber', 'Orange', 'Brown', 'Grey'];
|
||||
const animal = [
|
||||
'Antelope',
|
||||
'Ape',
|
||||
'Badger',
|
||||
'Bear',
|
||||
'Beaver',
|
||||
'Bison',
|
||||
'Boar',
|
||||
'Buffalo',
|
||||
'Cat',
|
||||
'Crane',
|
||||
'Crocodile',
|
||||
'Crow',
|
||||
'Deer',
|
||||
'Dog',
|
||||
'Eagle',
|
||||
'Elk',
|
||||
'Fox',
|
||||
'Goat',
|
||||
'Goose',
|
||||
'Hare',
|
||||
'Hawk',
|
||||
'Heron',
|
||||
'Horse',
|
||||
'Hyena',
|
||||
'Ibis',
|
||||
'Jackal',
|
||||
'Jaguar',
|
||||
'Lark',
|
||||
'Leopard',
|
||||
'Lion',
|
||||
'Mantis',
|
||||
'Marten',
|
||||
'Moose',
|
||||
'Mule',
|
||||
'Narwhal',
|
||||
'Owl',
|
||||
'Panther',
|
||||
'Rat',
|
||||
'Raven',
|
||||
'Rook',
|
||||
'Scorpion',
|
||||
'Shark',
|
||||
'Sheep',
|
||||
'Snake',
|
||||
'Spider',
|
||||
'Swan',
|
||||
'Tiger',
|
||||
'Turtle',
|
||||
'Wolf',
|
||||
'Wolverine',
|
||||
'Camel',
|
||||
'Falcon',
|
||||
'Hound',
|
||||
'Ox'
|
||||
];
|
||||
const adj = [
|
||||
'New',
|
||||
'Good',
|
||||
'High',
|
||||
'Old',
|
||||
'Great',
|
||||
'Big',
|
||||
'Major',
|
||||
'Happy',
|
||||
'Main',
|
||||
'Huge',
|
||||
'Far',
|
||||
'Beautiful',
|
||||
'Fair',
|
||||
'Prime',
|
||||
'Ancient',
|
||||
'Golden',
|
||||
'Proud',
|
||||
'Lucky',
|
||||
'Fat',
|
||||
'Honest',
|
||||
'Giant',
|
||||
'Distant',
|
||||
'Friendly',
|
||||
'Loud',
|
||||
'Hungry',
|
||||
'Magical',
|
||||
'Superior',
|
||||
'Peaceful',
|
||||
'Frozen',
|
||||
'Divine',
|
||||
'Favorable',
|
||||
'Brave',
|
||||
'Sunny',
|
||||
'Flying'
|
||||
];
|
||||
|
||||
for (let i = 0; i < taverns.length && i < count; i++) {
|
||||
const cell = taverns.splice(Math.floor(Math.random() * taverns.length), 1);
|
||||
const id = appendMarker(cell, 'inn');
|
||||
const type = P(0.3) ? 'inn' : 'tavern';
|
||||
const name = P(0.5) ? ra(color) + ' ' + ra(animal) : P(0.6) ? ra(adj) + ' ' + ra(animal) : ra(adj) + ' ' + capitalize(type);
|
||||
notes.push({id, name: 'The ' + name, legend: `A big and famous roadside ${type}`});
|
||||
}
|
||||
})();
|
||||
|
||||
void (function addLighthouses() {
|
||||
const lands = cells.i.filter((i) => cells.harbor[i] > 6 && cells.c[i].some((c) => cells.h[c] < 20 && cells.road[c]));
|
||||
const lighthouses = Array.from(lands).map((i) => [i, cells.v[i][cells.c[i].findIndex((c) => cells.h[c] < 20 && cells.road[c])]]);
|
||||
if (lighthouses.length) addMarker('lighthouse', '🚨', 50, 50, 16);
|
||||
const count = Math.ceil(4 * number);
|
||||
|
||||
for (let i = 0; i < lighthouses.length && i < count; i++) {
|
||||
const cell = lighthouses[i][0],
|
||||
vertex = lighthouses[i][1];
|
||||
const id = appendMarker(cell, 'lighthouse');
|
||||
const proper = cells.burg[cell] ? pack.burgs[cells.burg[cell]].name : Names.getCulture(cells.culture[cell]);
|
||||
notes.push({id, name: getAdjective(proper) + ' Lighthouse' + name, legend: `A lighthouse to keep the navigation safe`});
|
||||
}
|
||||
})();
|
||||
|
||||
void (function addWaterfalls() {
|
||||
const waterfalls = cells.i.filter((i) => cells.r[i] && cells.h[i] > 70);
|
||||
if (waterfalls.length) addMarker('waterfall', '⟱', 50, 54, 16.5);
|
||||
const count = Math.ceil(3 * number);
|
||||
|
||||
for (let i = 0; i < waterfalls.length && i < count; i++) {
|
||||
const cell = waterfalls[i];
|
||||
const id = appendMarker(cell, 'waterfall');
|
||||
const proper = cells.burg[cell] ? pack.burgs[cells.burg[cell]].name : Names.getCulture(cells.culture[cell]);
|
||||
notes.push({id, name: getAdjective(proper) + ' Waterfall' + name, legend: `An extremely beautiful waterfall`});
|
||||
}
|
||||
})();
|
||||
|
||||
void (function addBattlefields() {
|
||||
let battlefields = Array.from(cells.i).filter((i) => cells.state[i] && cells.pop[i] > 2 && cells.h[i] < 50 && cells.h[i] > 25);
|
||||
let count = battlefields.length < 100 ? 0 : Math.ceil((battlefields.length / 500) * number);
|
||||
if (count) addMarker('battlefield', '⚔️', 50, 52, 12);
|
||||
|
||||
while (count && battlefields.length) {
|
||||
const cell = battlefields.splice(Math.floor(Math.random() * battlefields.length), 1);
|
||||
const id = appendMarker(cell, 'battlefield');
|
||||
const campaign = ra(states[cells.state[cell]].campaigns);
|
||||
const date = generateDate(campaign.start, campaign.end);
|
||||
const name = Names.getCulture(cells.culture[cell]) + ' Battlefield';
|
||||
const legend = `A historical battle of the ${campaign.name}. \r\nDate: ${date} ${options.era}`;
|
||||
notes.push({id, name, legend});
|
||||
count--;
|
||||
}
|
||||
})();
|
||||
|
||||
function addMarker(id, icon, x, y, size) {
|
||||
const markers = svg.select('#defs-markers');
|
||||
if (markers.select('#marker_' + id).size()) return;
|
||||
|
||||
const symbol = markers
|
||||
.append('symbol')
|
||||
.attr('id', 'marker_' + id)
|
||||
.attr('viewBox', '0 0 30 30');
|
||||
symbol.append('path').attr('d', 'M6,19 l9,10 L24,19').attr('fill', '#000000').attr('stroke', 'none');
|
||||
symbol.append('circle').attr('cx', 15).attr('cy', 15).attr('r', 10).attr('fill', '#ffffff').attr('stroke', '#000000').attr('stroke-width', 1);
|
||||
symbol
|
||||
.append('text')
|
||||
.attr('x', x + '%')
|
||||
.attr('y', y + '%')
|
||||
.attr('fill', '#000000')
|
||||
.attr('stroke', '#3200ff')
|
||||
.attr('stroke-width', 0)
|
||||
.attr('font-size', size + 'px')
|
||||
.attr('dominant-baseline', 'central')
|
||||
.text(icon);
|
||||
}
|
||||
|
||||
function appendMarker(cell, type) {
|
||||
const x = cells.p[cell][0],
|
||||
y = cells.p[cell][1];
|
||||
const id = getNextId('markerElement');
|
||||
const name = '#marker_' + type;
|
||||
|
||||
markers
|
||||
.append('use')
|
||||
.attr('id', id)
|
||||
.attr('xlink:href', name)
|
||||
.attr('data-id', name)
|
||||
.attr('data-x', x)
|
||||
.attr('data-y', y)
|
||||
.attr('x', x - 15)
|
||||
.attr('y', y - 30)
|
||||
.attr('data-size', 1)
|
||||
.attr('width', 30)
|
||||
.attr('height', 30);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
TIME && console.timeEnd('addMarkers');
|
||||
}
|
||||
|
||||
// regenerate some zones
|
||||
function addZones(number = 1) {
|
||||
TIME && console.time('addZones');
|
||||
|
|
@ -1823,9 +1544,18 @@ function addZones(number = 1) {
|
|||
});
|
||||
}
|
||||
|
||||
const invasion = rw({Invasion: 4, Occupation: 3, Raid: 2, Conquest: 2, Subjugation: 1, Foray: 1, Skirmishes: 1, Incursion: 2, Pillaging: 1, Intervention: 1});
|
||||
const name = getAdjective(invader.name) + ' ' + invasion;
|
||||
data.push({name, type: 'Invasion', cells: cellsArray, fill: 'url(#hatch1)'});
|
||||
const invasion = rw({
|
||||
Invasion: 4,
|
||||
Occupation: 3,
|
||||
Raid: 2,
|
||||
Conquest: 2,
|
||||
Subjugation: 1,
|
||||
Foray: 1,
|
||||
Skirmishes: 1,
|
||||
Incursion: 2,
|
||||
Pillaging: 1,
|
||||
Intervention: 1
|
||||
});
|
||||
}
|
||||
|
||||
function addRebels() {
|
||||
|
|
@ -2133,7 +1863,7 @@ function addZones(number = 1) {
|
|||
|
||||
// show map stats on generation complete
|
||||
function showStatistics() {
|
||||
const template = templateInput.value;
|
||||
const template = templateInput.options[templateInput.selectedIndex].text;
|
||||
const templateRandom = locked('template') ? '' : '(random)';
|
||||
const stats = ` Seed: ${seed}
|
||||
Canvas size: ${graphWidth}x${graphHeight}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue