v. 0.59.07b

This commit is contained in:
Azgaar 2018-09-01 23:32:55 +03:00
parent f3e36dc455
commit d0687ed540
2 changed files with 185 additions and 133 deletions

View file

@ -32,8 +32,8 @@
<script src="libs/quantize.min.js" defer></script> <script src="libs/quantize.min.js" defer></script>
<script src="libs/d3-hexbin.v0.2.min.js" defer></script> <script src="libs/d3-hexbin.v0.2.min.js" defer></script>
<script src="libs/jquery.ui.touch-punch.min.js" defer></script> <script src="libs/jquery.ui.touch-punch.min.js" defer></script>
<link rel="stylesheet" type="text/css" href="index.css?version=0.59.01b"/> <link rel="stylesheet" type="text/css" href="index.css?version=0.59.07b"/>
<link rel="stylesheet" type="text/css" href="icons.css?version=0.59.01b"/> <link rel="stylesheet" type="text/css" href="icons.css?version=0.59.07b"/>
<link rel="stylesheet" type="text/css" href="libs/jquery-ui.css"/> <link rel="stylesheet" type="text/css" href="libs/jquery-ui.css"/>
</head> </head>
<body> <body>
@ -159,7 +159,7 @@
<div id="loading"> <div id="loading">
<div id="title_name">Azgaar's</div> <div id="title_name">Azgaar's</div>
<div id="title">Fantasy Map Generator</div> <div id="title">Fantasy Map Generator</div>
<div id="version">v. 0.58b</div> <div id="version">v. 0.59b</div>
<p id="loading-text">LOADING<span>.</span><span>.</span><span>.</span></p> <p id="loading-text">LOADING<span>.</span><span>.</span><span>.</span></p>
</div> </div>
<canvas id="canvas" style="opacity: 0"></canvas> <canvas id="canvas" style="opacity: 0"></canvas>
@ -205,8 +205,8 @@
<input id="toggleTooltips" class="checkbox" type="checkbox" checked onclick="$('#tooltip').fadeToggle()"> <input id="toggleTooltips" class="checkbox" type="checkbox" checked onclick="$('#tooltip').fadeToggle()">
<label for="toggleTooltips" onmouseover="tip('Toogle tooltip line')" class="checkbox-label">Show tooltips</label> <label for="toggleTooltips" onmouseover="tip('Toogle tooltip line')" class="checkbox-label">Show tooltips</label>
<label id="optionSeedLabel" style="margin-left:10px">Map seed: <label id="optionSeedLabel" style="margin-left:10px">Map seed:
<input id="optionsSeed" class="pureInput" style="width:65px" value=""> <input id="optionsSeed" class="pureInput" style="width:50px" value="">
<i onmouseover="tip('Click to generate a map for this seed')" id="optionsSeedGenerate" style="margin-left:-3px" class="icon-play"></i> <i onmouseover="tip('Click to generate a map for this seed')" id="optionsSeedGenerate" style="margin-left:-3px; color: #5d4651" class="icon-play"></i>
</label> </label>
</div> </div>
</div> </div>
@ -551,7 +551,7 @@
<div id="customizeOptions"> <div id="customizeOptions">
<input id="renderOcean" class="checkbox" type="checkbox"> <input id="renderOcean" class="checkbox" type="checkbox">
<label for="renderOcean" onmouseover="tip('Render cells below sea level')" class="checkbox-label">Render ocean cells</label> <label for="renderOcean" onmouseover="tip('Render cells below sea level')" class="checkbox-label">Render ocean cells</label>
<input id="changeHeights" class="checkbox" type="checkbox"> <input id="changeHeights" class="checkbox" type="checkbox" checked>
<label for="changeHeights" onmouseover="tip('Allow system to change custom heights if reasonable')" class="checkbox-label">Change heights</label> <label for="changeHeights" onmouseover="tip('Allow system to change custom heights if reasonable')" class="checkbox-label">Change heights</label>
</div> </div>
<label onmouseover="tip('Number of Land cells and landmass/ocean ratio')">Landmass: <label onmouseover="tip('Number of Land cells and landmass/ocean ratio')">Landmass:
@ -1143,5 +1143,5 @@
<input type="file" accept=".txt" id="namesbaseToLoad"> <input type="file" accept=".txt" id="namesbaseToLoad">
</div> </div>
<script src="script.js?version=0.59.01b"></script> <script src="script.js?version=0.59.07b"></script>
</body> </body>

304
script.js
View file

@ -75,7 +75,7 @@ function fantasyMap() {
oceanLayers.append("rect").attr("id", "oceanBase"); oceanLayers.append("rect").attr("id", "oceanBase");
// main data variables // main data variables
var seed, from, voronoi, diagram, polygons, points = [], heights; var seed, params, voronoi, diagram, polygons, points = [], heights;
// Common variables // Common variables
var modules = {}, customization = 0, history = [], historyStage = 0, elSelected, autoResize = true, graphSize, var modules = {}, customization = 0, history = [], historyStage = 0, elSelected, autoResize = true, graphSize,
cells = [], land = [], riversData = [], manors = [], states = [], features = [], cells = [], land = [], riversData = [], manors = [], states = [], features = [],
@ -226,12 +226,14 @@ function fantasyMap() {
applyNamesData(); // apply default namesbase on load applyNamesData(); // apply default namesbase on load
generate(); // generate map on load generate(); // generate map on load
applyDefaultStyle(); // apply style on load applyDefaultStyle(); // apply style on load
focusOn(); // based on searchParams focus on point, cell or burg from MFCG
invokeActiveZooming(); // to hide what need to be hidden invokeActiveZooming(); // to hide what need to be hidden
function generate() { function generate() {
console.group("Random map"); console.group("Random map");
console.time("TOTAL"); console.time("TOTAL");
applyMapSize(); applyMapSize();
randomizeOptions();
placePoints(); placePoints();
calculateVoronoi(points); calculateVoronoi(points);
detectNeighbors(); detectNeighbors();
@ -242,7 +244,6 @@ function fantasyMap() {
elevateLakes(); elevateLakes();
resolveDepressionsPrimary(); resolveDepressionsPrimary();
reGraph(); reGraph();
randomizeOptions();
resolveDepressionsSecondary(); resolveDepressionsSecondary();
flux(); flux();
addLakes(); addLakes();
@ -250,8 +251,6 @@ function fantasyMap() {
drawRelief(); drawRelief();
generateCultures(); generateCultures();
manorsAndRegions(); manorsAndRegions();
// focus on burg from MFCG
if (from === "MFCG") findBurgForMFCG();
cleanData(); cleanData();
console.timeEnd("TOTAL"); console.timeEnd("TOTAL");
console.groupEnd("Random map"); console.groupEnd("Random map");
@ -260,12 +259,11 @@ function fantasyMap() {
// get or generate map seed // get or generate map seed
function getSeed() { function getSeed() {
const url = new URL(window.location.href); const url = new URL(window.location.href);
const s = url.searchParams.get("seed"); params = url.searchParams;
seed = s || Math.floor(Math.random() * 1e9); seed = params.get("seed") || Math.floor(Math.random() * 1e9);
console.log(" seed: " + seed); console.log(" seed: " + seed);
optionsSeed.value = seed; optionsSeed.value = seed;
Math.seedrandom(seed); Math.seedrandom(seed);
from = url.searchParams.get("from");
} }
// generate new map seed // generate new map seed
@ -1183,7 +1181,7 @@ function fantasyMap() {
// Mark features (ocean, lakes, islands) // Mark features (ocean, lakes, islands)
function markFeatures() { function markFeatures() {
console.time("markFeatures"); console.time("markFeatures");
Math.seedrandom(seed); // reset seed to get the same result on heightmap edit
for (let i=0, queue=[0]; queue.length > 0; i++) { for (let i=0, queue=[0]; queue.length > 0; i++) {
const cell = cells[queue[0]]; const cell = cells[queue[0]];
cell.fn = i; // feature number cell.fn = i; // feature number
@ -1316,7 +1314,7 @@ function fantasyMap() {
} }
// recalculate Voronoi Graph to pack cells // recalculate Voronoi Graph to pack cells
function reGraph(noChange) { function reGraph() {
console.time("reGraph"); console.time("reGraph");
const tempCells = [], newPoints = []; // to store new data const tempCells = [], newPoints = []; // to store new data
// get average precipitation based on graph size // get average precipitation based on graph size
@ -1325,18 +1323,17 @@ function fantasyMap() {
let smallLakes = 0; let smallLakes = 0;
const evaporation = 2; const evaporation = 2;
cells.map(function(i, d) { cells.map(function(i, d) {
const height = heights[d]; let height = i.height || heights[d];
if (height > 100) console.log(height, d); if (height > 100) height = 100;
const pit = i.pit; const pit = i.pit;
const ctype = i.ctype; const ctype = i.ctype;
if (ctype !== -1 && ctype !== -2 && height < 20) return; // exclude all depp ocean points if (ctype !== -1 && ctype !== -2 && height < 20) return; // exclude all deep ocean points
const x = rn(i.data[0], 1), y = rn(i.data[1], 1); const x = rn(i.data[0], 1), y = rn(i.data[1], 1);
const fn = i.fn; const fn = i.fn;
const harbor = i.harbor; const harbor = i.harbor;
let lake = i.lake; let lake = i.lake;
// mark potential cells for small lakes to add additional point there // mark potential cells for small lakes to add additional point there
// not for custom map if "changeHeights" is not alkowed if (smallLakes < smallLakesMax && !lake && pit > evaporation && ctype !== 2) {
if (!noChange && smallLakes < smallLakesMax && !lake && pit > evaporation && ctype !== 2) {
lake = 2; lake = 2;
smallLakes++; smallLakes++;
} }
@ -1364,7 +1361,6 @@ function fantasyMap() {
}); });
} }
if (lake === 2) { // add potential small lakes if (lake === 2) { // add potential small lakes
//debug.append("circle").attr("r", 0.3).attr("cx", x).attr("cy", y).attr("fill", "blue");
polygons[i.index].forEach(function(e) { polygons[i.index].forEach(function(e) {
if (Math.random() > 0.8) return; if (Math.random() > 0.8) return;
let rnd = Math.random() * 0.6 + 0.8; let rnd = Math.random() * 0.6 + 0.8;
@ -1373,7 +1369,6 @@ function fantasyMap() {
const y1 = rn((e[1] * rnd + i.data[1]) / (1 + rnd), 2); const y1 = rn((e[1] * rnd + i.data[1]) / (1 + rnd), 2);
copy = $.grep(newPoints, function(c) {return x1 === c[0] && y1 === c[1];}); copy = $.grep(newPoints, function(c) {return x1 === c[0] && y1 === c[1];});
if (copy.length) return; if (copy.length) return;
//debug.append("circle").attr("r", 0.2).attr("cx", x1).attr("cy", y1).attr("fill", "red");
newPoints.push([x1, y1]); newPoints.push([x1, y1]);
tempCells.push({index:tempCells.length, data:[x1, y1], height, pit, ctype, fn, region, culture}); tempCells.push({index:tempCells.length, data:[x1, y1], height, pit, ctype, fn, region, culture});
}); });
@ -1508,6 +1503,7 @@ function fantasyMap() {
// Detect and draw the coasline // Detect and draw the coasline
function drawCoastline() { function drawCoastline() {
console.time('drawCoastline'); console.time('drawCoastline');
Math.seedrandom(seed); // reset seed to get the same result on heightmap edit
const shape = defs.append("mask").attr("id", "shape").attr("fill", "black").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%"); const shape = defs.append("mask").attr("id", "shape").attr("fill", "black").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%");
$("#landmass").empty(); $("#landmass").empty();
let minX = graphWidth, maxX = 0; // extreme points let minX = graphWidth, maxX = 0; // extreme points
@ -1533,12 +1529,12 @@ function fantasyMap() {
const x = (land[i].data[0] + cells[ea].data[0]) / 2; const x = (land[i].data[0] + cells[ea].data[0]) / 2;
const y = (land[i].data[1] + cells[ea].data[1]) / 2; const y = (land[i].data[1] + cells[ea].data[1]) / 2;
land[i].haven = ea; // harbor haven (oposite water cell) land[i].haven = ea; // harbor haven (oposite water cell)
land[i].coastX = x, land[i].coastY = y; land[i].coastX = rn(x + (land[i].data[0] - x) * 0.1, 1);
land[i].coastY = rn(y + (land[i].data[1] - y) * 0.1, 1);
land[i].data[0] = rn(x + (land[i].data[0] - x) * 0.5, 1); land[i].data[0] = rn(x + (land[i].data[0] - x) * 0.5, 1);
land[i].data[1] = rn(y + (land[i].data[1] - y) * 0.5, 1); land[i].data[1] = rn(y + (land[i].data[1] - y) * 0.5, 1);
} }
if (features[cells[ea].fn].border) { if (features[cells[ea].fn].border) {
//debug.append("line").attr("x1", edge[0][0]).attr("y1", edge[0][1]).attr("x2", edge[1][0]).attr("y2", edge[1][1]).attr("stroke", "blue").attr("stroke-width", .2);
oceanEdges[f].push({start, end}); oceanEdges[f].push({start, end});
// island extreme points // island extreme points
if (edge[0][0] < minX) {minX = edge[0][0]; minXedge = edge[0]} if (edge[0][0] < minX) {minX = edge[0][0]; minXedge = edge[0]}
@ -1549,12 +1545,10 @@ function fantasyMap() {
const l = cells[ea].fn; const l = cells[ea].fn;
if (!lakeEdges[f][l]) lakeEdges[f][l] = []; if (!lakeEdges[f][l]) lakeEdges[f][l] = [];
lakeEdges[f][l].push({start, end}); lakeEdges[f][l].push({start, end});
//debug.append("line").attr("x1", edge[0][0]).attr("y1", edge[0][1]).attr("x2", edge[1][0]).attr("y2", edge[1][1]).attr("stroke", "red").attr("stroke-width", .2);
} }
} }
} else { } else {
oceanEdges[f].push({start, end}); oceanEdges[f].push({start, end});
//debug.append("line").attr("x1", edge[0][0]).attr("y1", edge[0][1]).attr("x2", edge[1][0]).attr("y2", edge[1][1]).attr("stroke", "black").attr("stroke-width", .5);
} }
}); });
} }
@ -1842,12 +1836,16 @@ function fantasyMap() {
function elevateLakes() { function elevateLakes() {
console.time('elevateLakes'); console.time('elevateLakes');
const lakes = $.grep(cells, function(e, d) {return heights[d] < 20 && !features[e.fn].border;}); const lakes = $.grep(cells, function(e, d) {return heights[d] < 20 && !features[e.fn].border;});
lakes.sort(function(a, b) {return b.height - a.height;}); lakes.sort(function(a, b) {return heights[b.index] - heights[a.index];});
for (let i=0; i < lakes.length; i++) { for (let i=0; i < lakes.length; i++) {
const hs = [], id = lakes[i].index; const hs = [], id = lakes[i].index;
lakes[i].neighbors.forEach(function(n) {if (cells[n].height >= 20) hs.push(cells[n].height)}); cells[id].height = heights[id]; // use height on object level
if (hs.length) heights[id] = d3.min(hs) - 1; lakes[i].neighbors.forEach(function(n) {
if (heights[id] < 20) heights[id] = 20; const nHeight = cells[n].height || heights[n];
if (nHeight >= 20) hs.push(nHeight);
});
if (hs.length) cells[id].height = d3.min(hs) - 1;
if (cells[id].height < 20) cells[id].height = 20;
lakes[i].lake = 1; lakes[i].lake = 1;
} }
console.timeEnd('elevateLakes'); console.timeEnd('elevateLakes');
@ -1856,20 +1854,23 @@ function fantasyMap() {
// Depression filling algorithm (for a correct water flux modeling; phase1) // Depression filling algorithm (for a correct water flux modeling; phase1)
function resolveDepressionsPrimary() { function resolveDepressionsPrimary() {
console.time('resolveDepressionsPrimary'); console.time('resolveDepressionsPrimary');
land = $.grep(cells, function(e, d) {return heights[d] >= 20;}); land = $.grep(cells, function(e, d) {
land.sort(function(a, b) {return heights[b.index] - heights[a.index];}); if (!e.height) e.height = heights[d]; // use height on object level
return e.height >= 20;
});
land.sort(function(a, b) {return b.height - a.height;});
const limit = 10; const limit = 10;
for (let l = 0, depression = 1; depression > 0 && l < limit; l++) { for (let l = 0, depression = 1; depression > 0 && l < limit; l++) {
depression = 0; depression = 0;
for (let i = 0; i < land.length; i++) { for (let i = 0; i < land.length; i++) {
const id = land[i].index; const id = land[i].index;
if (land[i].type === "border") continue; if (land[i].type === "border") continue;
const hs = land[i].neighbors.map(function(n) {return heights[n];}); const hs = land[i].neighbors.map(function(n) {return cells[n].height;});
const minHigh = d3.min(hs); const minHigh = d3.min(hs);
if (heights[id] <= minHigh) { if (cells[id].height <= minHigh) {
depression++; depression++;
land[i].pit = land[i].pit ? land[i].pit + 1 : 1; land[i].pit = land[i].pit ? land[i].pit + 1 : 1;
heights[id] = minHigh + 2; cells[id].height = minHigh + 2;
} }
} }
if (l === 0) console.log(" depressions init: " + depression); if (l === 0) console.log(" depressions init: " + depression);
@ -1887,8 +1888,8 @@ function fantasyMap() {
depression = 0; depression = 0;
for (let i = 0; i < land.length; i++) { for (let i = 0; i < land.length; i++) {
if (land[i].ctype === 99) continue; if (land[i].ctype === 99) continue;
const heights = land[i].neighbors.map(function(n) {return cells[n].height}); const nHeights = land[i].neighbors.map(function(n) {return cells[n].height});
const minHigh = d3.min(heights); const minHigh = d3.min(nHeights);
if (land[i].height <= minHigh) { if (land[i].height <= minHigh) {
depression++; depression++;
land[i].pit = land[i].pit ? land[i].pit + 1 : 1; land[i].pit = land[i].pit ? land[i].pit + 1 : 1;
@ -1901,12 +1902,12 @@ function fantasyMap() {
console.timeEnd('resolveDepressionsSecondary'); console.timeEnd('resolveDepressionsSecondary');
} }
// restore initial heights if user don't want system to change heightmap
function restoreCustomHeights() { function restoreCustomHeights() {
land.forEach(function(l) { land.forEach(function(l) {
if (l.pit) { if (!l.pit) return;
l.height = Math.trunc(l.height - l.pit * 2); l.height = Math.trunc(l.height - l.pit * 2);
if (l.height < 20) l.height = 20; if (l.height < 20) l.height = 20;
}
}); });
} }
@ -2507,7 +2508,7 @@ function fantasyMap() {
land.map(function(l) { land.map(function(l) {
if (l.river === river) { if (l.river === river) {
l.river = undefined; l.river = undefined;
i.flux = avPrec; l.flux = avPrec;
} }
}); });
elSelected.remove(); elSelected.remove();
@ -3716,7 +3717,7 @@ function fantasyMap() {
y = cell.coastY; y = cell.coastY;
} }
} }
if (cell.river) { if (cell.river && cell.type !== 1) {
let shift = 0.2 * cell.flux; let shift = 0.2 * cell.flux;
if (shift < 0.2) shift = 0.2; if (shift < 0.2) shift = 0.2;
if (shift > 1) shift = 1; if (shift > 1) shift = 1;
@ -4659,32 +4660,55 @@ function fantasyMap() {
} }
} }
// focus on coorditanes, cell or burg provided in searchParams
function focusOn() {
if (params.get("from") === "MFCG") {
// focus on burg from MFCG
findBurgForMFCG();
return;
}
let s = params.get("scale") || 8;
let x = params.get("x");
let y = params.get("y");
let c = params.get("cell");
if (c !== null) {
x = cells[+c].data[0];
y = cells[+c].data[1];
}
let b = params.get("burg");
if (b !== null) {
x = manors[+b].x;
y = manors[+b].y;
}
if (x !== null && y !== null) zoomTo(x, y, s, 1600);
}
// find burg from MFCG and focus on it // find burg from MFCG and focus on it
function findBurgForMFCG() { function findBurgForMFCG() {
if (!manors.length) {console.error("No burgs generated. Cannot select a burg for MFCG"); return;} if (!manors.length) {console.error("No burgs generated. Cannot select a burg for MFCG"); return;}
const url = new URL(window.location.href); const size = +params.get("size");
const size = +url.searchParams.get("size"); let coast = +params.get("coast");
let coast = +url.searchParams.get("coast"); let port = +params.get("port");
let river = +url.searchParams.get("river"); let river = +params.get("river");
let selection = defineSelection(coast, river); let selection = defineSelection(coast, port, river);
if (!selection.length) selection = defineSelection(coast, !river); if (!selection.length) selection = defineSelection(coast, !port, !river);
if (!selection.length) selection = defineSelection(!coast, !river); if (!selection.length) selection = defineSelection(!coast, 0, !river);
if (!selection.length) selection = manors[0]; // select first if nothing is found
if (!selection.length) {console.error("Cannot find a burg for MFCG"); return;} if (!selection.length) {console.error("Cannot find a burg for MFCG"); return;}
function defineSelection(coast, river) { function defineSelection(coast, port, river) {
let selection = []; let selection = [];
if (coast && river) selection = $.grep(manors, function(e) {return cells[e.cell].port !== undefined && cells[e.cell].river !== undefined;}); if (port && river) selection = $.grep(manors, function(e) {return cells[e.cell].port !== undefined && cells[e.cell].river !== undefined;});
if (!coast && !river) selection = $.grep(manors, function(e) {return cells[e.cell].port === undefined && cells[e.cell].river === undefined;}); else if (!port && coast && river) selection = $.grep(manors, function(e) {return cells[e.cell].port === undefined && cells[e.cell].ctype === 1 && cells[e.cell].river !== undefined;});
if (!coast && river) selection = $.grep(manors, function(e) {return cells[e.cell].port === undefined && cells[e.cell].river !== undefined;}); else if (!coast && !river) selection = $.grep(manors, function(e) {return cells[e.cell].ctype !== 1 && cells[e.cell].river === undefined;});
if (coast && !river) selection = $.grep(manors, function(e) {return cells[e.cell].port !== undefined && cells[e.cell].river === undefined;}); else if (!coast && river) selection = $.grep(manors, function(e) {return cells[e.cell].ctype !== 1 && cells[e.cell].river !== undefined;});
else if (coast && !river) selection = $.grep(manors, function(e) {return cells[e.cell].ctype === 1 && cells[e.cell].river === undefined;});
return selection; return selection;
} }
// select a burg with closes population from selection // select a burg with closes population from selection
const selected = d3.scan(selection, function(a, b) {return Math.abs(a.population - size) - Math.abs(b.population - size);}); const selected = d3.scan(selection, function(a, b) {return Math.abs(a.population - size) - Math.abs(b.population - size);});
console.log(selection)
const burg = selection[selected].i; const burg = selection[selected].i;
console.log(selected, size)
if (size && burg !== undefined) {manors[burg].population = size;} else {return;} if (size && burg !== undefined) {manors[burg].population = size;} else {return;}
// focus on found burg // focus on found burg
@ -4714,8 +4738,8 @@ function fantasyMap() {
let height = i.height; let height = i.height;
if (height < 20 && !i.lake) return; if (height < 20 && !i.lake) return;
if (i.lake) { if (i.lake) {
const heights = i.neighbors.map(function(e) {if (cells[e].height >= 20) return cells[e].height;}) const nHeights = i.neighbors.map(function(e) {if (cells[e].height >= 20) return cells[e].height;})
const mean = d3.mean(heights); const mean = d3.mean(nHeights);
if (!mean) return; if (!mean) return;
height = Math.trunc(mean); height = Math.trunc(mean);
if (height < 20 || isNaN(height)) height = 20; if (height < 20 || isNaN(height)) height = 20;
@ -5051,12 +5075,11 @@ function fantasyMap() {
drawOcean(); drawOcean();
elevateLakes(); elevateLakes();
resolveDepressionsPrimary(); resolveDepressionsPrimary();
const noChange = !changeHeights.checked; reGraph();
reGraph(noChange);
resolveDepressionsSecondary(); resolveDepressionsSecondary();
flux(); flux();
addLakes(); addLakes();
if (noChange) restoreCustomHeights(); if (!changeHeights.checked) restoreCustomHeights();
drawCoastline(); drawCoastline();
drawRelief(); drawRelief();
const keepData = states.length && manors.length; const keepData = states.length && manors.length;
@ -5215,9 +5238,9 @@ function fantasyMap() {
cell.river = river; cell.river = river;
var x = cell.data[0], y = cell.data[1]; var x = cell.data[0], y = cell.data[1];
dataRiver.push({x, y, cell:index}); dataRiver.push({x, y, cell:index});
var heights = []; var nHeights = [];
cell.neighbors.forEach(function(e) {heights.push(cells[e].height);}); cell.neighbors.forEach(function(e) {nHeights.push(cells[e].height);});
var minId = heights.indexOf(d3.min(heights)); var minId = nHeights.indexOf(d3.min(nHeights));
var min = cell.neighbors[minId]; var min = cell.neighbors[minId];
var tx = cells[min].data[0], ty = cells[min].data[1]; var tx = cells[min].data[0], ty = cells[min].data[1];
if (cells[min].height < 20) { if (cells[min].height < 20) {
@ -5648,8 +5671,8 @@ function fantasyMap() {
if (editGroupInput.value !== "") { if (editGroupInput.value !== "") {
groupNew = editGroupInput.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, ""); groupNew = editGroupInput.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, "");
if (Number.isFinite(+groupNew.charAt(0))) groupNew = "g" + groupNew; if (Number.isFinite(+groupNew.charAt(0))) groupNew = "g" + groupNew;
if (groupNew === "towns") groupNew = "town_labels"; const size = svg.selectAll("#"+groupNew).size();
if (groupNew === "capitals") groupNew = "capital_labels"; if (size) groupNew += size; // if el with this id exists, add size to id;
} }
if (groupOld !== groupNew) { if (groupOld !== groupNew) {
var removed = elSelected.remove(); var removed = elSelected.remove();
@ -5734,11 +5757,13 @@ function fantasyMap() {
// downalod map as SVG or PNG file // downalod map as SVG or PNG file
function saveAsImage(type) { function saveAsImage(type) {
console.time("saveAsImage"); console.time("saveAsImage");
// get all used fonts const webSafe = ["Georgia", "Times+New+Roman", "Comic+Sans+MS", "Lucida+Sans+Unicode", "Courier+New", "Verdana", "Arial", "Impact"];
// get non-standard fonts used for labels to fetch them from web
const fontsInUse = []; // to store fonts currently in use const fontsInUse = []; // to store fonts currently in use
labels.selectAll("g").each(function(d) { labels.selectAll("g").each(function(d) {
const font = d3.select(this).attr("data-font"); const font = d3.select(this).attr("data-font");
if (!font) return; if (!font) return;
if (webSafe.indexOf(font) !== -1) return; // do not fetch web-safe fonts
if (fontsInUse.indexOf(font) === -1) fontsInUse.push(font); if (fontsInUse.indexOf(font) === -1) fontsInUse.push(font);
}); });
const fontsToLoad = "https://fonts.googleapis.com/css?family=" + fontsInUse.join("|"); const fontsToLoad = "https://fonts.googleapis.com/css?family=" + fontsInUse.join("|");
@ -5904,10 +5929,14 @@ function fantasyMap() {
// Save in .map format, based on FileSystem API // Save in .map format, based on FileSystem API
function saveMap() { function saveMap() {
console.time("saveMap"); console.time("saveMap");
// data convention: 0 - version; 1 - all points; 2 - cells; 3 - manors; 4 - states; // data convention: 0 - params; 1 - all points; 2 - cells; 3 - manors; 4 - states;
// 5 - svg; 6 - options (see below); 7 - cultures; // 5 - svg; 6 - options (see below); 7 - cultures;
// 8 - empty (former nameBase); 9 - empty (former nameBases); 10 - heights; // 8 - empty (former nameBase); 9 - empty (former nameBases); 10 - heights;
// size stats: points = 6%, cells = 36%, manors and states = 2%, svg = 56%; // size stats: points = 6%, cells = 36%, manors and states = 2%, svg = 56%;
const date = new Date();
const dateString = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate();
const license = "File can be loaded in azgaar.github.io/Fantasy-Map-Generator";
const params = version + "|" + license + "|" + dateString + "|" + seed;
const options = customization + "|" + const options = customization + "|" +
distanceUnit.value + "|" + distanceScale.value + "|" + areaUnit.value + "|" + distanceUnit.value + "|" + distanceScale.value + "|" + areaUnit.value + "|" +
barSize.value + "|" + barLabel.value + "|" + barBackOpacity.value + "|" + barBackColor.value + "|" + barSize.value + "|" + barLabel.value + "|" + barBackOpacity.value + "|" + barBackColor.value + "|" +
@ -5923,7 +5952,7 @@ function fantasyMap() {
var svg_xml = (new XMLSerializer()).serializeToString(svg.node()); var svg_xml = (new XMLSerializer()).serializeToString(svg.node());
var line = "\r\n"; var line = "\r\n";
var data = version + line + JSON.stringify(points) + line + JSON.stringify(cells) + line; var data = params + line + JSON.stringify(points) + line + JSON.stringify(cells) + line;
data += JSON.stringify(manors) + line + JSON.stringify(states) + line + svg_xml + line + options + line; data += JSON.stringify(manors) + line + JSON.stringify(states) + line + svg_xml + line + options + line;
data += JSON.stringify(cultures) + line + "" + line + "" + line + heights + line; data += JSON.stringify(cultures) + line + "" + line + "" + line + heights + line;
var dataBlob = new Blob([data], {type:"text/plain"}); var dataBlob = new Blob([data], {type:"text/plain"});
@ -5958,9 +5987,10 @@ function fantasyMap() {
fileReader.onload = function(fileLoadedEvent) { fileReader.onload = function(fileLoadedEvent) {
var dataLoaded = fileLoadedEvent.target.result; var dataLoaded = fileLoadedEvent.target.result;
var data = dataLoaded.split("\r\n"); var data = dataLoaded.split("\r\n");
// data convention: 0 - version; 1 - all points; 2 - cells; 3 - manors; 4 - states; // data convention: 0 - params; 1 - all points; 2 - cells; 3 - manors; 4 - states;
// 5 - svg; 6 - options; 7 - cultures; 8 - none; 9 - none; 10 - heights; // 5 - svg; 6 - options; 7 - cultures; 8 - none; 9 - none; 10 - heights;
var mapVersion = data[0]; const params = data[0].split("|");
const mapVersion = params[0] || data[0];
if (mapVersion !== version) { if (mapVersion !== version) {
var message = `The Map version `; var message = `The Map version `;
// mapVersion reference was not added to downloaded map before v. 0.52b, so I cannot support really old files // mapVersion reference was not added to downloaded map before v. 0.52b, so I cannot support really old files
@ -5971,7 +6001,7 @@ function fantasyMap() {
or just keep using or just keep using
<a href="https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog" target="_blank">an appropriate version</a> <a href="https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog" target="_blank">an appropriate version</a>
of the Generator`; of the Generator`;
} else { } else if (!mapVersion || parseFloat(mapVersion) < 0.54) {
message += `you are trying to load is too old and cannot be updated. Please re-create the map or just keep using message += `you are trying to load is too old and cannot be updated. Please re-create the map or just keep using
<a href="https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog" target="_blank">an archived version</a> <a href="https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog" target="_blank">an archived version</a>
of the Generator. Please note the Gennerator is still on demo and a lot of crusial changes are being made every month`; of the Generator. Please note the Gennerator is still on demo and a lot of crusial changes are being made every month`;
@ -5989,6 +6019,13 @@ function fantasyMap() {
function loadDataFromMap(data) { function loadDataFromMap(data) {
closeDialogs(); closeDialogs();
// update seed
const params = data[0].split("|");
if (params[3]) {
seed = params[3];
optionsSeed.value = seed;
}
// get options // get options
if (data[0] === "0.52b" || data[0] === "0.53b") { if (data[0] === "0.52b" || data[0] === "0.53b") {
customization = 0; customization = 0;
@ -6161,6 +6198,68 @@ function fantasyMap() {
// update data // update data
newPoints = [], riversData = [], queue = [], elSelected = ""; newPoints = [], riversData = [], queue = [], elSelected = "";
points = JSON.parse(data[1]); points = JSON.parse(data[1]);
cells = JSON.parse(data[2]);
manors = JSON.parse(data[3]);
if (data[7]) cultures = JSON.parse(data[7]);
if (data[7] === undefined) generateCultures();
// place random point
function placePoint() {
const x = Math.floor(Math.random() * graphWidth * 0.8 + graphWidth * 0.1);
const y = Math.floor(Math.random() * graphHeight * 0.8 + graphHeight * 0.1);
return [x, y];
}
// ensure each culure has a valid namesbase assigned, if not assign first base
if (!nameBase[0]) applyDefaultNamesData();
cultures.forEach(function(c) {
const b = c.base;
if (b === undefined) c.base = 0;
if (!nameBase[b] || !nameBases[b]) c.base = 0;
if (c.center === undefined) c.center = placePoint();
});
const graphSizeAdj = 90 / Math.sqrt(cells.length, 2); // adjust to different graphSize
// cells validations
cells.forEach(function(c, d) {
// collect points
newPoints.push(c.data);
// update old 0-1 height range to a new 0-100 range
if (c.height < 1) c.height = Math.trunc(c.height * 100);
if (c.height === 1 && c.region !== undefined && c.flux !== undefined) c.height = 100;
// check if there are any unavailable cultures
if (c.culture > cultures.length - 1) {
const center = [c.data[0], c.data[1]];
const cult = {name:"AUTO_"+c.culture, color:"#ff0000", base:0, center};
cultures.push(cult);
}
if (c.height >= 20) {
if (!polygons[d] || !polygons[d].length) return;
// calculate area
if (c.area === undefined || isNaN(c.area)) {
const area = d3.polygonArea(polygons[d]);
c.area = rn(Math.abs(area), 2);
}
// calculate population
if (c.pop === undefined || isNaN(c.pop)) {
let population = 0;
const elevationFactor = Math.pow((100 - c.height) / 100, 3);
population = elevationFactor * c.area * graphSizeAdj;
if (c.region === "neutral") population *= 0.5;
c.pop = rn(population, 1);
}
// if culture is undefined, set to 0
if (c.culture === undefined || isNaN(c.culture)) c.culture = 0;
}
});
land = $.grep(cells, function(e) {return (e.height >= 20);});
calculateVoronoi(newPoints);
// get heights Uint8Array
if (data[10]) {heights = new Uint8Array(data[10].split(","));} if (data[10]) {heights = new Uint8Array(data[10].split(","));}
else { else {
heights = new Uint8Array(points.length); heights = new Uint8Array(points.length);
@ -6169,52 +6268,6 @@ function fantasyMap() {
heights[i] = cells[cell].height; heights[i] = cells[cell].height;
} }
} }
cells = JSON.parse(data[2]);
manors = JSON.parse(data[3]);
if (data[7]) cultures = JSON.parse(data[7]);
if (data[7] === undefined) generateCultures();
// ensure each culure has a valid namesbase assigned, if not assign first base
if (!nameBase[0]) applyDefaultNamesData();
cultures.forEach(function(c) {
const b = c.base;
if (b === undefined) c.base = 0;
if (!nameBase[b] || !nameBases[b]) c.base = 0;
});
// cells validations
cells.forEach(function(c) {
// collect points
newPoints.push(c.data);
// update old 0-1 height range to new 0-100 range
if (c.height > 1) return;
if (c.height === 1 && c.region === undefined && c.flux === undefined) return;
c.height = Math.trunc(c.height * 100);
// check if there unavailable cultures
if (c.culture > cultures.length - 1) cultures.push({name:"AUTO_"+c.culture, color:"#ff0000", base:0});
});
land = $.grep(cells, function(e) {return (e.height >= 20);});
calculateVoronoi(newPoints);
// calculate areas / population for old maps
const graphSizeAdj = 90 / Math.sqrt(cells.length, 2); // adjust to different graphSize
land.forEach(function(i) {
const p = i.index;
if (!polygons[p] || !polygons[p].length) return;
const area = d3.polygonArea(polygons[p]);
i.area = rn(Math.abs(area), 2);
if (i.pop === undefined) {
let population = 0;
const elevationFactor = Math.pow((100 - i.height) / 100, 3);
population = elevationFactor * i.area * graphSizeAdj;
if (i.region === "neutral") population *= 0.5;
i.pop = rn(population, 1);
}
});
// restore Heightmap customization mode // restore Heightmap customization mode
if (customization === 1) { if (customization === 1) {
@ -6751,11 +6804,11 @@ function fantasyMap() {
}); });
} }
if (id === "fromHeightmap") { if (id === "fromHeightmap") {
const message = `Hightmap is a basic element on which secondary data (burgs, countries, cultures) is based. const message = `Hightmap is a basic element on which secondary data (rivers, burgs, countries etc) is based.
If you want to significantly change the hightmap, it may be better to clean up all the secondary data If you want to significantly change the hightmap, it may be better to clean up all the secondary data
and let the system to re-generate it based on the updated hightmap. In case of minor changes, you can keep the data. and let the system to re-generate it based on the updated hightmap. In case of minor changes, you can keep the data.
Newly added lands will be considered as neutral. Burgs located on a removed land cells will be deleted. Newly added lands will be considered as neutral. Burgs located on a removed land cells will be deleted.
Routes won't be regenerated.` Rivers and small lakes will be re-gerenated based on updated heightmap. Routes won't be regenerated.`
alertMessage.innerHTML = message; alertMessage.innerHTML = message;
$("#alert").dialog({resizable: false, title: "Edit Heightmap", $("#alert").dialog({resizable: false, title: "Edit Heightmap",
buttons: { buttons: {
@ -6977,7 +7030,11 @@ function fantasyMap() {
const regionData = [], cultureData = []; const regionData = [], cultureData = [];
if (type !== "clean") { if (type !== "clean") {
for (let i = 0; i < points.length; i++) { for (let i = 0; i < points.length; i++) {
const cell = diagram.find(points[i][0], points[i][1]).index; let cell = diagram.find(points[i][0], points[i][1]).index;
// if closest cell is a small lake, try to find a land neighbor
if (cells[cell].lake === 2) cells[cell].neighbors.forEach(function(n) {
if (cells[n].height >= 20) {cell = n; return;}
});
let region = cells[cell].region; let region = cells[cell].region;
if (region === undefined) region = -1; if (region === undefined) region = -1;
regionData.push(region); regionData.push(region);
@ -8654,7 +8711,7 @@ function fantasyMap() {
drawRegions(); drawRegions();
} }
// restore keeped region data on edit heightmap completion // restore keeped region / burgs / cultures data on edit heightmap completion
function restoreRegions() { function restoreRegions() {
borders.selectAll("path").remove(); borders.selectAll("path").remove();
labels.select("#countries").selectAll("text").remove(); labels.select("#countries").selectAll("text").remove();
@ -8664,8 +8721,7 @@ function fantasyMap() {
// remove manor in ocean // remove manor in ocean
m.region = "removed"; m.region = "removed";
m.cell = cell; m.cell = cell;
labels.select("[data-id='" + m.i + "']").remove(); d3.selectAll("[data-id='" + m.i + "']").remove();
icons.select("[data-id='" + m.i + "']").remove();
} else { } else {
m.cell = cell; m.cell = cell;
cells[cell].manor = m.i; cells[cell].manor = m.i;
@ -9294,16 +9350,12 @@ function fantasyMap() {
if (id === "aboutTab") {$("#aboutContent").show();} if (id === "aboutTab") {$("#aboutContent").show();}
}); });
// Generate map from provided seed // re-load page with provided seed
$("#optionsSeedGenerate").on("click", function() { $("#optionsSeedGenerate").on("click", function() {
if (optionsSeed.value == seed) return; if (optionsSeed.value == seed) return;
seed = optionsSeed.value; seed = optionsSeed.value;
console.log(" seed: " + seed); const url = new URL(window.location.href);
Math.seedrandom(seed); window.location.href = url.pathname + "?seed=" + seed;
exitCustomization();
undraw();
resetZoom(1000);
generate();
}); });
// Pull request from @evyatron // Pull request from @evyatron