diff --git a/index.css b/index.css index 36869b25..771e6422 100644 --- a/index.css +++ b/index.css @@ -104,7 +104,7 @@ a { } #biomes { - stroke-width: .7; + stroke-width: 0.7; } #landmass { @@ -285,6 +285,12 @@ i.icon-lock { animation: dash 80s linear backwards; } +.arrow { + marker-end: url(#end-arrow-small); + stroke: #555; + stroke-width: 0.5; +} + @keyframes dash { to { stroke-dashoffset: 0; diff --git a/index.html b/index.html index 698d876a..eb363635 100644 --- a/index.html +++ b/index.html @@ -3451,6 +3451,9 @@ + + + diff --git a/main.js b/main.js index c37dffb2..4cba5ac2 100644 --- a/main.js +++ b/main.js @@ -110,7 +110,12 @@ 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, + mapId, + mapHistory = [], + elSelected, + modules = {}, + notes = []; let rulers = new Rulers(); let customization = 0; // 0 - no; 1 = heightmap draw; 2 - states draw; 3 - add state/burg; 4 - cultures draw @@ -122,30 +127,34 @@ let color = d3.scaleSequential(d3.interpolateSpectral); // default color scheme const lineGen = d3.line().curve(d3.curveBasis); // d3 line generator with default curve interpolation // d3 zoom behavior -let scale = 1, viewX = 0, viewY = 0; +let scale = 1, + viewX = 0, + viewY = 0; const zoom = d3.zoom().scaleExtent([1, 20]).on("zoom", zoomed); // default options -let options = {pinNotes:false}; // options object +let options = {pinNotes: false}; // options object let mapCoordinates = {}; // map coordinates on globe options.winds = [225, 45, 225, 315, 135, 315]; // default wind directions 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 +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 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() { +void (function removeLoading() { 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() { +void (function checkLoadParameters() { const url = new URL(window.location.href); const params = url.searchParams; @@ -155,8 +164,10 @@ void function checkLoadParameters() { const maplink = params.get("maplink"); const pattern = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/; const valid = pattern.test(maplink); - if (valid) {loadMapFromURL(maplink, 1); return;} - else showUploadErrorMessage("Map link is not a valid URL", maplink); + if (valid) { + loadMapFromURL(maplink, 1); + return; + } else showUploadErrorMessage("Map link is not a valid URL", maplink); } // if there is a seed (user of MFCG provided), generate map for it @@ -173,8 +184,7 @@ void function checkLoadParameters() { WARN && console.warn("Load last saved map"); try { uploadMap(blob); - } - catch(error) { + } catch (error) { ERROR && console.error(error); WARN && console.warn("Cannot load stored map, random map to be generated"); generateMapOnLoad(); @@ -189,16 +199,17 @@ void function checkLoadParameters() { WARN && console.warn("Generate random map"); generateMapOnLoad(); -}() +})(); function loadMapFromURL(maplink, random) { const URL = decodeURIComponent(maplink); - fetch(URL, {method: 'GET', mode: 'cors'}) + fetch(URL, {method: "GET", mode: "cors"}) .then(response => { - if(response.ok) return response.blob(); + if (response.ok) return response.blob(); throw new Error("Cannot load map from URL"); - }).then(blob => uploadMap(blob)) + }) + .then(blob => uploadMap(blob)) .catch(error => { showUploadErrorMessage(error.message, URL, random); if (random) generateMapOnLoad(); @@ -208,9 +219,17 @@ function loadMapFromURL(maplink, random) { 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. `:''} + ${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");}}}); + $("#alert").dialog({ + title: "Loading error", + width: "32em", + buttons: { + OK: function () { + $(this).dialog("close"); + } + } + }); } function generateMapOnLoad() { @@ -257,8 +276,12 @@ function focusOn() { // find burg for MFCG and focus on it function findBurgForMFCG(params) { - const cells = pack.cells, burgs = pack.burgs; - if (pack.burgs.length < 2) {ERROR && console.error("Cannot select a burg for MFCG"); return;} + const cells = pack.cells, + burgs = pack.burgs; + if (pack.burgs.length < 2) { + ERROR && console.error("Cannot select a burg for MFCG"); + return; + } // used for selection const size = +params.get("size"); @@ -283,26 +306,32 @@ function findBurgForMFCG(params) { // select a burg with closest population from selection const selected = d3.scan(selection, (a, b) => Math.abs(a.population - size) - Math.abs(b.population - size)); const burgId = selection[selected].i; - if (!burgId) {ERROR && console.error("Cannot select a burg for MFCG"); return;} + if (!burgId) { + ERROR && console.error("Cannot select a burg for MFCG"); + return; + } const b = burgs[burgId]; const referrer = new URL(document.referrer); for (let p of referrer.searchParams) { - if (p[0] === "name") b.name = p[1]; else - if (p[0] === "size") b.population = +p[1]; else - if (p[0] === "seed") b.MFCG = +p[1]; else - if (p[0] === "shantytown") b.shanty = +p[1]; else - b[p[0]] = +p[1]; // other parameters + if (p[0] === "name") b.name = p[1]; + else if (p[0] === "size") b.population = +p[1]; + else if (p[0] === "seed") b.MFCG = +p[1]; + 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 + "']"); if (label.size()) { - label.text(b.name).classed("drag", true).on("mouseover", function() { - d3.select(this).classed("drag", false); - label.on("mouseover", null); - }); + label + .text(b.name) + .classed("drag", true) + .on("mouseover", function () { + d3.select(this).classed("drag", false); + label.on("mouseover", null); + }); } zoomTo(b.x, b.y, 8, 1600); @@ -312,30 +341,33 @@ function findBurgForMFCG(params) { // apply default biomes data function applyDefaultBiomesSystem() { - const name = ["Marine","Hot desert","Cold desert","Savanna","Grassland","Tropical seasonal forest","Temperate deciduous forest","Tropical rainforest","Temperate rainforest","Taiga","Tundra","Glacier","Wetland"]; - const color = ["#466eab","#fbe79f","#b5b887","#d2d082","#c8d68f","#b6d95d","#29bc56","#7dcb35","#409c43","#4b6b32","#96784b","#d5e7eb","#0b9131"]; - const habitability = [0,4,10,22,30,50,100,80,90,12,4,0,12]; - const iconsDensity = [0,3,2,120,120,120,120,150,150,100,5,0,150]; - const icons = [{},{dune:3, cactus:6, deadTree:1},{dune:9, deadTree:1},{acacia:1, grass:9},{grass:1},{acacia:8, palm:1},{deciduous:1},{acacia:5, palm:3, deciduous:1, swamp:1},{deciduous:6, swamp:1},{conifer:1},{grass:1},{},{swamp:1}]; - const cost = [10,200,150,60,50,70,70,80,90,200,1000,5000,150]; // biome movement cost - const biomesMartix = [ // hot ↔ cold [>19°C; <-4°C]; dry ↕ wet - new Uint8Array([1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,10]), - new Uint8Array([3,3,3,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,9,9,9,9,10,10,10]), - new Uint8Array([5,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,9,9,9,9,9,10,10,10]), - new Uint8Array([5,6,6,6,6,6,6,8,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,10,10,10]), - new Uint8Array([7,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,10,10]) + const name = ["Marine", "Hot desert", "Cold desert", "Savanna", "Grassland", "Tropical seasonal forest", "Temperate deciduous forest", "Tropical rainforest", "Temperate rainforest", "Taiga", "Tundra", "Glacier", "Wetland"]; + const color = ["#466eab", "#fbe79f", "#b5b887", "#d2d082", "#c8d68f", "#b6d95d", "#29bc56", "#7dcb35", "#409c43", "#4b6b32", "#96784b", "#d5e7eb", "#0b9131"]; + const habitability = [0, 4, 10, 22, 30, 50, 100, 80, 90, 12, 4, 0, 12]; + const iconsDensity = [0, 3, 2, 120, 120, 120, 120, 150, 150, 100, 5, 0, 150]; + const icons = [{}, {dune: 3, cactus: 6, deadTree: 1}, {dune: 9, deadTree: 1}, {acacia: 1, grass: 9}, {grass: 1}, {acacia: 8, palm: 1}, {deciduous: 1}, {acacia: 5, palm: 3, deciduous: 1, swamp: 1}, {deciduous: 6, swamp: 1}, {conifer: 1}, {grass: 1}, {}, {swamp: 1}]; + const cost = [10, 200, 150, 60, 50, 70, 70, 80, 90, 200, 1000, 5000, 150]; // biome movement cost + const biomesMartix = [ + // hot ↔ cold [>19°C; <-4°C]; dry ↕ wet + new Uint8Array([1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 10]), + new Uint8Array([3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 9, 9, 9, 9, 10, 10, 10]), + new Uint8Array([5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 9, 9, 9, 9, 9, 10, 10, 10]), + new Uint8Array([5, 6, 6, 6, 6, 6, 6, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 10, 10, 10]), + new Uint8Array([7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 10, 10]) ]; // parse icons weighted array into a simple array - for (let i=0; i < icons.length; i++) { + for (let i = 0; i < icons.length; i++) { const parsed = []; for (const icon in icons[i]) { - for (let j = 0; j < icons[i][icon]; j++) {parsed.push(icon);} + for (let j = 0; j < icons[i][icon]; j++) { + parsed.push(icon); + } } icons[i] = parsed; } - return {i:d3.range(0, name.length), name, color, biomesMartix, habitability, iconsDensity, icons, cost}; + return {i: d3.range(0, name.length), name, color, biomesMartix, habitability, iconsDensity, icons, cost}; } function showWelcomeMessage() { @@ -363,18 +395,24 @@ function showWelcomeMessage() {

Join our ${discord} and ${reddit} to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.

Thanks for all supporters on ${patreon}!`; - $("#alert").dialog( - {resizable: false, title: "Fantasy Map Generator update", width: "28em", - buttons: {OK: function() {$(this).dialog("close")}}, + $("#alert").dialog({ + resizable: false, + title: "Fantasy Map Generator update", + width: "28em", + buttons: { + OK: function () { + $(this).dialog("close"); + } + }, position: {my: "center center-4em", at: "center", of: "svg"}, - close: () => localStorage.setItem("version", version)} - ); + close: () => localStorage.setItem("version", version) + }); } function zoomed() { const transform = d3.event.transform; const scaleDiff = scale - transform.k; - const positionDiff = viewX - transform.x | viewY - transform.y; + const positionDiff = (viewX - transform.x) | (viewY - transform.y); if (!positionDiff && !scaleDiff) return; scale = transform.k; @@ -417,7 +455,10 @@ function resetZoom(d = 1000) { 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]]; + return [ + [Math.abs(viewX / scale), Math.abs(viewY / scale)], + [Math.abs(viewX / scale) + graphWidth / scale, Math.abs(viewY / scale) + graphHeight / scale] + ]; } // active zooming feature @@ -430,7 +471,7 @@ function invokeActiveZooming() { // rescale lables on zoom if (labels.style("display") !== "none") { - labels.selectAll("g").each(function(d) { + labels.selectAll("g").each(function (d) { if (this.id === "burgLabels") return; const desired = +this.dataset.size; const relative = Math.max(rn((desired + desired / scale) / 2, 2), 1); @@ -440,19 +481,23 @@ function invokeActiveZooming() { else this.classList.remove("hidden"); }); } - + // rescale emblems on zoom if (emblems.style("display") !== "none") { - emblems.selectAll("g").each(function() { + emblems.selectAll("g").each(function () { const size = this.getAttribute("font-size") * scale; const hidden = hideEmblems.checked && (size < 25 || size > 300); - if (hidden) this.classList.add("hidden"); else this.classList.remove("hidden"); + if (hidden) this.classList.add("hidden"); + else this.classList.remove("hidden"); if (!hidden && window.COArenderer && this.children.length && !this.children[0].getAttribute("href")) renderGroupCOAs(this); }); } // turn off ocean pattern if scale is big (improves performance) - oceanPattern.select("rect").attr("fill", scale > 10 ? "#fff" : "url(#oceanic)").attr("opacity", scale > 10 ? .2 : null); + oceanPattern + .select("rect") + .attr("fill", scale > 10 ? "#fff" : "url(#oceanic)") + .attr("opacity", scale > 10 ? 0.2 : null); // change states halo width if (!customization) { @@ -462,45 +507,49 @@ function invokeActiveZooming() { // rescale map markers if (+markers.attr("rescale") && markers.style("display") !== "none") { - markers.selectAll("use").each(function(d) { - const x = +this.dataset.x, y = +this.dataset.y, desired = +this.dataset.size; + markers.selectAll("use").each(function (d) { + 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); + d3.select(this) + .attr("x", x - size / 2) + .attr("y", y - size) + .attr("width", size) + .attr("height", size); }); } // rescale rulers to have always the same size if (ruler.style("display") !== "none") { - const size = rn(10 / scale ** .3 * 2, 2); + const size = rn((10 / scale ** 0.3) * 2, 2); ruler.selectAll("text").attr("font-size", size); } } async function renderGroupCOAs(g) { - const [group, type] = g.id === "burgEmblems" ? [pack.burgs, "burg"] : - g.id === "provinceEmblems" ? [pack.provinces, "province"] : - [pack.states, "state"]; + const [group, type] = g.id === "burgEmblems" ? [pack.burgs, "burg"] : g.id === "provinceEmblems" ? [pack.provinces, "province"] : [pack.states, "state"]; for (let use of g.children) { const i = +use.dataset.i; - const id = type+"COA"+i; + const id = type + "COA" + i; COArenderer.trigger(id, group[i].coa); - use.setAttribute("href", "#"+id); + use.setAttribute("href", "#" + id); } } // add drag to upload logic, pull request from @evyatron -void function addDragToUpload() { - document.addEventListener("dragover", function(e) { +void (function addDragToUpload() { + document.addEventListener("dragover", function (e) { e.stopPropagation(); e.preventDefault(); document.getElementById("mapOverlay").style.display = null; }); - document.addEventListener('dragleave', function(e) { + document.addEventListener("dragleave", function (e) { document.getElementById("mapOverlay").style.display = "none"; }); - document.addEventListener("drop", function(e) { + document.addEventListener("drop", function (e) { e.stopPropagation(); e.preventDefault(); @@ -508,11 +557,18 @@ void function addDragToUpload() { overlay.style.display = "none"; if (e.dataTransfer.items == null || e.dataTransfer.items.length !== 1) return; // no files or more than one const file = e.dataTransfer.items[0].getAsFile(); - if (file.name.indexOf('.map') == -1) { // not a .map file - alertMessage.innerHTML = 'Please upload a .map file you have previously downloaded'; + if (file.name.indexOf(".map") == -1) { + // not a .map file + alertMessage.innerHTML = "Please upload a .map file you have previously downloaded"; $("#alert").dialog({ - resizable: false, title: "Invalid file format", position: {my: "center", at: "center", of: "svg"}, - buttons: {Close: function() {$(this).dialog("close");}} + resizable: false, + title: "Invalid file format", + position: {my: "center", at: "center", of: "svg"}, + buttons: { + Close: function () { + $(this).dialog("close"); + } + } }); return; } @@ -526,7 +582,7 @@ void function addDragToUpload() { overlay.innerHTML = "Drop a .map file to open"; }); }); -}() +})(); function generate() { try { @@ -541,7 +597,7 @@ function generate() { drawScaleBar(); HeightmapGenerator.generate(); markFeatures(); - getSignedDistanceField(); + markupGridOcean(); openNearSeaLakes(); OceanLayers(); defineMapSize(); @@ -576,11 +632,10 @@ function generate() { addZones(); Names.getMapName(); - WARN && console.warn(`TOTAL: ${rn((performance.now()-timeStart)/1000,2)}s`); + WARN && console.warn(`TOTAL: ${rn((performance.now() - timeStart) / 1000, 2)}s`); showStatistics(); INFO && console.groupEnd("Generated Map " + seed); - } - catch(error) { + } catch (error) { ERROR && console.error(error); clearMainTip(); @@ -588,14 +643,25 @@ function generate() {
If error is critical, clear the stored data and try again.

${parseError(error)}

`; $("#alert").dialog({ - resizable: false, title: "Generation error", width:"32em", buttons: { - "Clear data": function() {localStorage.clear(); localStorage.setItem("version", version);}, - Regenerate: function() {regenerateMap(); $(this).dialog("close");}, - Ignore: function() {$(this).dialog("close");} - }, position: {my: "center", at: "center", of: "svg"} + resizable: false, + title: "Generation error", + width: "32em", + buttons: { + "Clear data": function () { + localStorage.clear(); + localStorage.setItem("version", version); + }, + Regenerate: function () { + regenerateMap(); + $(this).dialog("close"); + }, + Ignore: function () { + $(this).dialog("close"); + } + }, + position: {my: "center", at: "center", of: "svg"} }); } - } // generate map seed (string!) or get it from URL searchParams @@ -604,7 +670,7 @@ function generateSeed() { const url = new URL(window.location.href); const params = url.searchParams; const urlSeed = url.searchParams.get("seed"); - if (first && params.get("from") === "MFCG" && urlSeed.length === 13) seed = urlSeed.slice(0,-4); + if (first && params.get("from") === "MFCG" && urlSeed.length === 13) seed = urlSeed.slice(0, -4); else if (first && urlSeed) seed = urlSeed; else if (optionsSeed.value && optionsSeed.value != seed) seed = optionsSeed.value; else seed = Math.floor(Math.random() * 1e9).toString(); @@ -617,7 +683,7 @@ function placePoints() { TIME && console.time("placePoints"); const cellsDesired = +pointsInput.dataset.cells; - const spacing = grid.spacing = rn(Math.sqrt(graphWidth * graphHeight / cellsDesired), 2); // spacing between points before jirrering + const spacing = (grid.spacing = rn(Math.sqrt((graphWidth * graphHeight) / cellsDesired), 2)); // spacing between points before jirrering grid.boundary = getBoundaryPoints(graphWidth, graphHeight, spacing); grid.points = getJitteredGrid(graphWidth, graphHeight, spacing); // jittered square grid grid.cellsX = Math.floor((graphWidth + 0.5 * spacing) / spacing); @@ -646,7 +712,8 @@ function markFeatures() { TIME && console.time("markFeatures"); Math.random = aleaPRNG(seed); // get the same result on heightmap edit in Erase mode - const cells = grid.cells, heights = grid.cells.h; + const cells = grid.cells, + heights = grid.cells.h; cells.f = new Uint16Array(cells.i.length); // cell feature number cells.t = new Int8Array(cells.i.length); // cell type: 1 = land coast; -1 = water near coast grid.features = [0]; @@ -680,35 +747,33 @@ function markFeatures() { TIME && console.timeEnd("markFeatures"); } -function getSignedDistanceField() { - TIME && console.time("getSignedDistanceField"); - const cells = grid.cells, pointsN = cells.i.length; - markup(-2, -1, -10); - markup(2, 1, 0); +function markupGridOcean() { + TIME && console.time("markupGridOcean"); + markup(grid.cells, -2, -1, -10); + TIME && console.timeEnd("markupGridOcean"); +} - // build signed distance field - function markup(start, increment, limit) { - for (let t = start, count = Infinity; count > 0 && t > limit; t += increment) { - count = 0; - const prevT = t - increment; - for (let i = 0; i < pointsN; i++) { - if (cells.t[i] !== prevT) continue; +function markup(cells, start, increment, limit) { + for (let t = start, count = Infinity; count > 0 && t > limit; t += increment) { + count = 0; + const prevT = t - increment; + for (let i = 0; i < cells.i.length; i++) { + if (cells.t[i] !== prevT) continue; - for (const c of cells.c[i]) { - if (cells.t[c]) continue; - cells.t[c] = t; - count++; - } + for (const c of cells.c[i]) { + if (cells.t[c]) continue; + cells.t[c] = t; + count++; } } } - TIME && console.timeEnd("getSignedDistanceField"); } // near sea lakes usually get a lot of water inflow, most of them should brake treshold and flow out to sea (see Ancylus Lake) function openNearSeaLakes() { if (templateInput.value === "Atoll") return; // no need for Atolls - const cells = grid.cells, features = grid.features; + const cells = grid.cells, + features = grid.features; if (!features.find(f => f.type === "lake")) return; // no lakes TIME && console.time("openLakes"); const LIMIT = 22; // max height that can be breached by water @@ -717,8 +782,7 @@ function openNearSeaLakes() { const lake = cells.f[i]; if (features[lake].type !== "lake") continue; // not a lake cell - check_neighbours: - for (const c of cells.c[i]) { + check_neighbours: for (const c of cells.c[i]) { if (cells.t[c] !== 1 || cells.h[c] > LIMIT) continue; // water cannot brake this for (const n of cells.c[c]) { @@ -734,7 +798,7 @@ function openNearSeaLakes() { cells.h[treshold] = 19; cells.t[treshold] = -1; cells.f[treshold] = ocean; - cells.c[treshold].forEach(function(c) { + cells.c[treshold].forEach(function (c) { if (cells.h[c] >= 20) cells.t[c] = 1; // mark as coastline }); features[lake].type = "ocean"; // mark former lake as ocean @@ -754,15 +818,15 @@ function defineMapSize() { const template = document.getElementById("templateInput").value; // heightmap template const part = grid.features.some(f => f.land && f.border); // if land goes over map borders const max = part ? 80 : 100; // max size - const lat = () => gauss(P(.5) ? 40 : 60, 15, 25, 75); // latiture shift + const lat = () => gauss(P(0.5) ? 40 : 60, 15, 25, 75); // latiture shift if (!part) { if (template === "Pangea") return [100, 50]; - if (template === "Shattered" && P(.7)) return [100, 50]; - if (template === "Continents" && P(.5)) return [100, 50]; - if (template === "Archipelago" && P(.35)) return [100, 50]; - if (template === "High Island" && P(.25)) return [100, 50]; - if (template === "Low Island" && P(.1)) return [100, 50]; + if (template === "Shattered" && P(0.7)) return [100, 50]; + if (template === "Continents" && P(0.5)) return [100, 50]; + if (template === "Archipelago" && P(0.35)) return [100, 50]; + if (template === "High Island" && P(0.25)) return [100, 50]; + if (template === "Low Island" && P(0.1)) return [100, 50]; } if (template === "Pangea") return [gauss(70, 20, 30, max), lat()]; @@ -781,30 +845,30 @@ 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 latT = (size / 100) * 180; + const latN = 90 - ((180 - latT) * latShift) / 100; const latS = latN - latT; - const lon = Math.min(graphWidth / graphHeight * latT / 2, 180); - mapCoordinates = {latT, latN, latS, lonT: lon*2, lonW: -lon, lonE: lon}; + const lon = Math.min(((graphWidth / graphHeight) * latT) / 2, 180); + mapCoordinates = {latT, latN, latS, lonT: lon * 2, lonW: -lon, lonE: lon}; } // temperature model function calculateTemperatures() { - TIME && console.time('calculateTemperatures'); + TIME && console.time("calculateTemperatures"); const cells = grid.cells; cells.temp = new Int8Array(cells.i.length); // temperature array const tEq = +temperatureEquatorInput.value; const tPole = +temperaturePoleInput.value; const tDelta = tEq - tPole; - const int = d3.easePolyInOut.exponent(.5); // interpolation function + const int = d3.easePolyInOut.exponent(0.5); // interpolation function - d3.range(0, cells.i.length, grid.cellsX).forEach(function(r) { + d3.range(0, cells.i.length, grid.cellsX).forEach(function (r) { const y = grid.points[r][1]; - const lat = Math.abs(mapCoordinates.latN - y / graphHeight * mapCoordinates.latT); // [0; 90] + 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++) { + for (let i = r; i < r + grid.cellsX; i++) { cells.temp[i] = Math.max(Math.min(initTemp - convertToFriendly(cells.h[i]), 127), -128); } }); @@ -814,41 +878,46 @@ function calculateTemperatures() { if (h < 20) return 0; const exponent = +heightExponentInput.value; const height = Math.pow(h - 18, exponent); - return rn(height / 1000 * 6.5); + return rn((height / 1000) * 6.5); } - TIME && console.timeEnd('calculateTemperatures'); + TIME && console.timeEnd("calculateTemperatures"); } // simplest precipitation model function generatePrecipitation() { - TIME && console.time('generatePrecipitation'); + TIME && console.time("generatePrecipitation"); prec.selectAll("*").remove(); const cells = grid.cells; 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; + 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) + { + // 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 lalitudeModifier = [4, 2, 2, 2, 1, 1, 2, 2, 2, 2, 3, 3, 2, 2, 1, 1, 1, 0.5]; // by 5d step // difine 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; + 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 + 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]); + 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++; }); @@ -856,24 +925,27 @@ function generatePrecipitation() { // 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); + const vertT = southerly + northerly; if (northerly) { - const bandN = (Math.abs(mapCoordinates.latN) - 1) / 5 | 0; + const bandN = ((Math.abs(mapCoordinates.latN) - 1) / 5) | 0; const latModN = mapCoordinates.latT > 60 ? d3.mean(lalitudeModifier) : lalitudeModifier[bandN]; - const maxPrecN = northerly / vertT * 60 * modifier * latModN; + 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 bandS = ((Math.abs(mapCoordinates.latS) - 1) / 5) | 0; const latModS = mapCoordinates.latT > 60 ? d3.mean(lalitudeModifier) : lalitudeModifier[bandS]; - const maxPrecS = southerly / vertT * 60 * modifier * latModS; + const maxPrecS = (southerly / vertT) * 60 * modifier * latModS; passWind(d3.range(cells.i.length - cellsX, cells.i.length, 1), maxPrecS, -cellsX, cellsY); } 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];} + 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) { @@ -881,8 +953,8 @@ function generatePrecipitation() { if (cells.temp[current] < -5) continue; // water cell if (cells.h[current] < 20) { - if (cells.h[current+next] >= 20) { - cells.prec[current+next] += Math.max(humidity / rand(10, 20), 1); // coastal precipitation + if (cells.h[current + next] >= 20) { + cells.prec[current + next] += Math.max(humidity / rand(10, 20), 1); // coastal precipitation } else { humidity = Math.min(humidity + 5 * modifier, maxPrec); // wind gets more humidity passing water cell cells.prec[current] += 5 * modifier; // water cells precipitation (need to correctly pour water through lakes) @@ -900,21 +972,22 @@ function generatePrecipitation() { } function getPrecipitation(humidity, i, n) { - if (cells.h[i+n] > 85) return humidity; // 85 is max passable height + 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 + 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); } - void function drawWindDirection() { - const wind = prec.append("g").attr("id", "wind"); + void (function drawWindDirection() { + const wind = prec.append("g").attr("id", "wind"); - d3.range(0, 6).forEach(function(t) { + d3.range(0, 6).forEach(function (t) { if (westerly.length > 1) { const west = westerly.filter(w => w[2] === t); if (west && west.length > 3) { - const from = west[0][0], to = west[west.length-1][0]; + const from = west[0][0], + to = west[west.length - 1][0]; const y = (grid.points[from][1] + grid.points[to][1]) / 2; wind.append("text").attr("x", 20).attr("y", y).text("\u21C9"); } @@ -922,32 +995,47 @@ function generatePrecipitation() { if (easterly.length > 1) { const east = easterly.filter(w => w[2] === t); if (east && east.length > 3) { - const from = east[0][0], to = east[east.length-1][0]; + const from = east[0][0], + to = east[east.length - 1][0]; const y = (grid.points[from][1] + grid.points[to][1]) / 2; - wind.append("text").attr("x", graphWidth - 52).attr("y", y).text("\u21C7"); + wind + .append("text") + .attr("x", graphWidth - 52) + .attr("y", y) + .text("\u21C7"); } } }); - if (northerly) wind.append("text").attr("x", graphWidth / 2).attr("y", 42).text("\u21CA"); - if (southerly) wind.append("text").attr("x", graphWidth / 2).attr("y", graphHeight - 20).text("\u21C8"); - }(); + if (northerly) + wind + .append("text") + .attr("x", graphWidth / 2) + .attr("y", 42) + .text("\u21CA"); + if (southerly) + wind + .append("text") + .attr("x", graphWidth / 2) + .attr("y", graphHeight - 20) + .text("\u21C8"); + })(); - TIME && console.timeEnd('generatePrecipitation'); + TIME && console.timeEnd("generatePrecipitation"); } // recalculate Voronoi Graph to pack cells function reGraph() { TIME && console.time("reGraph"); let {cells, points, features} = grid; - const newCells = {p:[], g:[], h:[]}; // to store new data + const newCells = {p: [], g: [], h: []}; // to store new data const spacing2 = grid.spacing ** 2; for (const i of cells.i) { const height = cells.h[i]; const type = cells.t[i]; if (height < 20 && type !== -1 && type !== -2) continue; // exclude all deep ocean points - if (type === -2 && (i%4=== 0 || features[cells.f[i]].type === "lake")) continue; // exclude non-coastal lake points + if (type === -2 && (i % 4 === 0 || features[cells.f[i]].type === "lake")) continue; // exclude non-coastal lake points const [x, y] = points[i]; addNewPoint(i, x, y, height); @@ -955,7 +1043,7 @@ function reGraph() { // add additional points for cells along coast if (type === 1 || type === -1) { if (cells.b[i]) continue; // not for near-border cells - cells.c[i].forEach(function(e) { + cells.c[i].forEach(function (e) { if (i > e) return; if (cells.t[e] === type) { const dist2 = (y - points[e][1]) ** 2 + (x - points[e][0]) ** 2; @@ -981,18 +1069,25 @@ function reGraph() { cells.q = d3.quadtree(cells.p.map((p, d) => [p[0], p[1], d])); // points quadtree for fast search cells.h = new Uint8Array(newCells.h); // heights cells.area = new Uint16Array(cells.i.length); // cell area - cells.i.forEach(i => cells.area[i] = Math.abs(d3.polygonArea(getPackPolygon(i)))); + cells.i.forEach(i => (cells.area[i] = Math.abs(d3.polygonArea(getPackPolygon(i))))); TIME && console.timeEnd("reGraph"); } // Detect and draw the coasline function drawCoastline() { - TIME && console.time('drawCoastline'); + TIME && console.time("drawCoastline"); reMarkFeatures(); - const cells = pack.cells, vertices = pack.vertices, n = cells.i.length, features = pack.features; + + const cells = pack.cells, + vertices = pack.vertices, + n = cells.i.length, + features = pack.features; const used = new Uint8Array(features.length); // store conneted features - const largestLand = d3.scan(features.map(f => f.land ? f.cells : 0), (a, b) => b - a); + const largestLand = d3.scan( + features.map(f => (f.land ? f.cells : 0)), + (a, b) => b - a + ); const landMask = defs.select("#land"); const waterMask = defs.select("#water"); lineGen.curve(d3.curveBasisClosed); @@ -1010,7 +1105,10 @@ function drawCoastline() { let vchain = connectVertices(start, type); if (features[f].type === "lake") relax(vchain, 1.2); used[f] = 1; - let points = clipPoly(vchain.map(v => vertices.p[v]), 1); + let points = clipPoly( + vchain.map(v => vertices.p[v]), + 1 + ); const area = d3.polygonArea(points); // area with lakes/islands if (area > 0 && features[f].type === "lake") { points = points.reverse(); @@ -1022,14 +1120,36 @@ function drawCoastline() { const path = round(lineGen(points)); if (features[f].type === "lake") { - landMask.append("path").attr("d", path).attr("fill", "black").attr("id", "land_"+f); + landMask + .append("path") + .attr("d", path) + .attr("fill", "black") + .attr("id", "land_" + f); // waterMask.append("path").attr("d", path).attr("fill", "white").attr("id", "water_"+id); // uncomment to show over lakes - lakes.select("#freshwater").append("path").attr("d", path).attr("id", "lake_"+f).attr("data-f", f); // draw the lake + lakes + .select("#freshwater") + .append("path") + .attr("d", path) + .attr("id", "lake_" + f) + .attr("data-f", f); // draw the lake } else { - landMask.append("path").attr("d", path).attr("fill", "white").attr("id", "land_"+f); - waterMask.append("path").attr("d", path).attr("fill", "black").attr("id", "water_"+f); + landMask + .append("path") + .attr("d", path) + .attr("fill", "white") + .attr("id", "land_" + f); + waterMask + .append("path") + .attr("d", path) + .attr("fill", "black") + .attr("id", "water_" + f); const g = features[f].group === "lake_island" ? "lake_island" : "sea_island"; - coastline.select("#"+g).append("path").attr("d", path).attr("id", "island_"+f).attr("data-f", f); // draw the coastline + coastline + .select("#" + g) + .append("path") + .attr("d", path) + .attr("id", "island_" + f) + .attr("data-f", f); // draw the coastline } // draw ruler to cover the biggest land piece @@ -1051,31 +1171,36 @@ function drawCoastline() { // connect vertices to chain function connectVertices(start, t) { const chain = []; // vertices chain to form a path - for (let i=0, current = start; i === 0 || current !== start && i < 50000; i++) { - const prev = chain[chain.length-1]; // previous vertex in chain + for (let i = 0, current = start; i === 0 || (current !== start && i < 50000); i++) { + const prev = chain[chain.length - 1]; // previous vertex in chain chain.push(current); // add current vertex to sequence - const c = vertices.c[current] // cells adjacent to vertex - const v = vertices.v[current] // neighboring vertices + const c = vertices.c[current]; // cells adjacent to vertex + const v = vertices.v[current]; // neighboring vertices const c0 = c[0] >= n || cells.t[c[0]] === t; const c1 = c[1] >= n || cells.t[c[1]] === t; const c2 = c[2] >= n || cells.t[c[2]] === t; - if (v[0] !== prev && c0 !== c1) current = v[0]; else - if (v[1] !== prev && c1 !== c2) current = v[1]; else - if (v[2] !== prev && c0 !== c2) current = v[2]; - if (current === chain[chain.length-1]) {ERROR && console.error("Next vertex is not found"); break;} + if (v[0] !== prev && c0 !== c1) current = v[0]; + else if (v[1] !== prev && c1 !== c2) current = v[1]; + else if (v[2] !== prev && c0 !== c2) current = v[2]; + if (current === chain[chain.length - 1]) { + ERROR && console.error("Next vertex is not found"); + break; + } } return chain; } // move vertices that are too close to already added ones function relax(vchain, r) { - const p = vertices.p, tree = d3.quadtree(); + const p = vertices.p, + tree = d3.quadtree(); - for (let i=0; i < vchain.length; i++) { + for (let i = 0; i < vchain.length; i++) { const v = vchain[i]; let [x, y] = [p[v][0], p[v][1]]; - if (i && vchain[i+1] && tree.find(x, y, r) !== undefined) { - const v1 = vchain[i-1], v2 = vchain[i+1]; + if (i && vchain[i + 1] && tree.find(x, y, r) !== undefined) { + const v1 = vchain[i - 1], + v2 = vchain[i + 1]; const [x1, y1] = [p[v1][0], p[v1][1]]; const [x2, y2] = [p[v2][0], p[v2][1]]; [x, y] = [(x1 + x2) / 2, (y1 + y2) / 2]; @@ -1085,19 +1210,20 @@ function drawCoastline() { } } - TIME && console.timeEnd('drawCoastline'); + TIME && console.timeEnd("drawCoastline"); } // Re-mark features (ocean, lakes, islands) function reMarkFeatures() { TIME && console.time("reMarkFeatures"); - const cells = pack.cells, features = pack.features = [0]; + const cells = pack.cells, + features = (pack.features = [0]); cells.f = new Uint16Array(cells.i.length); // cell feature number cells.t = new Int8Array(cells.i.length); // cell type: 1 = land along coast; -1 = water along coast; - cells.haven = cells.i.length < 65535 ? new Uint16Array(cells.i.length) : new Uint32Array(cells.i.length);// cell haven (opposite water cell); + cells.haven = cells.i.length < 65535 ? new Uint16Array(cells.i.length) : new Uint32Array(cells.i.length); // cell haven (opposite water cell); cells.harbor = new Uint8Array(cells.i.length); // cell harbor (number of adjacent water cells); - for (let i=1, queue=[0]; queue[0] !== -1; i++) { + for (let i = 1, queue = [0]; queue[0] !== -1; i++) { const start = queue[0]; // first cell cells.f[start] = i; // assign feature number const land = cells.h[start] >= 20; @@ -1107,7 +1233,7 @@ function reMarkFeatures() { while (queue.length) { const q = queue.pop(); if (cells.b[q]) border = true; - cells.c[q].forEach(function(e) { + cells.c[q].forEach(function (e) { const eLand = cells.h[e] >= 20; if (land && !eLand) { cells.t[q] = 1; @@ -1134,6 +1260,9 @@ function reMarkFeatures() { queue[0] = cells.f.findIndex(f => !f); // find unmarked cell } + // markupPackLand + markup(pack.cells, 3, 1, 0); + function defineOceanGroup(number) { if (number > grid.cells.i.length / 25) return "ocean"; if (number > grid.cells.i.length / 100) return "sea"; @@ -1141,7 +1270,7 @@ function reMarkFeatures() { } function defineIslandGroup(cell, number) { - if (cell && features[cells.f[cell-1]].type === "lake") return "lake_island"; + if (cell && features[cells.f[cell - 1]].type === "lake") return "lake_island"; if (number > grid.cells.i.length / 10) return "continent"; if (number > grid.cells.i.length / 1000) return "island"; return "isle"; @@ -1153,7 +1282,10 @@ 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.cells, + f = pack.features, + temp = grid.cells.temp, + prec = grid.cells.prec; cells.biome = new Uint8Array(cells.i.length); // biomes array for (const i of cells.i) { @@ -1166,7 +1298,10 @@ function defineBiomes() { function calculateMoisture(i) { let moist = prec[cells.g[i]]; if (cells.r[i]) moist += Math.max(cells.fl[i] / 20, 2); - const n = cells.c[i].filter(isLand).map(c => prec[cells.g[c]]).concat([moist]); + const n = cells.c[i] + .filter(isLand) + .map(c => prec[cells.g[c]]) + .concat([moist]); return rn(4 + d3.mean(n)); } @@ -1177,20 +1312,21 @@ function defineBiomes() { 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 + 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]; } // assess cells suitability to calculate population and rand cells for culture center and burgs placement function rankCells() { - TIME && console.time('rankCells'); + TIME && console.time("rankCells"); const {cells, features} = pack; cells.s = new Int16Array(cells.i.length); // cell suitability array cells.pop = new Float32Array(cells.i.length); // cell population array - const flMean = d3.median(cells.fl.filter(f => f)) || 0, flMax = d3.max(cells.fl) + d3.max(cells.conf); // to normalize flux + const flMean = d3.median(cells.fl.filter(f => f)) || 0, + flMax = d3.max(cells.fl) + d3.max(cells.conf); // to normalize flux const areaMean = d3.mean(cells.area); // to adjust population by cell area for (const i of cells.i) { @@ -1218,56 +1354,62 @@ function rankCells() { cells.s[i] = s / 5; // general population rate // cell rural population is suitability adjusted by cell area - cells.pop[i] = cells.s[i] > 0 ? cells.s[i] * cells.area[i] / areaMean : 0; + cells.pop[i] = cells.s[i] > 0 ? (cells.s[i] * cells.area[i]) / areaMean : 0; } - TIME && console.timeEnd('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; + 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); + 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 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(.3) ? "Mount " + proper : Math.random() > .3 ? proper + " Volcano" : proper; - notes.push({id, name, legend:`Active volcano. Height: ${getFriendlyHeight([x, y])}`}); + 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); + 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 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}`}); + 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() { + 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); + 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}; + 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); @@ -1280,17 +1422,17 @@ function addMarkers(number = 1) { notes.push({id, name, legend}); count--; } - }() + })(); - void function addBridges() { + 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)); + .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); + let count = !bridges.length ? 0 : Math.ceil((bridges.length / 12) * number); if (count) addMarker("bridge", "🌉", 50, 50, 14); while (count && bridges.length) { @@ -1299,14 +1441,14 @@ function addMarkers(number = 1) { 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(.2) ? river.name : burg.name; - notes.push({id, name:`${name} Bridge`, legend:`A stone bridge over the ${riverName} near ${burg.name}`}); + 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) * .9; + 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); @@ -1316,45 +1458,46 @@ function addMarkers(number = 1) { 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++) { + 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(.3) ? "inn" : "tavern"; - const name = P(.5) ? ra(color) + " " + ra(animal) : P(.6) ? ra(adj) + " " + ra(animal) : ra(adj) + " " + capitalize(type); - notes.push({id, name: "The " + name, legend:`A big and famous roadside ${type}`}); + 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() { + 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]; + 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`}); + notes.push({id, name: getAdjective(proper) + " Lighthouse" + name, legend: `A lighthouse to keep the navigation safe`}); } - }() + })(); - void function addWaterfalls() { + 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++) { + 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`}); + notes.push({id, name: getAdjective(proper) + " Waterfall" + name, legend: `An extremely beautiful waterfall`}); } - }() + })(); - void function addBattlefields() { + 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); + let count = battlefields.length < 100 ? 0 : Math.ceil((battlefields.length / 500) * number); if (count) addMarker("battlefield", "⚔️", 50, 52, 12); while (count && battlefields.length) { @@ -1367,28 +1510,48 @@ function addMarkers(number = 1) { 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; + if (markers.select("#marker_" + id).size()) return; - const symbol = markers.append("symbol").attr("id", "marker_"+id).attr("viewBox", "0 0 30 30"); + 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); + 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 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); + 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; } @@ -1399,20 +1562,23 @@ function addMarkers(number = 1) { // regenerate some zones function addZones(number = 1) { TIME && console.time("addZones"); - const data = [], cells = pack.cells, states = pack.states, burgs = pack.burgs; + const data = [], + cells = pack.cells, + states = pack.states, + burgs = pack.burgs; const used = new Uint8Array(cells.i.length); // to store used cells - for (let i=0; i < rn(Math.random() * 1.8 * number); i++) addInvasion(); // invasion of enemy lands - for (let i=0; i < rn(Math.random() * 1.6 * number); i++) addRebels(); // rebels along a state border - for (let i=0; i < rn(Math.random() * 1.6 * number); i++) addProselytism(); // proselitism of organized religion - for (let i=0; i < rn(Math.random() * 1.6 * number); i++) addCrusade(); // crusade on heresy lands - for (let i=0; i < rn(Math.random() * 1.8 * number); i++) addDisease(); // disease starting in a random city - for (let i=0; i < rn(Math.random() * 1.4 * number); i++) addDisaster(); // disaster starting in a random city - for (let i=0; i < rn(Math.random() * 1.4 * number); i++) addEruption(); // volcanic eruption aroung volcano - for (let i=0; i < rn(Math.random() * 1.0 * number); i++) addAvalanche(); // avalanche impacting highland road - for (let i=0; i < rn(Math.random() * 1.4 * number); i++) addFault(); // fault line in elevated areas - for (let i=0; i < rn(Math.random() * 1.4 * number); i++) addFlood() // flood on river banks - for (let i=0; i < rn(Math.random() * 1.2 * number); i++) addTsunami() // tsunami starting near coast + for (let i = 0; i < rn(Math.random() * 1.8 * number); i++) addInvasion(); // invasion of enemy lands + for (let i = 0; i < rn(Math.random() * 1.6 * number); i++) addRebels(); // rebels along a state border + for (let i = 0; i < rn(Math.random() * 1.6 * number); i++) addProselytism(); // proselitism of organized religion + for (let i = 0; i < rn(Math.random() * 1.6 * number); i++) addCrusade(); // crusade on heresy lands + for (let i = 0; i < rn(Math.random() * 1.8 * number); i++) addDisease(); // disease starting in a random city + for (let i = 0; i < rn(Math.random() * 1.4 * number); i++) addDisaster(); // disaster starting in a random city + for (let i = 0; i < rn(Math.random() * 1.4 * number); i++) addEruption(); // volcanic eruption aroung volcano + for (let i = 0; i < rn(Math.random() * 1.0 * number); i++) addAvalanche(); // avalanche impacting highland road + for (let i = 0; i < rn(Math.random() * 1.4 * number); i++) addFault(); // fault line in elevated areas + for (let i = 0; i < rn(Math.random() * 1.4 * number); i++) addFlood(); // flood on river banks + for (let i = 0; i < rn(Math.random() * 1.2 * number); i++) addTsunami(); // tsunami starting near coast function addInvasion() { const atWar = states.filter(s => s.diplomacy && s.diplomacy.some(d => d === "Enemy")); @@ -1424,10 +1590,12 @@ function addZones(number = 1) { const cell = ra(cells.i.filter(i => cells.state[i] === target && cells.c[i].some(c => cells.state[c] === invader.i))); if (!cell) return; - const cellsArray = [], queue = [cell], power = rand(5, 30); + const cellsArray = [], + queue = [cell], + power = rand(5, 30); while (queue.length) { - const q = P(.4) ? queue.shift() : queue.pop(); + const q = P(0.4) ? queue.shift() : queue.pop(); cellsArray.push(q); if (cellsArray.length > power) break; @@ -1439,10 +1607,9 @@ 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 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)"}); + data.push({name, type: "Invasion", cells: cellsArray, fill: "url(#hatch1)"}); } function addRebels() { @@ -1451,7 +1618,9 @@ function addZones(number = 1) { const neib = ra(state.neighbors.filter(n => n)); const cell = cells.i.find(i => cells.state[i] === state.i && cells.c[i].some(c => cells.state[c] === neib)); - const cellsArray = [], queue = [cell], power = rand(10, 30); + const cellsArray = [], + queue = [cell], + power = rand(10, 30); while (queue.length) { const q = queue.shift(); @@ -1462,15 +1631,14 @@ function addZones(number = 1) { if (used[e]) return; if (cells.state[e] !== state.i) return; used[e] = 1; - if (e%4 !== 0 && !cells.c[e].some(c => cells.state[c] === neib)) return; + if (e % 4 !== 0 && !cells.c[e].some(c => cells.state[c] === neib)) return; queue.push(e); }); } - const rebels = rw({"Rebels":5, "Insurgents":2, "Mutineers":1, "Rioters":1, "Separatists":1, - "Secessionists":1, "Insurrection":2, "Rebellion":1, "Conspiracy":2}); + const rebels = rw({Rebels: 5, Insurgents: 2, Mutineers: 1, Rioters: 1, Separatists: 1, Secessionists: 1, Insurrection: 2, Rebellion: 1, Conspiracy: 2}); const name = getAdjective(states[neib].name) + " " + rebels; - data.push({name, type:"Rebels", cells:cellsArray, fill:"url(#hatch3)"}); + data.push({name, type: "Rebels", cells: cellsArray, fill: "url(#hatch3)"}); } function addProselytism() { @@ -1480,7 +1648,9 @@ function addZones(number = 1) { const cell = ra(cells.i.filter(i => cells.religion[i] && cells.religion[i] !== organized.i && cells.c[i].some(c => cells.religion[c] === organized.i))); if (!cell) return; const target = cells.religion[cell]; - const cellsArray = [], queue = [cell], power = rand(10, 30); + const cellsArray = [], + queue = [cell], + power = rand(10, 30); while (queue.length) { const q = queue.shift(); @@ -1498,7 +1668,7 @@ function addZones(number = 1) { } const name = getAdjective(organized.name.split(" ")[0]) + " Proselytism"; - data.push({name, type:"Proselytism", cells:cellsArray, fill:"url(#hatch6)"}); + data.push({name, type: "Proselytism", cells: cellsArray, fill: "url(#hatch6)"}); } function addCrusade() { @@ -1507,26 +1677,28 @@ function addZones(number = 1) { const cellsArray = cells.i.filter(i => !used[i] && cells.religion[i] === heresy.i); if (!cellsArray.length) return; - cellsArray.forEach(i => used[i] = 1); + cellsArray.forEach(i => (used[i] = 1)); const name = getAdjective(heresy.name.split(" ")[0]) + " Crusade"; - data.push({name, type:"Crusade", cells:cellsArray, fill:"url(#hatch6)"}); + data.push({name, type: "Crusade", cells: cellsArray, fill: "url(#hatch6)"}); } function addDisease() { const burg = ra(burgs.filter(b => !used[b.cell] && b.i && !b.removed)); // random burg if (!burg) return; - const cellsArray = [], cost = [], power = rand(20, 37); + const cellsArray = [], + cost = [], + power = rand(20, 37); const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); - queue.queue({e:burg.cell, p:0}); + queue.queue({e: burg.cell, p: 0}); while (queue.length) { const next = queue.dequeue(); if (cells.burg[next.e] || cells.pop[next.e]) cellsArray.push(next.e); used[next.e] = 1; - cells.c[next.e].forEach(function(e) { + cells.c[next.e].forEach(function (e) { const r = cells.road[next.e]; const c = r ? Math.max(10 - r, 1) : 100; const p = next.p + c; @@ -1543,25 +1715,27 @@ function addZones(number = 1) { const animal = () => ra(["Ape", "Bear", "Boar", "Cat", "Cow", "Dog", "Pig", "Fox", "Bird", "Horse", "Rat", "Raven", "Sheep", "Spider", "Wolf"]); const color = () => ra(["Golden", "White", "Black", "Red", "Pink", "Purple", "Blue", "Green", "Yellow", "Amber", "Orange", "Brown", "Grey"]); - const type = rw({"Fever":5, "Pestilence":2, "Flu":2, "Pox":2, "Smallpox":2, "Plague":4, "Cholera":2, "Dropsy":1, "Leprosy":2}); - const name = rw({[color()]:4, [animal()]:2, [adjective()]:1}) + " " + type; - data.push({name, type:"Disease", cells:cellsArray, fill:"url(#hatch12)"}); + const type = rw({Fever: 5, Pestilence: 2, Flu: 2, Pox: 2, Smallpox: 2, Plague: 4, Cholera: 2, Dropsy: 1, Leprosy: 2}); + const name = rw({[color()]: 4, [animal()]: 2, [adjective()]: 1}) + " " + type; + data.push({name, type: "Disease", cells: cellsArray, fill: "url(#hatch12)"}); } function addDisaster() { const burg = ra(burgs.filter(b => !used[b.cell] && b.i && !b.removed)); // random burg if (!burg) return; - const cellsArray = [], cost = [], power = rand(5, 25); + const cellsArray = [], + cost = [], + power = rand(5, 25); const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); - queue.queue({e:burg.cell, p:0}); + queue.queue({e: burg.cell, p: 0}); while (queue.length) { const next = queue.dequeue(); if (cells.burg[next.e] || cells.pop[next.e]) cellsArray.push(next.e); used[next.e] = 1; - cells.c[next.e].forEach(function(e) { + cells.c[next.e].forEach(function (e) { const c = rand(1, 10); const p = next.p + c; if (p > power) return; @@ -1573,26 +1747,30 @@ function addZones(number = 1) { }); } - const type = rw({"Famine":5, "Dearth":1, "Drought":3, "Earthquake":3, "Tornadoes":1, "Wildfires":1}); + const type = rw({Famine: 5, Dearth: 1, Drought: 3, Earthquake: 3, Tornadoes: 1, Wildfires: 1}); const name = getAdjective(burg.name) + " " + type; - data.push({name, type:"Disaster", cells:cellsArray, fill:"url(#hatch5)"}); + data.push({name, type: "Disaster", cells: cellsArray, fill: "url(#hatch5)"}); } function addEruption() { const volcano = document.getElementById("markers").querySelector("use[data-id='#marker_volcano']"); if (!volcano) return; - const x = +volcano.dataset.x, y = +volcano.dataset.y, cell = findCell(x, y); + const x = +volcano.dataset.x, + y = +volcano.dataset.y, + cell = findCell(x, y); const id = volcano.id; const note = notes.filter(n => n.id === id); if (note[0]) note[0].legend = note[0].legend.replace("Active volcano", "Erupting volcano"); const name = note[0] ? note[0].name.replace(" Volcano", "") + " Eruption" : "Volcano Eruption"; - const cellsArray = [], queue = [cell], power = rand(10, 30); + const cellsArray = [], + queue = [cell], + power = rand(10, 30); while (queue.length) { - const q = P(.5) ? queue.shift() : queue.pop(); + const q = P(0.5) ? queue.shift() : queue.pop(); cellsArray.push(q); if (cellsArray.length > power) break; cells.c[q].forEach(e => { @@ -1602,7 +1780,7 @@ function addZones(number = 1) { }); } - data.push({name, type:"Disaster", cells:cellsArray, fill:"url(#hatch7)"}); + data.push({name, type: "Disaster", cells: cellsArray, fill: "url(#hatch7)"}); } function addAvalanche() { @@ -1610,10 +1788,12 @@ function addZones(number = 1) { if (!roads.length) return; const cell = +ra(roads); - const cellsArray = [], queue = [cell], power = rand(3, 15); + const cellsArray = [], + queue = [cell], + power = rand(3, 15); while (queue.length) { - const q = P(.3) ? queue.shift() : queue.pop(); + const q = P(0.3) ? queue.shift() : queue.pop(); cellsArray.push(q); if (cellsArray.length > power) break; cells.c[q].forEach(e => { @@ -1625,7 +1805,7 @@ function addZones(number = 1) { const proper = getAdjective(Names.getCultureShort(cells.culture[cell])); const name = proper + " Avalanche"; - data.push({name, type:"Disaster", cells:cellsArray, fill:"url(#hatch5)"}); + data.push({name, type: "Disaster", cells: cellsArray, fill: "url(#hatch5)"}); } function addFault() { @@ -1633,7 +1813,9 @@ function addZones(number = 1) { if (!elevated.length) return; const cell = ra(elevated); - const cellsArray = [], queue = [cell], power = rand(3, 15); + const cellsArray = [], + queue = [cell], + power = rand(3, 15); while (queue.length) { const q = queue.pop(); @@ -1648,16 +1830,22 @@ function addZones(number = 1) { const proper = getAdjective(Names.getCultureShort(cells.culture[cell])); const name = proper + " Fault"; - data.push({name, type:"Disaster", cells:cellsArray, fill:"url(#hatch2)"}); + data.push({name, type: "Disaster", cells: cellsArray, fill: "url(#hatch2)"}); } function addFlood() { - const fl = cells.fl.filter(fl => fl), meanFlux = d3.mean(fl), maxFlux = d3.max(fl), flux = (maxFlux - meanFlux) / 2 + meanFlux; + const fl = cells.fl.filter(fl => fl), + meanFlux = d3.mean(fl), + maxFlux = d3.max(fl), + flux = (maxFlux - meanFlux) / 2 + meanFlux; const rivers = cells.i.filter(i => !used[i] && cells.h[i] < 50 && cells.r[i] && cells.fl[i] > flux && cells.burg[i]); if (!rivers.length) return; - const cell = +ra(rivers), river = cells.r[cell]; - const cellsArray = [], queue = [cell], power = rand(5, 30); + const cell = +ra(rivers), + river = cells.r[cell]; + const cellsArray = [], + queue = [cell], + power = rand(5, 30); while (queue.length) { const q = queue.pop(); @@ -1672,7 +1860,7 @@ function addZones(number = 1) { } const name = getAdjective(burgs[cells.burg[cell]].name) + " Flood"; - data.push({name, type:"Disaster", cells:cellsArray, fill:"url(#hatch13)"}); + data.push({name, type: "Disaster", cells: cellsArray, fill: "url(#hatch13)"}); } function addTsunami() { @@ -1680,7 +1868,9 @@ function addZones(number = 1) { if (!coastal.length) return; const cell = +ra(coastal); - const cellsArray = [], queue = [cell], power = rand(10, 30); + const cellsArray = [], + queue = [cell], + power = rand(10, 30); while (queue.length) { const q = queue.shift(); @@ -1698,16 +1888,29 @@ function addZones(number = 1) { const proper = getAdjective(Names.getCultureShort(cells.culture[cell])); const name = proper + " Tsunami"; - data.push({name, type:"Disaster", cells:cellsArray, fill:"url(#hatch13)"}); + data.push({name, type: "Disaster", cells: cellsArray, fill: "url(#hatch13)"}); } - void function drawZones() { - zones.selectAll("g").data(data).enter().append("g") - .attr("id", (d, i) => "zone"+i).attr("data-description", d => d.name).attr("data-type", d => d.type) - .attr("data-cells", d => d.cells.join(",")).attr("fill", d => d.fill) - .selectAll("polygon").data(d => d.cells).enter().append("polygon") - .attr("points", d => getPackPolygon(d)).attr("id", function(d) {return this.parentNode.id+"_"+d}); - }() + void (function drawZones() { + zones + .selectAll("g") + .data(data) + .enter() + .append("g") + .attr("id", (d, i) => "zone" + i) + .attr("data-description", d => d.name) + .attr("data-type", d => d.type) + .attr("data-cells", d => d.cells.join(",")) + .attr("fill", d => d.fill) + .selectAll("polygon") + .data(d => d.cells) + .enter() + .append("polygon") + .attr("points", d => getPackPolygon(d)) + .attr("id", function (d) { + return this.parentNode.id + "_" + d; + }); + })(); TIME && console.timeEnd("addZones"); } @@ -1722,19 +1925,19 @@ function showStatistics() { Points: ${grid.points.length} Cells: ${pack.cells.i.length} Map size: ${mapSizeOutput.value}% - States: ${pack.states.length-1} - Provinces: ${pack.provinces.length-1} - Burgs: ${pack.burgs.length-1} - Religions: ${pack.religions.length-1} + States: ${pack.states.length - 1} + Provinces: ${pack.provinces.length - 1} + Burgs: ${pack.burgs.length - 1} + Religions: ${pack.religions.length - 1} Culture set: ${culturesSet.selectedOptions[0].innerText} - Cultures: ${pack.cultures.length-1}`; + Cultures: ${pack.cultures.length - 1}`; mapId = Date.now(); // unique map id is it's creation date number - mapHistory.push({seed, width:graphWidth, height:graphHeight, template, created:mapId}); + mapHistory.push({seed, width: graphWidth, height: graphHeight, template, created: mapId}); INFO && console.log(stats); } -const regenerateMap = debounce(function() { +const regenerateMap = debounce(function () { WARN && console.warn("Generate new random map"); closeDialogs("#worldConfigurator, #options3d"); customization = 0; @@ -1749,7 +1952,10 @@ const regenerateMap = debounce(function() { // clear the map function undraw() { viewbox.selectAll("path, circle, polygon, line, text, use, #zones > g, #armies > g, #ruler > g").remove(); - document.getElementById("deftemp").querySelectorAll("path, clipPath, svg").forEach(el => el.remove()); + document + .getElementById("deftemp") + .querySelectorAll("path, clipPath, svg") + .forEach(el => el.remove()); document.getElementById("coas").innerHTML = ""; // remove auto-generated emblems notes = []; rulers = new Rulers(); diff --git a/modules/lakes.js b/modules/lakes.js index 7138cbf8..f4bc8c6f 100644 --- a/modules/lakes.js +++ b/modules/lakes.js @@ -1,90 +1,110 @@ (function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : - typeof define === 'function' && define.amd ? define(factory) : - (global.Lakes = factory()); -}(this, (function () {'use strict'; + typeof exports === "object" && typeof module !== "undefined" ? (module.exports = factory()) : typeof define === "function" && define.amd ? define(factory) : (global.Lakes = factory()); +})(this, function () { + "use strict"; -const setClimateData = function(h) { - const cells = pack.cells; - const lakeOutCells = new Uint16Array(cells.i.length); + const setClimateData = function (h) { + const cells = pack.cells; + const lakeOutCells = new Uint16Array(cells.i.length); - pack.features.forEach(f => { - if (f.type !== "lake") return; + pack.features.forEach(f => { + if (f.type !== "lake") return; - // default flux: sum of precipition around lake first cell - f.flux = rn(d3.sum(f.shoreline.map(c => grid.cells.prec[cells.g[c]])) / 2); - - // temperature and evaporation to detect closed lakes - f.temp = f.cells < 6 ? grid.cells.temp[cells.g[f.firstCell]] : rn(d3.mean(f.shoreline.map(c => grid.cells.temp[cells.g[c]])), 1); - const height = (f.height - 18) ** heightExponentInput.value; // height in meters - const evaporation = (700 * (f.temp + .006 * height) / 50 + 75) / (80 - f.temp); // based on Penman formula, [1-11] - f.evaporation = rn(evaporation * f.cells); + // default flux: sum of precipition around lake first cell + f.flux = rn(d3.sum(f.shoreline.map(c => grid.cells.prec[cells.g[c]])) / 2); - // lake outlet cell - f.outCell = f.shoreline[d3.scan(f.shoreline, (a,b) => h[a] - h[b])]; - lakeOutCells[f.outCell] = f.i; - }); + // temperature and evaporation to detect closed lakes + f.temp = f.cells < 6 ? grid.cells.temp[cells.g[f.firstCell]] : rn(d3.mean(f.shoreline.map(c => grid.cells.temp[cells.g[c]])), 1); + const height = (f.height - 18) ** heightExponentInput.value; // height in meters + const evaporation = ((700 * (f.temp + 0.006 * height)) / 50 + 75) / (80 - f.temp); // based on Penman formula, [1-11] + f.evaporation = rn(evaporation * f.cells); - return lakeOutCells; -} + // no outlet for lakes in depressed areas + if (f.closed) return; -const cleanupLakeData = function() { - for (const feature of pack.features) { - if (feature.type !== "lake") continue; - delete feature.river; - delete feature.enteringFlux; - delete feature.shoreline; - delete feature.outCell; - feature.height = rn(feature.height); + // lake outlet cell + f.outCell = f.shoreline[d3.scan(f.shoreline, (a, b) => h[a] - h[b])]; + lakeOutCells[f.outCell] = f.i; + }); - const inlets = feature.inlets?.filter(r => pack.rivers.find(river => river.i === r)); - if (!inlets || !inlets.length) delete feature.inlets; - else feature.inlets = inlets; + return lakeOutCells; + }; - const outlet = feature.outlet && pack.rivers.find(river => river.i === feature.outlet); - if (!outlet) delete feature.outlet; - } -} + // get array of land cells aroound lake + const getShoreline = function (lake) { + const queue = [lake.firstCell]; + const used = [queue[0]]; + const landCellsAround = []; + while (queue.length) { + const q = queue.pop(); + for (const c of pack.cells.c[q]) { + if (used[c]) continue; + used[c] = true; + if (pack.cells.f[c] === lake.i) queue.push(c); + if (pack.cells.h[c] >= 20) landCellsAround.push(c); + } + } -const defineGroup = function() { - for (const feature of pack.features) { - if (feature.type !== "lake") continue; - const lakeEl = lakes.select(`[data-f="${feature.i}"]`).node(); - if (!lakeEl) continue; + lake.shoreline = landCellsAround; + }; - feature.group = getGroup(feature); - document.getElementById(feature.group).appendChild(lakeEl); - } -} + const cleanupLakeData = function () { + for (const feature of pack.features) { + if (feature.type !== "lake") continue; + delete feature.river; + delete feature.enteringFlux; + delete feature.shoreline; + delete feature.outCell; + delete feature.closed; + feature.height = rn(feature.height); -const generateName = function() { - Math.random = aleaPRNG(seed); - for (const feature of pack.features) { - if (feature.type !== "lake") continue; - feature.name = getName(feature); - } -} + const inlets = feature.inlets?.filter(r => pack.rivers.find(river => river.i === r)); + if (!inlets || !inlets.length) delete feature.inlets; + else feature.inlets = inlets; -const getName = function(feature) { - const landCell = pack.cells.c[feature.firstCell].find(c => pack.cells.h[c] >= 20); - const culture = pack.cells.culture[landCell]; - return Names.getCulture(culture); -} + const outlet = feature.outlet && pack.rivers.find(river => river.i === feature.outlet); + if (!outlet) delete feature.outlet; + } + }; -function getGroup(feature) { - if (feature.temp < -3) return "frozen"; - if (feature.height > 60 && feature.cells < 10 && feature.firstCell % 5 === 0) return "lava"; + const defineGroup = function () { + for (const feature of pack.features) { + if (feature.type !== "lake") continue; + const lakeEl = lakes.select(`[data-f="${feature.i}"]`).node(); + if (!lakeEl) continue; - if (!feature.inlets && !feature.outlet) { - if (feature.evaporation / 2 > feature.flux) return "dry"; - if (feature.cells < 3 && feature.firstCell % 5 === 0) return "sinkhole"; + feature.group = getGroup(feature); + document.getElementById(feature.group).appendChild(lakeEl); + } + }; + + const generateName = function () { + Math.random = aleaPRNG(seed); + for (const feature of pack.features) { + if (feature.type !== "lake") continue; + feature.name = getName(feature); + } + }; + + const getName = function (feature) { + const landCell = pack.cells.c[feature.firstCell].find(c => pack.cells.h[c] >= 20); + const culture = pack.cells.culture[landCell]; + return Names.getCulture(culture); + }; + + function getGroup(feature) { + if (feature.temp < -3) return "frozen"; + if (feature.height > 60 && feature.cells < 10 && feature.firstCell % 5 === 0) return "lava"; + + if (!feature.inlets && !feature.outlet) { + if (feature.evaporation / 2 > feature.flux) return "dry"; + if (feature.cells < 3 && feature.firstCell % 5 === 0) return "sinkhole"; + } + + if (!feature.outlet && feature.evaporation > feature.flux) return "salt"; + + return "freshwater"; } - if (!feature.outlet && feature.evaporation > feature.flux) return "salt"; - - return "freshwater"; -} - -return {setClimateData, cleanupLakeData, defineGroup, generateName, getName}; - -}))); \ No newline at end of file + return {setClimateData, cleanupLakeData, defineGroup, generateName, getName, getShoreline}; +}); diff --git a/modules/river-generator.js b/modules/river-generator.js index e231871e..28e29dd9 100644 --- a/modules/river-generator.js +++ b/modules/river-generator.js @@ -1,355 +1,389 @@ (function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : - typeof define === 'function' && define.amd ? define(factory) : - (global.Rivers = factory()); -}(this, (function () {'use strict'; + typeof exports === "object" && typeof module !== "undefined" ? (module.exports = factory()) : typeof define === "function" && define.amd ? define(factory) : (global.Rivers = factory()); +})(this, function () { + "use strict"; -const generate = function(changeHeights = true) { - TIME && console.time('generateRivers'); - Math.random = aleaPRNG(seed); - const cells = pack.cells, p = cells.p, features = pack.features; + const generate = function (changeHeights = true) { + TIME && console.time("generateRivers"); + Math.random = aleaPRNG(seed); + const cells = pack.cells, + p = cells.p, + features = pack.features; - const riversData = []; // rivers data - cells.fl = new Uint16Array(cells.i.length); // water flux array - cells.r = new Uint16Array(cells.i.length); // rivers array - cells.conf = new Uint8Array(cells.i.length); // confluences array - let riverNext = 1; // first river id is 1 + const riversData = []; // rivers data + cells.fl = new Uint16Array(cells.i.length); // water flux array + cells.r = new Uint16Array(cells.i.length); // rivers array + cells.conf = new Uint8Array(cells.i.length); // confluences array + let riverNext = 1; // first river id is 1 - const h = alterHeights(); - removeStoredLakeData(); - resolveDepressions(h); - drainWater(); - defineRivers(); - Lakes.cleanupLakeData(); + const h = alterHeights(); + prepareLakeData(); + resolveDepressions(h, 200); + drainWater(); + defineRivers(); + Lakes.cleanupLakeData(); - if (changeHeights) cells.h = Uint8Array.from(h); // apply changed heights as basic one + if (changeHeights) cells.h = Uint8Array.from(h); // apply changed heights as basic one - TIME && console.timeEnd('generateRivers'); + TIME && console.timeEnd("generateRivers"); - // height with added t value to make map less depressed - function alterHeights() { - const h = Array.from(cells.h) - .map((h, i) => h < 20 || cells.t[i] < 1 ? h : h + cells.t[i] / 100) - .map((h, i) => h < 20 || cells.t[i] < 1 ? h : h + d3.mean(cells.c[i].map(c => cells.t[c])) / 10000); - return h; - } + function prepareLakeData() { + features.forEach(f => { + if (f.type !== "lake") return; + delete f.flux; + delete f.inlets; + delete f.outlet; + delete f.height; + !f.shoreline && Lakes.getShoreline(f); + }); + } - function removeStoredLakeData() { - features.forEach(f => { - delete f.flux; - delete f.inlets; - delete f.outlet; - delete f.height; - }); - } + function drainWater() { + const MIN_FLUX_TO_FORM_RIVER = 30; + const land = cells.i.filter(i => h[i] >= 20).sort((a, b) => h[b] - h[a]); + const lakeOutCells = Lakes.setClimateData(h); - function drainWater() { - const MIN_FLUX_TO_FORM_RIVER = 30; - const land = cells.i.filter(i => h[i] >= 20).sort((a,b) => h[b] - h[a]); - const lakeOutCells = Lakes.setClimateData(h); + land.forEach(function (i) { + cells.fl[i] += grid.cells.prec[cells.g[i]]; // flux from precipitation + const [x, y] = p[i]; - land.forEach(function(i) { - cells.fl[i] += grid.cells.prec[cells.g[i]]; // flux from precipitation - const x = p[i][0], y = p[i][1]; + // create lake outlet if lake is not in deep depression and flux > evaporation + const lakes = lakeOutCells[i] ? features.filter(feature => i === feature.outCell && feature.flux > feature.evaporation) : []; + for (const lake of lakes) { + const lakeCell = cells.c[i].find(c => h[c] < 20 && cells.f[c] === lake.i); - // create lake outlet if flux > evaporation - const lakes = !lakeOutCells[i] ? [] : features.filter(feature => i === feature.outCell && feature.flux > feature.evaporation); - for (const lake of lakes) { - const lakeCell = cells.c[i].find(c => h[c] < 20 && cells.f[c] === lake.i); + cells.fl[lakeCell] += Math.max(lake.flux - lake.evaporation, 0); // not evaporated lake water drains to outlet - cells.fl[lakeCell] += Math.max(lake.flux - lake.evaporation, 0); // not evaporated lake water drains to outlet - - // allow chain lakes to retain identity - if (cells.r[lakeCell] !== lake.river) { - const sameRiver = cells.c[lakeCell].some(c => cells.r[c] === lake.river); - if (sameRiver) { - cells.r[lakeCell] = lake.river; - riversData.push({river: lake.river, cell: lakeCell, x: p[lakeCell][0], y: p[lakeCell][1], flux: cells.fl[lakeCell]}); - } else { - cells.r[lakeCell] = riverNext; - riversData.push({river: riverNext, cell: lakeCell, x: p[lakeCell][0], y: p[lakeCell][1], flux: cells.fl[lakeCell]}); - riverNext++; + // allow chain lakes to retain identity + if (cells.r[lakeCell] !== lake.river) { + const sameRiver = cells.c[lakeCell].some(c => cells.r[c] === lake.river); + if (sameRiver) { + cells.r[lakeCell] = lake.river; + riversData.push({river: lake.river, cell: lakeCell, x: p[lakeCell][0], y: p[lakeCell][1], flux: cells.fl[lakeCell]}); + } else { + cells.r[lakeCell] = riverNext; + riversData.push({river: riverNext, cell: lakeCell, x: p[lakeCell][0], y: p[lakeCell][1], flux: cells.fl[lakeCell]}); + riverNext++; + } } + + lake.outlet = cells.r[lakeCell]; + flowDown(i, cells.fl[i], cells.fl[lakeCell], lake.outlet); } - lake.outlet = cells.r[lakeCell]; - flowDown(i, cells.fl[i], cells.fl[lakeCell], lake.outlet); + // assign all tributary rivers to outlet basin + for (let outlet = lakes[0]?.outlet, l = 0; l < lakes.length; l++) { + lakes[l].inlets?.forEach(fork => (riversData.find(r => r.river === fork).parent = outlet)); + } + + // near-border cell: pour water out of the screen + if (cells.b[i] && cells.r[i]) { + let to = []; + const min = Math.min(y, graphHeight - y, x, graphWidth - x); + if (min === y) to = [x, 0]; + else if (min === graphHeight - y) to = [x, graphHeight]; + else if (min === x) to = [0, y]; + else if (min === graphWidth - x) to = [graphWidth, y]; + riversData.push({river: cells.r[i], cell: i, x: to[0], y: to[1], flux: cells.fl[i]}); + return; + } + + // downhill cell (make sure it's not in the source lake) + const min = lakeOutCells[i] ? cells.c[i].filter(c => !lakes.map(lake => lake.i).includes(cells.f[c])).sort((a, b) => h[a] - h[b])[0] : cells.c[i].sort((a, b) => h[a] - h[b])[0]; + + if (cells.fl[i] < MIN_FLUX_TO_FORM_RIVER) { + if (h[min] >= 20) cells.fl[min] += cells.fl[i]; + return; // flux is too small to operate as river + } + + // proclaim a new river + if (!cells.r[i]) { + cells.r[i] = riverNext; + riversData.push({river: riverNext, cell: i, x, y, flux: cells.fl[i]}); + riverNext++; + } + + flowDown(min, cells.fl[min], cells.fl[i], cells.r[i], i); + }); + } + + function flowDown(toCell, toFlux, fromFlux, river, fromCell = 0) { + if (cells.r[toCell]) { + // downhill cell already has river assigned + if (toFlux < fromFlux) { + cells.conf[toCell] = cells.fl[toCell]; // mark confluence + if (h[toCell] >= 20) riversData.find(r => r.river === cells.r[toCell]).parent = river; // min river is a tributary of current river + cells.r[toCell] = river; // re-assign river if downhill part has less flux + } else { + cells.conf[toCell] += fromFlux; // mark confluence + if (h[toCell] >= 20) riversData.find(r => r.river === river).parent = cells.r[toCell]; // current river is a tributary of min river + } + } else cells.r[toCell] = river; // assign the river to the downhill cell + + if (h[toCell] < 20) { + // pour water to the water body + const haven = fromCell ? cells.haven[fromCell] : toCell; + riversData.push({river, cell: haven, x: p[toCell][0], y: p[toCell][1], flux: fromFlux}); + + const waterBody = features[cells.f[toCell]]; + if (waterBody.type === "lake") { + if (!waterBody.river || fromFlux > waterBody.enteringFlux) { + waterBody.river = river; + waterBody.enteringFlux = fromFlux; + } + waterBody.flux = waterBody.flux + fromFlux; + waterBody.inlets ? waterBody.inlets.push(river) : (waterBody.inlets = [river]); + } + } else { + // propagate flux and add next river segment + cells.fl[toCell] += fromFlux; + riversData.push({river, cell: toCell, x: p[toCell][0], y: p[toCell][1], flux: fromFlux}); + } + } + + function defineRivers() { + cells.r = new Uint16Array(cells.i.length); // re-initiate rivers array + pack.rivers = []; // rivers data + const riverPaths = []; + + for (let r = 1; r <= riverNext; r++) { + const riverSegments = riversData.filter(d => d.river === r); + if (riverSegments.length < 3) continue; + + for (const segment of riverSegments) { + const i = segment.cell; + if (cells.r[i]) continue; + if (cells.h[i] < 20) continue; + cells.r[i] = r; + } + + const source = riverSegments[0].cell; + const mouth = riverSegments[riverSegments.length - 2].cell; + + const widthFactor = rn(0.8 + Math.random() * 0.4, 1); // river width modifier [.8, 1.2] + const sourceWidth = cells.h[source] >= 20 ? 0.1 : rn(Math.min(Math.max((cells.fl[source] / 500) ** 0.4, 0.5), 1.7), 2); + + const riverMeandered = addMeandering(riverSegments, sourceWidth * 10, 0.5); + const [path, length, offset] = getPath(riverMeandered, widthFactor, sourceWidth); + riverPaths.push([path, r]); + + const parent = riverSegments[0].parent || 0; + const width = rn(offset ** 2, 2); // mounth width in km + const discharge = last(riverSegments).flux; // in m3/s + pack.rivers.push({i: r, source, mouth, discharge, length, width, widthFactor, sourceWidth, parent}); } - // assign all tributary rivers to outlet basin - for (let outlet = lakes[0]?.outlet, l = 0; l < lakes.length; l++) { - lakes[l].inlets?.forEach(fork => riversData.find(r => r.river === fork).parent = outlet); - } + // draw rivers + rivers.html(riverPaths.map(d => ``).join("")); + } + }; - // near-border cell: pour water out of the screen - if (cells.b[i] && cells.r[i]) { - const to = []; - const min = Math.min(y, graphHeight - y, x, graphWidth - x); - if (min === y) {to[0] = x; to[1] = 0;} else - if (min === graphHeight - y) {to[0] = x; to[1] = graphHeight;} else - if (min === x) {to[0] = 0; to[1] = y;} else - if (min === graphWidth - x) {to[0] = graphWidth; to[1] = y;} - riversData.push({river: cells.r[i], cell: i, x: to[0], y: to[1], flux: cells.fl[i]}); - return; - } - - // downhill cell (make sure it's not in the source lake) - const min = lakeOutCells[i] - ? cells.c[i].filter(c => !lakes.map(lake => lake.i).includes(cells.f[c])).sort((a, b) => h[a] - h[b])[0] - : cells.c[i].sort((a, b) => h[a] - h[b])[0]; - - if (cells.fl[i] < MIN_FLUX_TO_FORM_RIVER) { - if (h[min] >= 20) cells.fl[min] += cells.fl[i]; - return; // flux is too small to operate as river - } - - // proclaim a new river - if (!cells.r[i]) { - cells.r[i] = riverNext; - riversData.push({river: riverNext, cell: i, x, y, flux: cells.fl[i]}); - riverNext++; - } - - flowDown(min, cells.fl[min], cells.fl[i], cells.r[i], i); + // add distance to water value to land cells to make map less depressed + function alterHeights() { + const cells = pack.cells; + return Array.from(cells.h).map((h, i) => { + if (h < 20 || cells.t[i] < 1) return h; + return h + cells.t[i] / 100 + d3.mean(cells.c[i].map(c => cells.t[c])) / 10000; }); } - function flowDown(toCell, toFlux, fromFlux, river, fromCell = 0) { - if (cells.r[toCell]) { - // downhill cell already has river assigned - if (toFlux < fromFlux) { - cells.conf[toCell] = cells.fl[toCell]; // mark confluence - if (h[toCell] >= 20) riversData.find(r => r.river === cells.r[toCell]).parent = river; // min river is a tributary of current river - cells.r[toCell] = river; // re-assign river if downhill part has less flux - } else { - cells.conf[toCell] += fromFlux; // mark confluence - if (h[toCell] >= 20) riversData.find(r => r.river === river).parent = cells.r[toCell]; // current river is a tributary of min river + // depression filling algorithm (for a correct water flux modeling) + const resolveDepressions = function (h, maxIterations) { + const {cells, features} = pack; + const height = i => features[cells.f[i]].height || h[i]; // height of lake or specific cell + + const lakes = features.filter(f => f.type === "lake"); + const land = cells.i.filter(i => h[i] >= 20 && !cells.b[i]); // exclude near-border cells + land.sort((a, b) => h[b] - h[a]); // highest cells go first + + const progress = []; + let depressions = Infinity; + let prevDepressions = null; + for (let iteration = 0; depressions && iteration < maxIterations; iteration++) { + if (progress.length > 5 && d3.sum(progress) > 0) { + // bad progress, abort and set heights back + h = alterHeights(); + depressions = progress[0]; + break; } - } else cells.r[toCell] = river; // assign the river to the downhill cell - if (h[toCell] < 20) { - // pour water to the water body - const haven = fromCell ? cells.haven[fromCell] : toCell; - riversData.push({river, cell: haven, x: p[toCell][0], y: p[toCell][1], flux: fromFlux}); + depressions = 0; - const waterBody = features[cells.f[toCell]]; - if (waterBody.type === "lake") { - if (!waterBody.river || fromFlux > waterBody.enteringFlux) { - waterBody.river = river; - waterBody.enteringFlux = fromFlux; + if (iteration < 180) { + for (const l of lakes) { + if (l.closed) continue; + const minHeight = d3.min(l.shoreline.map(s => h[s])); + if (minHeight >= 100 || l.height > minHeight) continue; + + if (iteration > 150) { + l.shoreline.forEach(i => (h[i] = cells.h[i])); + l.height = d3.min(l.shoreline.map(s => h[s])) - 1; + l.closed = true; + continue; + } + + depressions++; + l.height = minHeight + 0.2; } - waterBody.flux = waterBody.flux + fromFlux; - waterBody.inlets ? waterBody.inlets.push(river) : waterBody.inlets = [river]; - } - } else { - // propagate flux and add next river segment - cells.fl[toCell] += fromFlux; - riversData.push({river, cell: toCell, x: p[toCell][0], y: p[toCell][1], flux: fromFlux}); - } - } - - function defineRivers() { - cells.r = new Uint16Array(cells.i.length); // re-initiate rivers array - pack.rivers = []; // rivers data - const riverPaths = []; - - for (let r = 1; r <= riverNext; r++) { - const riverSegments = riversData.filter(d => d.river === r); - if (riverSegments.length < 3) continue; - - for (const segment of riverSegments) { - const i = segment.cell; - if (cells.r[i]) continue; - if (cells.h[i] < 20) continue; - cells.r[i] = r; } - const source = riverSegments[0].cell; - const mouth = riverSegments[riverSegments.length-2].cell; + for (const i of land) { + const minHeight = d3.min(cells.c[i].map(c => height(c))); + if (minHeight >= 100 || h[i] > minHeight) continue; - const widthFactor = rn(.8 + Math.random() * .4, 1); // river width modifier [.8, 1.2] - const sourceWidth = cells.h[source] >= 20 ? .1 : rn(Math.min(Math.max((cells.fl[source] / 500) ** .4, .5), 1.7), 2); + depressions++; + h[i] = minHeight + 0.1; + } - const riverMeandered = addMeandering(riverSegments, sourceWidth * 10, .5); - const [path, length, offset] = getPath(riverMeandered, widthFactor, sourceWidth); - riverPaths.push([path, r]); - - const parent = riverSegments[0].parent || 0; - const width = rn(offset ** 2, 2); // mounth width in km - const discharge = last(riverSegments).flux; // in m3/s - pack.rivers.push({i:r, source, mouth, discharge, length, width, widthFactor, sourceWidth, parent}); + prevDepressions !== null && progress.push(depressions - prevDepressions); + prevDepressions = depressions; } - // draw rivers - rivers.html(riverPaths.map(d => ``).join("")); - } -} + if (!depressions) return; + WARN && console.warn(`Unresolved depressions: ${depressions}. Edit heightmap to fix`); + //const flow = cells.i.length < 65535 ? new Uint16Array(cells.i.length) : new Uint32Array(cells.i.length); + //flow[i] = min; + //debug.append("path").attr("class", "arrow").attr("d", `M${cells.p[i][0]},${cells.p[i][1]}L${cells.p[min][0]},${cells.p[min][1]}`); + }; -// depression filling algorithm (for a correct water flux modeling) -const resolveDepressions = function(h) { - const {cells, features} = pack; - const ITERATIONS = 150; + // add more river points on 1/3 and 2/3 of length + const addMeandering = function (segments, width = 1, meandering = 0.5) { + const riverMeandered = []; // to store enhanced segments - const lakes = features.filter(f => f.type === "lake"); - lakes.forEach(l => { - const uniqueCells = new Set(); - l.vertices.forEach(v => pack.vertices.c[v].forEach(c => cells.h[c] >= 20 && uniqueCells.add(c))); - l.shoreline = [...uniqueCells]; - }); + for (let s = 0; s < segments.length; s++, width++) { + const sX = segments[s].x, + sY = segments[s].y; // segment start coordinates + const c = pack.cells.conf[segments[s].cell] || 0; // if segment is river confluence + riverMeandered.push([sX, sY, c]); - const land = cells.i.filter(i => h[i] >= 20 && !cells.b[i]); // exclude near-border cells - land.sort((a,b) => h[b] - h[a]); // highest cells go first + if (s + 1 === segments.length) break; // do not meander last segment - let depressions = Infinity; - for (let l = 0; depressions && l < ITERATIONS; l++) { - depressions = 0; + const eX = segments[s + 1].x, + eY = segments[s + 1].y; // segment end coordinates + const angle = Math.atan2(eY - sY, eX - sX); + const sin = Math.sin(angle), + cos = Math.cos(angle); - for (const l of lakes) { - const minHeight = d3.min(l.shoreline.map(s => h[s])); - if (minHeight >= 100 || l.height > minHeight) continue; - l.height = minHeight + 1; - depressions++; + const meander = meandering + 1 / width + Math.random() * Math.max(meandering - width / 100, 0); + const dist2 = (eX - sX) ** 2 + (eY - sY) ** 2; // square distance between segment start and end + + if (width < 10 && (dist2 > 64 || (dist2 > 36 && segments.length < 6))) { + // if dist2 is big or river is small add extra points at 1/3 and 2/3 of segment + const p1x = (sX * 2 + eX) / 3 + -sin * meander; + const p1y = (sY * 2 + eY) / 3 + cos * meander; + const p2x = (sX + eX * 2) / 3 + sin * meander; + const p2y = (sY + eY * 2) / 3 + cos * meander; + riverMeandered.push([p1x, p1y], [p2x, p2y]); + } else if (dist2 > 25 || segments.length < 6) { + // if dist is medium or river is small add 1 extra middlepoint + const p1x = (sX + eX) / 2 + -sin * meander; + const p1y = (sY + eY) / 2 + cos * meander; + riverMeandered.push([p1x, p1y]); + } } - for (const i of land) { - const minHeight = d3.min(cells.c[i].map(c => cells.t[c] > 0 ? h[c] : pack.features[cells.f[c]].height || h[c])); - if (minHeight >= 100 || h[i] > minHeight) continue; - h[i] = minHeight + 1; - depressions++; - } - } + return riverMeandered; + }; - depressions && ERROR && console.error("Heightmap is depressed. Issues with rivers expected. Remove depressed areas to resolve"); -} + const getPath = function (points, widthFactor = 1, sourceWidth = 0.1) { + let offset, + extraOffset = sourceWidth; // starting river width (to make river source visible) + const riverLength = points.reduce((s, v, i, p) => s + (i ? Math.hypot(v[0] - p[i - 1][0], v[1] - p[i - 1][1]) : 0), 0); // summ of segments length + const widening = 1000 + riverLength * 30; + const riverPointsLeft = [], + riverPointsRight = []; // store points on both sides to build a valid polygon + const last = points.length - 1; + const factor = riverLength / points.length; -// add more river points on 1/3 and 2/3 of length -const addMeandering = function(segments, width = 1, meandering = .5) { - const riverMeandered = []; // to store enhanced segments - - for (let s = 0; s < segments.length; s++, width++) { - const sX = segments[s].x, sY = segments[s].y; // segment start coordinates - const c = pack.cells.conf[segments[s].cell] || 0; // if segment is river confluence - riverMeandered.push([sX, sY, c]); - - if (s+1 === segments.length) break; // do not meander last segment - - const eX = segments[s+1].x, eY = segments[s+1].y; // segment end coordinates - const angle = Math.atan2(eY - sY, eX - sX); - const sin = Math.sin(angle), cos = Math.cos(angle); - - const meander = meandering + 1 / width + Math.random() * Math.max(meandering - width / 100, 0); - const dist2 = (eX - sX) ** 2 + (eY - sY) ** 2; // square distance between segment start and end - - if (width < 10 && (dist2 > 64 || (dist2 > 36 && segments.length < 6))) { - // if dist2 is big or river is small add extra points at 1/3 and 2/3 of segment - const p1x = (sX * 2 + eX) / 3 + -sin * meander; - const p1y = (sY * 2 + eY) / 3 + cos * meander; - const p2x = (sX + eX * 2) / 3 + sin * meander; - const p2y = (sY + eY * 2) / 3 + cos * meander; - riverMeandered.push([p1x, p1y], [p2x, p2y]); - } else if (dist2 > 25 || segments.length < 6) { - // if dist is medium or river is small add 1 extra middlepoint - const p1x = (sX + eX) / 2 + -sin * meander; - const p1y = (sY + eY) / 2 + cos * meander; - riverMeandered.push([p1x, p1y]); - } - - } - - return riverMeandered; -} - -const getPath = function(points, widthFactor = 1, sourceWidth = .1) { - let offset, extraOffset = sourceWidth; // starting river width (to make river source visible) - const riverLength = points.reduce((s, v, i, p) => s + (i ? Math.hypot(v[0] - p[i-1][0], v[1] - p[i-1][1]) : 0), 0); // summ of segments length - const widening = 1000 + riverLength * 30; - const riverPointsLeft = [], riverPointsRight = []; // store points on both sides to build a valid polygon - const last = points.length - 1; - const factor = riverLength / points.length; - - // first point - let x = points[0][0], y = points[0][1], c; - let angle = Math.atan2(y - points[1][1], x - points[1][0]); - let sin = Math.sin(angle), cos = Math.cos(angle); - let xLeft = x + -sin * extraOffset, yLeft = y + cos * extraOffset; - riverPointsLeft.push([xLeft, yLeft]); - let xRight = x + sin * extraOffset, yRight = y + -cos * extraOffset; - riverPointsRight.unshift([xRight, yRight]); - - // middle points - for (let p = 1; p < last; p++) { - x = points[p][0], y = points[p][1], c = points[p][2] || 0; - const xPrev = points[p-1][0], yPrev = points[p - 1][1]; - const xNext = points[p+1][0], yNext = points[p + 1][1]; - angle = Math.atan2(yPrev - yNext, xPrev - xNext); - sin = Math.sin(angle), cos = Math.cos(angle); - offset = (Math.atan(Math.pow(p * factor, 2) / widening) / 2 * widthFactor) + extraOffset; - const confOffset = Math.atan(c * 5 / widening); - extraOffset += confOffset; - xLeft = x + -sin * offset, yLeft = y + cos * (offset + confOffset); + // first point + let x = points[0][0], + y = points[0][1], + c; + let angle = Math.atan2(y - points[1][1], x - points[1][0]); + let sin = Math.sin(angle), + cos = Math.cos(angle); + let xLeft = x + -sin * extraOffset, + yLeft = y + cos * extraOffset; riverPointsLeft.push([xLeft, yLeft]); - xRight = x + sin * offset, yRight = y + -cos * offset; + let xRight = x + sin * extraOffset, + yRight = y + -cos * extraOffset; riverPointsRight.unshift([xRight, yRight]); - } - // end point - x = points[last][0], y = points[last][1], c = points[last][2]; - if (c) extraOffset += Math.atan(c * 10 / widening); // add extra width on river confluence - angle = Math.atan2(points[last-1][1] - y, points[last-1][0] - x); - sin = Math.sin(angle), cos = Math.cos(angle); - xLeft = x + -sin * offset, yLeft = y + cos * offset; - riverPointsLeft.push([xLeft, yLeft]); - xRight = x + sin * offset, yRight = y + -cos * offset; - riverPointsRight.unshift([xRight, yRight]); + // middle points + for (let p = 1; p < last; p++) { + (x = points[p][0]), (y = points[p][1]), (c = points[p][2] || 0); + const xPrev = points[p - 1][0], + yPrev = points[p - 1][1]; + const xNext = points[p + 1][0], + yNext = points[p + 1][1]; + angle = Math.atan2(yPrev - yNext, xPrev - xNext); + (sin = Math.sin(angle)), (cos = Math.cos(angle)); + offset = (Math.atan(Math.pow(p * factor, 2) / widening) / 2) * widthFactor + extraOffset; + const confOffset = Math.atan((c * 5) / widening); + extraOffset += confOffset; + (xLeft = x + -sin * offset), (yLeft = y + cos * (offset + confOffset)); + riverPointsLeft.push([xLeft, yLeft]); + (xRight = x + sin * offset), (yRight = y + -cos * offset); + riverPointsRight.unshift([xRight, yRight]); + } - // generate polygon path and return - lineGen.curve(d3.curveCatmullRom.alpha(0.1)); - const right = lineGen(riverPointsRight); - let left = lineGen(riverPointsLeft); - left = left.substring(left.indexOf("C")); - return [round(right + left, 2), rn(riverLength, 2), offset]; -} + // end point + (x = points[last][0]), (y = points[last][1]), (c = points[last][2]); + if (c) extraOffset += Math.atan((c * 10) / widening); // add extra width on river confluence + angle = Math.atan2(points[last - 1][1] - y, points[last - 1][0] - x); + (sin = Math.sin(angle)), (cos = Math.cos(angle)); + (xLeft = x + -sin * offset), (yLeft = y + cos * offset); + riverPointsLeft.push([xLeft, yLeft]); + (xRight = x + sin * offset), (yRight = y + -cos * offset); + riverPointsRight.unshift([xRight, yRight]); -const specify = function() { - const rivers = pack.rivers; - if (!rivers.length) return; - Math.random = aleaPRNG(seed); - const tresholdElement = Math.ceil(rivers.length * .15); - const smallLength = rivers.map(r => r.length || 0).sort((a, b) => a-b)[tresholdElement]; - const smallType = {"Creek":9, "River":3, "Brook":3, "Stream":1}; // weighted small river types + // generate polygon path and return + lineGen.curve(d3.curveCatmullRom.alpha(0.1)); + const right = lineGen(riverPointsRight); + let left = lineGen(riverPointsLeft); + left = left.substring(left.indexOf("C")); + return [round(right + left, 2), rn(riverLength, 2), offset]; + }; - for (const r of rivers) { - r.basin = getBasin(r.i); - r.name = getName(r.mouth); - const small = r.length < smallLength; - r.type = r.parent && !(r.i%6) ? small ? "Branch" : "Fork" : small ? rw(smallType) : "River"; - } -} + const specify = function () { + const rivers = pack.rivers; + if (!rivers.length) return; + Math.random = aleaPRNG(seed); + const tresholdElement = Math.ceil(rivers.length * 0.15); + const smallLength = rivers.map(r => r.length || 0).sort((a, b) => a - b)[tresholdElement]; + const smallType = {Creek: 9, River: 3, Brook: 3, Stream: 1}; // weighted small river types -const getName = function(cell) { - return Names.getCulture(pack.cells.culture[cell]); -} + for (const r of rivers) { + r.basin = getBasin(r.i); + r.name = getName(r.mouth); + const small = r.length < smallLength; + r.type = r.parent && !(r.i % 6) ? (small ? "Branch" : "Fork") : small ? rw(smallType) : "River"; + } + }; -// remove river and all its tributaries -const remove = function(id) { - const cells = pack.cells; - const riversToRemove = pack.rivers.filter(r => r.i === id || r.parent === id || r.basin === id).map(r => r.i); - riversToRemove.forEach(r => rivers.select("#river"+r).remove()); - cells.r.forEach((r, i) => { - if (!r || !riversToRemove.includes(r)) return; - cells.r[i] = 0; - cells.fl[i] = grid.cells.prec[cells.g[i]]; - cells.conf[i] = 0; - }); - pack.rivers = pack.rivers.filter(r => !riversToRemove.includes(r.i)); -} + const getName = function (cell) { + return Names.getCulture(pack.cells.culture[cell]); + }; -const getBasin = function(r) { - const parent = pack.rivers.find(river => river.i === r)?.parent; - if (!parent || r === parent) return r; - return getBasin(parent); -} + // remove river and all its tributaries + const remove = function (id) { + const cells = pack.cells; + const riversToRemove = pack.rivers.filter(r => r.i === id || r.parent === id || r.basin === id).map(r => r.i); + riversToRemove.forEach(r => rivers.select("#river" + r).remove()); + cells.r.forEach((r, i) => { + if (!r || !riversToRemove.includes(r)) return; + cells.r[i] = 0; + cells.fl[i] = grid.cells.prec[cells.g[i]]; + cells.conf[i] = 0; + }); + pack.rivers = pack.rivers.filter(r => !riversToRemove.includes(r.i)); + }; -return {generate, resolveDepressions, addMeandering, getPath, specify, getName, getBasin, remove}; + const getBasin = function (r) { + const parent = pack.rivers.find(river => river.i === r)?.parent; + if (!parent || r === parent) return r; + return getBasin(parent); + }; -}))); \ No newline at end of file + return {generate, resolveDepressions, addMeandering, getPath, specify, getName, getBasin, remove}; +}); diff --git a/modules/save-and-load.js b/modules/save-and-load.js index f71fbc99..e30da762 100644 --- a/modules/save-and-load.js +++ b/modules/save-and-load.js @@ -27,19 +27,19 @@ async function savePNG() { const img = new Image(); img.src = url; - img.onload = function() { + img.onload = function () { ctx.drawImage(img, 0, 0, canvas.width, canvas.height); link.download = getFileName() + ".png"; - canvas.toBlob(function(blob) { - link.href = window.URL.createObjectURL(blob); - link.click(); - window.setTimeout(function() { - canvas.remove(); - window.URL.revokeObjectURL(link.href); - tip(`${link.download} is saved. Open "Downloads" screen (crtl + J) to check. You can set image scale in options`, true, "success", 5000); - }, 1000); + canvas.toBlob(function (blob) { + link.href = window.URL.createObjectURL(blob); + link.click(); + window.setTimeout(function () { + canvas.remove(); + window.URL.revokeObjectURL(link.href); + tip(`${link.download} is saved. Open "Downloads" screen (crtl + J) to check. You can set image scale in options`, true, "success", 5000); + }, 1000); }); - } + }; TIME && console.timeEnd("savePNG"); } @@ -55,9 +55,9 @@ async function saveJPEG() { const img = new Image(); img.src = url; - img.onload = async function() { + img.onload = async function () { canvas.getContext("2d").drawImage(img, 0, 0, canvas.width, canvas.height); - const quality = Math.min(rn(1 - pngResolutionInput.value / 20, 2), .92); + const quality = Math.min(rn(1 - pngResolutionInput.value / 20, 2), 0.92); const URL = await canvas.toDataURL("image/jpeg", quality); const link = document.createElement("a"); link.download = getFileName() + ".jpeg"; @@ -65,7 +65,7 @@ async function saveJPEG() { link.click(); tip(`${link.download} is saved. Open "Downloads" screen (CTRL + J) to check`, true, "success", 7000); window.setTimeout(() => window.URL.revokeObjectURL(URL), 5000); - } + }; TIME && console.timeEnd("saveJPEG"); } @@ -81,7 +81,7 @@ async function getMapURL(type, subtype) { const cloneDefs = cloneEl.getElementsByTagName("defs")[0]; const svgDefs = document.getElementById("defElements"); - const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; + const isFirefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1; if (isFirefox && type === "mesh") clone.select("#oceanPattern").remove(); if (subtype === "globe") clone.select("#scaleBar").remove(); if (subtype === "noWater") { @@ -99,37 +99,40 @@ async function getMapURL(type, subtype) { // remove unused filters const filters = cloneEl.querySelectorAll("filter"); - for (let i=0; i < filters.length; i++) { + for (let i = 0; i < filters.length; i++) { const id = filters[i].id; - if (cloneEl.querySelector("[filter='url(#"+id+")']")) continue; - if (cloneEl.getAttribute("filter") === "url(#"+id+")") continue; + if (cloneEl.querySelector("[filter='url(#" + id + ")']")) continue; + if (cloneEl.getAttribute("filter") === "url(#" + id + ")") continue; filters[i].remove(); } // remove unused patterns const patterns = cloneEl.querySelectorAll("pattern"); - for (let i=0; i < patterns.length; i++) { + for (let i = 0; i < patterns.length; i++) { const id = patterns[i].id; - if (cloneEl.querySelector("[fill='url(#"+id+")']")) continue; + if (cloneEl.querySelector("[fill='url(#" + id + ")']")) continue; patterns[i].remove(); } // remove unused symbols const symbols = cloneEl.querySelectorAll("symbol"); - for (let i=0; i < symbols.length; i++) { + for (let i = 0; i < symbols.length; i++) { const id = symbols[i].id; - if (cloneEl.querySelector("use[*|href='#"+id+"']")) continue; + if (cloneEl.querySelector("use[*|href='#" + id + "']")) continue; symbols[i].remove(); } // add displayed emblems if (layerIsOn("toggleEmblems") && emblems.selectAll("use").size()) { - cloneEl.getElementById("emblems")?.querySelectorAll("use").forEach(el => { - const href = el.getAttribute("href") || el.getAttribute("xlink:href"); - if (!href) return; - const emblem = document.getElementById(href.slice(1)); - if (emblem) cloneDefs.append(emblem.cloneNode(true)); - }); + cloneEl + .getElementById("emblems") + ?.querySelectorAll("use") + .forEach(el => { + const href = el.getAttribute("href") || el.getAttribute("xlink:href"); + if (!href) return; + const emblem = document.getElementById(href.slice(1)); + if (emblem) cloneDefs.append(emblem.cloneNode(true)); + }); } else { cloneDefs.querySelector("#defs-emblems")?.remove(); } @@ -150,7 +153,7 @@ async function getMapURL(type, subtype) { if (cloneEl.getElementById("terrain")) { const uniqueElements = new Set(); const terrainNodes = cloneEl.getElementById("terrain").childNodes; - for (let i=0; i < terrainNodes.length; i++) { + for (let i = 0; i < terrainNodes.length; i++) { const href = terrainNodes[i].getAttribute("href") || terrainNodes[i].getAttribute("xlink:href"); uniqueElements.add(href); } @@ -177,7 +180,7 @@ async function getMapURL(type, subtype) { // add grid pattern if (cloneEl.getElementById("gridOverlay")?.hasChildNodes()) { const type = cloneEl.getElementById("gridOverlay").getAttribute("type"); - const pattern = svgDefs.getElementById("pattern_"+type); + const pattern = svgDefs.getElementById("pattern_" + type); if (pattern) cloneDefs.appendChild(pattern.cloneNode(true)); } @@ -190,11 +193,11 @@ async function getMapURL(type, subtype) { if (cloneEl.getElementById("armies")) cloneEl.insertAdjacentHTML("afterbegin", ""); const fontStyle = await GFontToDataURI(getFontsToLoad(clone)); // load non-standard fonts - if (fontStyle) clone.select("defs").append("style").text(fontStyle.join('\n')); // add font to style + if (fontStyle) clone.select("defs").append("style").text(fontStyle.join("\n")); // add font to style clone.remove(); - const serialized = `` + (new XMLSerializer()).serializeToString(cloneEl); - const blob = new Blob([serialized], {type: 'image/svg+xml;charset=utf-8'}); + const serialized = `` + new XMLSerializer().serializeToString(cloneEl); + const blob = new Blob([serialized], {type: "image/svg+xml;charset=utf-8"}); const url = window.URL.createObjectURL(blob); window.setTimeout(() => window.URL.revokeObjectURL(url), 5000); return url; @@ -205,10 +208,13 @@ function removeUnusedElements(clone) { if (!terrain.selectAll("use").size()) clone.select("#defs-relief").remove(); if (markers.style("display") === "none") clone.select("#defs-markers").remove(); - for (let empty = 1; empty;) { + for (let empty = 1; empty; ) { empty = 0; - clone.selectAll("g").each(function() { - if (!this.hasChildNodes() || this.style.display === "none" || this.classList.contains("hidden")) {empty++; this.remove();} + clone.selectAll("g").each(function () { + if (!this.hasChildNodes() || this.style.display === "none" || this.classList.contains("hidden")) { + empty++; + this.remove(); + } if (this.hasAttribute("display") && this.style.display === "inline") this.removeAttribute("display"); }); } @@ -218,8 +224,14 @@ function updateMeshCells(clone) { const data = renderOcean.checked ? grid.cells.i : grid.cells.i.filter(i => grid.cells.h[i] >= 20); const scheme = getColorScheme(); clone.select("#heights").attr("filter", "url(#blur1)"); - clone.select("#heights").selectAll("polygon").data(data).join("polygon").attr("points", d => getGridPolygon(d)) - .attr("id", d => "cell"+d).attr("stroke", d => getColor(grid.cells.h[d], scheme)); + clone + .select("#heights") + .selectAll("polygon") + .data(data) + .join("polygon") + .attr("points", d => getGridPolygon(d)) + .attr("id", d => "cell" + d) + .attr("stroke", d => getColor(grid.cells.h[d], scheme)); } // for each g element get inline style @@ -227,11 +239,11 @@ function inlineStyle(clone) { const emptyG = clone.append("g").node(); const defaultStyles = window.getComputedStyle(emptyG); - clone.selectAll("g, #ruler *, #scaleBar > text").each(function() { + clone.selectAll("g, #ruler *, #scaleBar > text").each(function () { const compStyle = window.getComputedStyle(this); let style = ""; - for (let i=0; i < compStyle.length; i++) { + for (let i = 0; i < compStyle.length; i++) { const key = compStyle[i]; const value = compStyle.getPropertyValue(key); @@ -244,7 +256,7 @@ function inlineStyle(clone) { if (key === "cursor") continue; // cursor should be default if (this.hasAttribute(key)) continue; // don't add style if there is the same attribute if (value === defaultStyles.getPropertyValue(key)) continue; - style += key + ':' + value + ';'; + style += key + ":" + value + ";"; } for (const key in compStyle) { @@ -253,10 +265,10 @@ function inlineStyle(clone) { if (key === "cursor") continue; // cursor should be default if (this.hasAttribute(key)) continue; // don't add style if there is the same attribute if (value === defaultStyles.getPropertyValue(key)) continue; - style += key + ':' + value + ';'; + style += key + ":" + value + ";"; } - if (style != "") this.setAttribute('style', style); + if (style != "") this.setAttribute("style", style); }); emptyG.remove(); @@ -267,7 +279,7 @@ function getFontsToLoad(clone) { const webSafe = ["Georgia", "Times+New+Roman", "Comic+Sans+MS", "Lucida+Sans+Unicode", "Courier+New", "Verdana", "Arial", "Impact"]; // fonts to not fetch const fontsInUse = new Set(); // to store fonts currently in use - clone.selectAll("#labels > g").each(function() { + clone.selectAll("#labels > g").each(function () { if (!this.hasChildNodes()) return; const font = this.dataset.font; if (!font || webSafe.includes(font)) return; @@ -285,16 +297,16 @@ function GFontToDataURI(url) { return fetch(url) // first fecth the embed stylesheet page .then(resp => resp.text()) // we only need the text of it .then(text => { - let s = document.createElement('style'); + let s = document.createElement("style"); s.innerHTML = text; document.head.appendChild(s); const styleSheet = Array.prototype.filter.call(document.styleSheets, sS => sS.ownerNode === s)[0]; const FontRule = rule => { - const src = rule.style.getPropertyValue('src'); - const url = src ? src.split('url(')[1].split(')')[0] : ""; + const src = rule.style.getPropertyValue("src"); + const url = src ? src.split("url(")[1].split(")")[0] : ""; return {rule, src, url: url.substring(url.length - 1, 1)}; - } + }; const fontProms = []; for (const r of styleSheet.cssRules) { @@ -303,16 +315,16 @@ function GFontToDataURI(url) { fontProms.push( fetch(fR.url) // fetch the actual font-file (.woff) - .then(resp => resp.blob()) - .then(blob => { - return new Promise(resolve => { - let f = new FileReader(); - f.onload = e => resolve(f.result); - f.readAsDataURL(blob); + .then(resp => resp.blob()) + .then(blob => { + return new Promise(resolve => { + let f = new FileReader(); + f.onload = e => resolve(f.result); + f.readAsDataURL(blob); + }); }) - }) - .then(dataURL => fR.rule.cssText.replace(fR.url, dataURL)) - ) + .then(dataURL => fR.rule.cssText.replace(fR.url, dataURL)) + ); } document.head.removeChild(s); // clean up return Promise.all(fontProms); // wait for all this has been done @@ -328,13 +340,7 @@ function getMapData() { 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, graphWidth, graphHeight, mapId].join("|"); - const settings = [distanceUnitInput.value, distanceScaleInput.value, areaUnit.value, - heightUnit.value, heightExponentInput.value, temperatureScale.value, - barSize.value, barLabel.value, barBackOpacity.value, barBackColor.value, - barPosX.value, barPosY.value, populationRate.value, urbanization.value, - mapSizeOutput.value, latitudeOutput.value, temperatureEquatorOutput.value, - temperaturePoleOutput.value, precOutput.value, JSON.stringify(options), - mapName.value].join("|"); + const settings = [distanceUnitInput.value, distanceScaleInput.value, areaUnit.value, heightUnit.value, heightExponentInput.value, temperatureScale.value, barSize.value, barLabel.value, barBackOpacity.value, barBackColor.value, barPosX.value, barPosY.value, populationRate.value, urbanization.value, mapSizeOutput.value, latitudeOutput.value, temperatureEquatorOutput.value, temperaturePoleOutput.value, precOutput.value, JSON.stringify(options), mapName.value].join("|"); const coords = JSON.stringify(mapCoordinates); const biomes = [biomesData.color, biomesData.habitability, biomesData.name].join("|"); const notesData = JSON.stringify(notes); @@ -351,9 +357,9 @@ function getMapData() { // always remove rulers cloneEl.querySelector("#ruler").innerHTML = ""; - const svg_xml = (new XMLSerializer()).serializeToString(cloneEl); + const svg_xml = new XMLSerializer().serializeToString(cloneEl); - const gridGeneral = JSON.stringify({spacing:grid.spacing, cellsX:grid.cellsX, cellsY:grid.cellsY, boundary:grid.boundary, points:grid.points, features:grid.features}); + const gridGeneral = JSON.stringify({spacing: grid.spacing, cellsX: grid.cellsX, cellsY: grid.cellsY, boundary: grid.boundary, points: grid.points, features: grid.features}); const features = JSON.stringify(pack.features); const cultures = JSON.stringify(pack.cultures); const states = JSON.stringify(pack.states); @@ -364,22 +370,18 @@ function getMapData() { // store name array only if it is not the same as default const defaultNB = Names.getNameBases(); - const namesData = nameBases.map((b,i) => { - const names = defaultNB[i] && defaultNB[i].b === b.b ? "" : b.b; - return `${b.name}|${b.min}|${b.max}|${b.d}|${b.m}|${names}`; - }).join("/"); + const namesData = nameBases + .map((b, i) => { + const names = defaultNB[i] && defaultNB[i].b === b.b ? "" : b.b; + return `${b.name}|${b.min}|${b.max}|${b.d}|${b.m}|${names}`; + }) + .join("/"); // round population to save resources const pop = Array.from(pack.cells.pop).map(p => rn(p, 4)); // data format as below - const data = [params, settings, coords, biomes, notesData, svg_xml, - gridGeneral, grid.cells.h, grid.cells.prec, grid.cells.f, grid.cells.t, grid.cells.temp, - features, cultures, states, burgs, - pack.cells.biome, pack.cells.burg, pack.cells.conf, pack.cells.culture, pack.cells.fl, - pop, pack.cells.r, pack.cells.road, pack.cells.s, pack.cells.state, - pack.cells.religion, pack.cells.province, pack.cells.crossroad, religions, provinces, - namesData, rivers, rulersString].join("\r\n"); + const data = [params, settings, coords, biomes, notesData, svg_xml, gridGeneral, grid.cells.h, grid.cells.prec, grid.cells.f, grid.cells.t, grid.cells.temp, features, cultures, states, burgs, pack.cells.biome, pack.cells.burg, pack.cells.conf, pack.cells.culture, pack.cells.fl, pop, pack.cells.r, pack.cells.road, pack.cells.s, pack.cells.state, pack.cells.religion, pack.cells.province, pack.cells.crossroad, religions, provinces, namesData, rivers, rulersString].join("\r\n"); const blob = new Blob([data], {type: "text/plain"}); TIME && console.timeEnd("createMapDataBlob"); @@ -389,7 +391,10 @@ function getMapData() { // Download .map file async function saveMap() { - if (customization) {tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error"); return;} + if (customization) { + tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error"); + return; + } closeDialogs("#alert"); const blob = await getMapData(); @@ -405,8 +410,11 @@ async function saveMap() { function saveGeoJSON_Cells() { const json = {type: "FeatureCollection", features: []}; const cells = pack.cells; - const getPopulation = i => {const [r, u] = getCellPopulation(i); return rn(r+u)}; - const getHeight = i => parseInt(getFriendlyHeight([cells.p[i][0],cells.p[i][1]])); + const getPopulation = i => { + const [r, u] = getCellPopulation(i); + return rn(r + u); + }; + const getHeight = i => parseInt(getFriendlyHeight([cells.p[i][0], cells.p[i][1]])); cells.i.forEach(i => { const coordinates = getCellCoordinates(cells.v[i]); @@ -420,7 +428,7 @@ function saveGeoJSON_Cells() { const religion = cells.religion[i]; const neighbors = cells.c[i]; - const properties = {id:i, height, biome, type, population, state, province, culture, religion, neighbors} + const properties = {id: i, height, biome, type, population, state, province, culture, religion, neighbors}; const feature = {type: "Feature", geometry: {type: "Polygon", coordinates}, properties}; json.features.push(feature); }); @@ -432,7 +440,7 @@ function saveGeoJSON_Cells() { function saveGeoJSON_Routes() { const json = {type: "FeatureCollection", features: []}; - routes.selectAll("g > path").each(function() { + routes.selectAll("g > path").each(function () { const coordinates = getRoutePoints(this); const id = this.id; const type = this.parentElement.id; @@ -448,7 +456,7 @@ function saveGeoJSON_Routes() { function saveGeoJSON_Rivers() { const json = {type: "FeatureCollection", features: []}; - rivers.selectAll("path").each(function() { + rivers.selectAll("path").each(function () { const coordinates = getRiverPoints(this); const id = this.id; const width = +this.dataset.increment; @@ -470,10 +478,10 @@ function saveGeoJSON_Rivers() { function saveGeoJSON_Markers() { const json = {type: "FeatureCollection", features: []}; - markers.selectAll("use").each(function() { + markers.selectAll("use").each(function () { const coordinates = getQGIScoordinates(this.dataset.x, this.dataset.y); const id = this.id; - const type = (this.dataset.id).substring(1); + const type = this.dataset.id.substring(1); const icon = document.getElementById(type).textContent; const note = notes.length ? notes.find(note => note.id === this.id) : null; const name = note ? note.name : ""; @@ -497,7 +505,7 @@ function getRoutePoints(node) { let points = []; const l = node.getTotalLength(); const increment = l / Math.ceil(l / 2); - for (let i=0; i <= l; i += increment) { + for (let i = 0; i <= l; i += increment) { const p = node.getPointAtLength(i); points.push(getQGIScoordinates(p.x, p.y)); } @@ -508,17 +516,20 @@ function getRiverPoints(node) { let points = []; const l = node.getTotalLength() / 2; // half-length const increment = 0.25; // defines density of points - for (let i=l, c=i; i >= 0; i -= increment, c += increment) { + for (let i = l, c = i; i >= 0; i -= increment, c += increment) { const p1 = node.getPointAtLength(i); const p2 = node.getPointAtLength(c); const [x, y] = getQGIScoordinates((p1.x + p2.x) / 2, (p1.y + p2.y) / 2); - points.push([x,y]); + points.push([x, y]); } return points; } async function quickSave() { - if (customization) {tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error"); return;} + if (customization) { + tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error"); + return; + } const blob = await getMapData(); if (blob) ldb.set("lastMap", blob); // auto-save map tip("Map is saved to browser memory. Please also save as .map file to secure progress", true, "success", 2000); @@ -537,14 +548,24 @@ function quickLoad() { function loadMapPrompt(blob) { const workingTime = (Date.now() - last(mapHistory).created) / 60000; // minutes - if (workingTime < 5) {loadLastSavedMap(); return;} + if (workingTime < 5) { + loadLastSavedMap(); + return; + } alertMessage.innerHTML = `Are you sure you want to load saved map?
All unsaved changes made to the current map will be lost`; - $("#alert").dialog({resizable: false, title: "Load saved map", + $("#alert").dialog({ + resizable: false, + title: "Load saved map", buttons: { - Cancel: function() {$(this).dialog("close");}, - Load: function() {loadLastSavedMap(); $(this).dialog("close");} + Cancel: function () { + $(this).dialog("close"); + }, + Load: function () { + loadLastSavedMap(); + $(this).dialog("close"); + } } }); @@ -552,31 +573,23 @@ function loadMapPrompt(blob) { WARN && console.warn("Load last saved map"); try { uploadMap(blob); - } - catch(error) { + } catch (error) { ERROR && console.error(error); tip("Cannot load last saved map", true, "error", 2000); } } } -const saveReminder = function() { +const saveReminder = function () { if (localStorage.getItem("noReminder")) return; - const message = ["Please don't forget to save your work as a .map file", - "Please remember to save work as a .map file", - "Saving in .map format will ensure your data won't be lost in case of issues", - "Safety is number one priority. Please save the map", - "Don't forget to save your map on a regular basis!", - "Just a gentle reminder for you to save the map", - "Please don't forget to save your progress (saving as .map is the best option)", - "Don't want to be reminded about need to save? Press CTRL+Q"]; + const message = ["Please don't forget to save your work as a .map file", "Please remember to save work as a .map file", "Saving in .map format will ensure your data won't be lost in case of issues", "Safety is number one priority. Please save the map", "Don't forget to save your map on a regular basis!", "Just a gentle reminder for you to save the map", "Please don't forget to save your progress (saving as .map is the best option)", "Don't want to be reminded about need to save? Press CTRL+Q"]; saveReminder.reminder = setInterval(() => { if (customization) return; tip(ra(message), true, "warn", 2500); }, 1e6); saveReminder.status = 1; -} +}; saveReminder(); @@ -597,7 +610,7 @@ function uploadMap(file, callback) { uploadMap.timeStart = performance.now(); const fileReader = new FileReader(); - fileReader.onload = function(fileLoadedEvent) { + fileReader.onload = function (fileLoadedEvent) { if (callback) callback(); document.getElementById("coas").innerHTML = ""; // remove auto-generated emblems @@ -605,11 +618,15 @@ function uploadMap(file, callback) { const data = dataLoaded.split("\r\n"); const mapVersion = data[0].split("|")[0] || data[0]; - if (mapVersion === version) {parseLoadedData(data); return;} + if (mapVersion === version) { + parseLoadedData(data); + return; + } const archive = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "archived version"); const parsed = parseFloat(mapVersion); - let message = "", load = false; + let message = "", + load = false; if (isNaN(parsed) || data.length < 26 || !data[5]) { message = `The file you are trying to load is outdated or not a valid .map file.
Please try to open it using an ${archive}`; @@ -618,13 +635,20 @@ function uploadMap(file, callback) {
Please keep using an ${archive}`; } else { load = true; - message = `The map version (${mapVersion}) does not match the Generator version (${version}). + message = `The map version (${mapVersion}) does not match the Generator version (${version}).
Click OK to get map auto-updated. In case of issues please keep using an ${archive} of the Generator`; } alertMessage.innerHTML = message; - $("#alert").dialog({title: "Version conflict", width: "38em", buttons: { - OK: function() {$(this).dialog("close"); if (load) parseLoadedData(data);} - }}); + $("#alert").dialog({ + title: "Version conflict", + width: "38em", + buttons: { + OK: function () { + $(this).dialog("close"); + if (load) parseLoadedData(data); + } + } + }); }; fileReader.readAsText(file, "UTF-8"); @@ -640,17 +664,20 @@ function parseLoadedData(data) { const reliefIcons = document.getElementById("defs-relief").innerHTML; // save relief icons const hatching = document.getElementById("hatching").cloneNode(true); // save hatching - void function parseParameters() { + void (function parseParameters() { const params = data[0].split("|"); - if (params[3]) {seed = params[3]; optionsSeed.value = seed;} + if (params[3]) { + seed = params[3]; + optionsSeed.value = seed; + } if (params[4]) graphWidth = +params[4]; if (params[5]) graphHeight = +params[5]; mapId = params[6] ? +params[6] : Date.now(); - }() + })(); INFO && console.group("Loaded Map " + seed); - void function parseSettings() { + void (function parseSettings() { const settings = data[1].split("|"); if (settings[0]) applyOption(distanceUnitInput, settings[0]); if (settings[1]) distanceScaleInput.value = distanceScaleOutput.value = settings[1]; @@ -673,9 +700,9 @@ function parseLoadedData(data) { if (settings[18]) precInput.value = precOutput.value = settings[18]; if (settings[19]) options = JSON.parse(settings[19]); if (settings[20]) mapName.value = settings[20]; - }() + })(); - void function parseConfiguration() { + void (function parseConfiguration() { if (data[2]) mapCoordinates = JSON.parse(data[2]); if (data[4]) notes = JSON.parse(data[4]); if (data[33]) rulers.fromString(data[33]); @@ -687,20 +714,20 @@ function parseLoadedData(data) { biomesData.name = biomes[2].split(","); // push custom biomes if any - for (let i=biomesData.i.length; i < biomesData.name.length; i++) { + for (let i = biomesData.i.length; i < biomesData.name.length; i++) { biomesData.i.push(biomesData.i.length); biomesData.iconsDensity.push(0); biomesData.icons.push([]); biomesData.cost.push(50); } - }() + })(); - void function replaceSVG() { + void (function replaceSVG() { svg.remove(); document.body.insertAdjacentHTML("afterbegin", data[5]); - }() + })(); - void function redefineElements() { + void (function redefineElements() { svg = d3.select("#map"); defs = svg.select("#deftemp"); viewbox = svg.select("#viewbox"); @@ -750,9 +777,9 @@ function parseLoadedData(data) { fogging = viewbox.select("#fogging"); debug = viewbox.select("#debug"); burgLabels = labels.select("#burgLabels"); - }() + })(); - void function parseGridData() { + void (function parseGridData() { grid = JSON.parse(data[6]); calculateVoronoi(grid, grid.points); grid.cells.h = Uint8Array.from(data[7].split(",")); @@ -760,9 +787,9 @@ function parseLoadedData(data) { grid.cells.f = Uint16Array.from(data[9].split(",")); grid.cells.t = Int8Array.from(data[10].split(",")); grid.cells.temp = Int8Array.from(data[11].split(",")); - }() + })(); - void function parsePackData() { + void (function parsePackData() { pack = {}; reGraph(); reMarkFeatures(); @@ -795,19 +822,22 @@ function parseLoadedData(data) { const e = d.split("|"); if (!e.length) return; const b = e[5].split(",").length > 2 || !nameBases[i] ? e[5] : nameBases[i].b; - nameBases[i] = {name:e[0], min:e[1], max:e[2], d:e[3], m:e[4], b}; + nameBases[i] = {name: e[0], min: e[1], max: e[2], d: e[3], m: e[4], b}; }); } - }() + })(); const notHidden = selection => selection.node() && selection.style("display") !== "none"; const hasChildren = selection => selection.node()?.hasChildNodes(); const hasChild = (selection, selector) => selection.node()?.querySelector(selector); const turnOn = el => document.getElementById(el).classList.remove("buttonoff"); - void function restoreLayersState() { + void (function restoreLayersState() { // turn all layers off - document.getElementById("mapLayers").querySelectorAll("li").forEach(el => el.classList.add("buttonoff")); + document + .getElementById("mapLayers") + .querySelectorAll("li") + .forEach(el => el.classList.add("buttonoff")); // turn on active layers if (notHidden(texture) && hasChild(texture, "image")) turnOn("toggleTexture"); @@ -839,14 +869,14 @@ function parseLoadedData(data) { if (notHidden(scaleBar)) turnOn("toggleScaleBar"); getCurrentPreset(); - }() + })(); - void function restoreEvents() { + void (function restoreEvents() { scaleBar.on("mousemove", () => tip("Click to open Units Editor")).on("click", () => editUnits()); legend.on("mousemove", () => tip("Drag to change the position. Click to hide the legend")).on("click", () => clearLegend()); - }() + })(); - void function resolveVersionConflicts() { + void (function resolveVersionConflicts() { const version = parseFloat(data[0].split("|")[0]); if (version < 0.9) { // 0.9 has additional relief icons to be included into older maps @@ -860,19 +890,17 @@ function parseLoadedData(data) { // 1.0 adds a legend box legend = svg.append("g").attr("id", "legend"); - legend.attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC") - .attr("font-size", 13).attr("data-size", 13).attr("data-x", 99).attr("data-y", 93) - .attr("stroke-width", 2.5).attr("stroke", "#812929").attr("stroke-dasharray", "0 4 10 4").attr("stroke-linecap", "round"); + legend.attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC").attr("font-size", 13).attr("data-size", 13).attr("data-x", 99).attr("data-y", 93).attr("stroke-width", 2.5).attr("stroke", "#812929").attr("stroke-dasharray", "0 4 10 4").attr("stroke-linecap", "round"); // 1.0 separated drawBorders fron drawStates() stateBorders = borders.append("g").attr("id", "stateBorders"); provinceBorders = borders.append("g").attr("id", "provinceBorders"); borders.attr("opacity", null).attr("stroke", null).attr("stroke-width", null).attr("stroke-dasharray", null).attr("stroke-linecap", null).attr("filter", null); - stateBorders.attr("opacity", .8).attr("stroke", "#56566d").attr("stroke-width", 1).attr("stroke-dasharray", "2").attr("stroke-linecap", "butt"); - provinceBorders.attr("opacity", .8).attr("stroke", "#56566d").attr("stroke-width", .5).attr("stroke-dasharray", "1").attr("stroke-linecap", "butt"); + stateBorders.attr("opacity", 0.8).attr("stroke", "#56566d").attr("stroke-width", 1).attr("stroke-dasharray", "2").attr("stroke-linecap", "butt"); + provinceBorders.attr("opacity", 0.8).attr("stroke", "#56566d").attr("stroke-width", 0.5).attr("stroke-dasharray", "1").attr("stroke-linecap", "butt"); // 1.0 adds state relations, provinces, forms and full names - provs = viewbox.insert("g", "#borders").attr("id", "provs").attr("opacity", .6); + provs = viewbox.insert("g", "#borders").attr("id", "provs").attr("opacity", 0.6); BurgsAndStates.collectStatistics(); BurgsAndStates.generateCampaigns(); BurgsAndStates.generateDiplomacy(); @@ -880,7 +908,7 @@ function parseLoadedData(data) { drawStates(); BurgsAndStates.generateProvinces(); drawBorders(); - if (!layerIsOn("toggleBorders")) $('#borders').fadeOut(); + if (!layerIsOn("toggleBorders")) $("#borders").fadeOut(); if (!layerIsOn("toggleStates")) regions.attr("display", "none").selectAll("path").remove(); // 1.0 adds hatching @@ -888,9 +916,12 @@ function parseLoadedData(data) { // 1.0 adds zones layer zones = viewbox.insert("g", "#borders").attr("id", "zones").attr("display", "none"); - zones.attr("opacity", .6).attr("stroke", null).attr("stroke-width", 0).attr("stroke-dasharray", null).attr("stroke-linecap", "butt"); + zones.attr("opacity", 0.6).attr("stroke", null).attr("stroke-width", 0).attr("stroke-dasharray", null).attr("stroke-linecap", "butt"); addZones(); - if (!markers.selectAll("*").size()) {addMarkers(); turnButtonOn("toggleMarkers");} + if (!markers.selectAll("*").size()) { + addMarkers(); + turnButtonOn("toggleMarkers"); + } // 1.0 add fogging layer (state focus) fogging = viewbox.insert("g", "#ruler").attr("id", "fogging-cont").attr("mask", "url(#fog)").append("g").attr("id", "fogging").style("display", "none"); @@ -904,7 +935,7 @@ function parseLoadedData(data) { } // 1.0 changed labels to multi-lined - labels.selectAll("textPath").each(function() { + labels.selectAll("textPath").each(function () { const text = this.textContent; const shift = this.getComputedTextLength() / -1.5; this.innerHTML = `${text}`; @@ -923,7 +954,7 @@ function parseLoadedData(data) { // v 1.0 initially has Sympathy status then relaced with Friendly for (const s of pack.states) { if (!s.diplomacy) continue; - s.diplomacy = s.diplomacy.map(r => r === "Sympathy" ? "Friendly" : r); + s.diplomacy = s.diplomacy.map(r => (r === "Sympathy" ? "Friendly" : r)); } // labels should be toggled via style attribute, so remove display attribute @@ -931,20 +962,22 @@ function parseLoadedData(data) { // v 1.0 added religions heirarchy tree if (pack.religions[1] && !pack.religions[1].code) { - pack.religions.filter(r => r.i).forEach(r => { - r.origin = 0; - r.code = r.name.slice(0, 2); - }); + pack.religions + .filter(r => r.i) + .forEach(r => { + r.origin = 0; + r.code = r.name.slice(0, 2); + }); } if (!document.getElementById("freshwater")) { lakes.append("g").attr("id", "freshwater"); - lakes.select("#freshwater").attr("opacity", .5).attr("fill", "#a6c1fd").attr("stroke", "#5f799d").attr("stroke-width", .7).attr("filter", null); + lakes.select("#freshwater").attr("opacity", 0.5).attr("fill", "#a6c1fd").attr("stroke", "#5f799d").attr("stroke-width", 0.7).attr("filter", null); } if (!document.getElementById("salt")) { lakes.append("g").attr("id", "salt"); - lakes.select("#salt").attr("opacity", .5).attr("fill", "#409b8a").attr("stroke", "#388985").attr("stroke-width", .7).attr("filter", null); + lakes.select("#salt").attr("opacity", 0.5).attr("fill", "#409b8a").attr("stroke", "#388985").attr("stroke-width", 0.7).attr("filter", null); } // v 1.1 added new lake and coast groups @@ -952,14 +985,14 @@ function parseLoadedData(data) { lakes.append("g").attr("id", "sinkhole"); lakes.append("g").attr("id", "frozen"); lakes.append("g").attr("id", "lava"); - lakes.select("#sinkhole").attr("opacity", 1).attr("fill", "#5bc9fd").attr("stroke", "#53a3b0").attr("stroke-width", .7).attr("filter", null); - lakes.select("#frozen").attr("opacity", .95).attr("fill", "#cdd4e7").attr("stroke", "#cfe0eb").attr("stroke-width", 0).attr("filter", null); - lakes.select("#lava").attr("opacity", .7).attr("fill", "#90270d").attr("stroke", "#f93e0c").attr("stroke-width", 2).attr("filter", "url(#crumpled)"); + lakes.select("#sinkhole").attr("opacity", 1).attr("fill", "#5bc9fd").attr("stroke", "#53a3b0").attr("stroke-width", 0.7).attr("filter", null); + lakes.select("#frozen").attr("opacity", 0.95).attr("fill", "#cdd4e7").attr("stroke", "#cfe0eb").attr("stroke-width", 0).attr("filter", null); + lakes.select("#lava").attr("opacity", 0.7).attr("fill", "#90270d").attr("stroke", "#f93e0c").attr("stroke-width", 2).attr("filter", "url(#crumpled)"); coastline.append("g").attr("id", "sea_island"); coastline.append("g").attr("id", "lake_island"); - coastline.select("#sea_island").attr("opacity", .5).attr("stroke", "#1f3846").attr("stroke-width", .7).attr("filter", "url(#dropShadow)"); - coastline.select("#lake_island").attr("opacity", 1).attr("stroke", "#7c8eaf").attr("stroke-width", .35).attr("filter", null); + coastline.select("#sea_island").attr("opacity", 0.5).attr("stroke", "#1f3846").attr("stroke-width", 0.7).attr("filter", "url(#dropShadow)"); + coastline.select("#lake_island").attr("opacity", 1).attr("stroke", "#7c8eaf").attr("stroke-width", 0.35).attr("filter", null); } // v 1.1 features stores more data @@ -979,10 +1012,12 @@ function parseLoadedData(data) { // v 1.11 added cultures heirarchy tree if (pack.cultures[1] && !pack.cultures[1].code) { - pack.cultures.filter(c => c.i).forEach(c => { - c.origin = 0; - c.code = c.name.slice(0, 2); - }); + pack.cultures + .filter(c => c.i) + .forEach(c => { + c.origin = 0; + c.code = c.name.slice(0, 2); + }); } // v 1.11 had an issue with fogging being displayed on load @@ -991,12 +1026,12 @@ function parseLoadedData(data) { // v 1.2 added new terrain attributes if (!terrain.attr("set")) terrain.attr("set", "simple"); if (!terrain.attr("size")) terrain.attr("size", 1); - if (!terrain.attr("density")) terrain.attr("density", .4); + if (!terrain.attr("density")) terrain.attr("density", 0.4); } if (version < 1.21) { // v 1.11 replaced "display" attribute by "display" style - viewbox.selectAll("g").each(function() { + viewbox.selectAll("g").each(function () { if (this.hasAttribute("display")) { this.removeAttribute("display"); this.style.display = "none"; @@ -1005,16 +1040,17 @@ function parseLoadedData(data) { // v 1.21 added rivers data to pack pack.rivers = []; // rivers data - rivers.selectAll("path").each(function() { + rivers.selectAll("path").each(function () { const i = +this.id.slice(5); const length = this.getTotalLength() / 2; - const s = this.getPointAtLength(length), e = this.getPointAtLength(0); - const source = findCell(s.x, s.y), mouth = findCell(e.x, e.y); + const s = this.getPointAtLength(length), + e = this.getPointAtLength(0); + const source = findCell(s.x, s.y), + mouth = findCell(e.x, e.y); const name = Rivers.getName(mouth); - const type = length < 25 ? rw({"Creek":9, "River":3, "Brook":3, "Stream":1}) : "River"; - pack.rivers.push({i, parent:0, length, source, mouth, basin:i, name, type}); + const type = length < 25 ? rw({Creek: 9, River: 3, Brook: 3, Stream: 1}) : "River"; + pack.rivers.push({i, parent: 0, length, source, mouth, basin: i, name, type}); }); - } if (version < 1.22) { @@ -1026,7 +1062,7 @@ function parseLoadedData(data) { // v 1.3 added global options object const winds = options.slice(); // previostly wind was saved in settings[19] const year = rand(100, 2000); - const era = Names.getBaseShort(P(.7) ? 1 : rand(nameBases.length)) + " Era"; + const era = Names.getBaseShort(P(0.7) ? 1 : rand(nameBases.length)) + " Era"; const eraShort = era[0] + "E"; const military = Military.getDefaultOptions(); options = {winds, year, era, eraShort, military}; @@ -1036,7 +1072,7 @@ function parseLoadedData(data) { // v 1.3 added militry layer armies = viewbox.insert("g", "#icons").attr("id", "armies"); - armies.attr("opacity", 1).attr("fill-opacity", 1).attr("font-size", 6).attr("box-size", 3).attr("stroke", "#000").attr("stroke-width", .3); + armies.attr("opacity", 1).attr("fill-opacity", 1).attr("font-size", 6).attr("box-size", 3).attr("stroke", "#000").attr("stroke-width", 0.3); turnButtonOn("toggleMilitary"); Military.generate(); } @@ -1045,7 +1081,7 @@ function parseLoadedData(data) { // v 1.35 added dry lakes if (!lakes.select("#dry").size()) { lakes.append("g").attr("id", "dry"); - lakes.select("#dry").attr("opacity", 1).attr("fill", "#c9bfa7").attr("stroke", "#8e816f").attr("stroke-width", .7).attr("filter", null); + lakes.select("#dry").attr("opacity", 1).attr("fill", "#c9bfa7").attr("stroke", "#8e816f").attr("stroke-width", 0.7).attr("filter", null); } // v 1.4 added ice layer @@ -1071,7 +1107,7 @@ function parseLoadedData(data) { } // 1.4 added state reference for regiments - pack.states.filter(s => s.military).forEach(s => s.military.forEach(r => r.state = s.i)); + pack.states.filter(s => s.military).forEach(s => s.military.forEach(r => (r.state = s.i))); } if (version < 1.5) { @@ -1103,7 +1139,7 @@ function parseLoadedData(data) { toggleEmblems(); // v 1.5 changed releif icons data - terrain.selectAll("use").each(function() { + terrain.selectAll("use").each(function () { const type = this.getAttribute("data-type") || this.getAttribute("xlink:href"); this.removeAttribute("xlink:href"); this.removeAttribute("data-type"); @@ -1115,14 +1151,14 @@ function parseLoadedData(data) { if (version < 1.6) { // v 1.6 changed rivers data for (const river of pack.rivers) { - const el = document.getElementById("river"+river.i); + const el = document.getElementById("river" + river.i); if (el) { river.widthFactor = +el.getAttribute("data-width"); el.removeAttribute("data-width"); el.removeAttribute("data-increment"); river.discharge = pack.cells.fl[river.mouth] || 1; river.width = rn(river.length / 100, 2); - river.sourceWidth = .1; + river.sourceWidth = 0.1; } else { Rivers.remove(river.i); } @@ -1137,7 +1173,7 @@ function parseLoadedData(data) { f.temp = grid.cells.temp[pack.cells.g[f.firstCell]]; f.height = f.height || d3.min(pack.cells.c[f.firstCell].map(c => pack.cells.h[c]).filter(h => h >= 20)); const height = (f.height - 18) ** heightExponentInput.value; - const evaporation = (700 * (f.temp + .006 * height) / 50 + 75) / (80 - f.temp); + const evaporation = ((700 * (f.temp + 0.006 * height)) / 50 + 75) / (80 - f.temp); f.evaporation = rn(evaporation * f.cells); f.name = f.name || Lakes.getName(f); delete f.river; @@ -1149,31 +1185,34 @@ function parseLoadedData(data) { ruler.style("display", null); rulers = new Rulers(); - ruler.selectAll(".ruler > .white").each(function() { + ruler.selectAll(".ruler > .white").each(function () { const x1 = +this.getAttribute("x1"); const y1 = +this.getAttribute("y1"); const x2 = +this.getAttribute("x2"); const y2 = +this.getAttribute("y2"); if (isNaN(x1) || isNaN(y1) || isNaN(x2) || isNaN(y2)) return; - const points = [[x1, y1], [x2, y2]]; + const points = [ + [x1, y1], + [x2, y2] + ]; rulers.create(Ruler, points); }); - ruler.selectAll("g.opisometer").each(function() { + ruler.selectAll("g.opisometer").each(function () { const pointsString = this.dataset.points; if (!pointsString) return; const points = JSON.parse(pointsString); rulers.create(Opisometer, points); }); - ruler.selectAll("path.planimeter").each(function() { + ruler.selectAll("path.planimeter").each(function () { const length = this.getTotalLength(); if (length < 30) return; const step = length > 1000 ? 40 : length > 400 ? 20 : 10; const increment = length / Math.ceil(length / step); const points = []; - for (let i=0; i <= length; i += increment) { + for (let i = 0; i <= length; i += increment) { const point = this.getPointAtLength(i); points.push([point.x | 0, point.y | 0]); } @@ -1193,16 +1232,16 @@ function parseLoadedData(data) { const filter = pattern.firstElementChild.getAttribute("filter"); const href = filter ? "./images/" + filter.replace("url(#", "").replace(")", "") + ".png" : ""; pattern.innerHTML = ``; - document.getElementById("oceanPattern").setAttribute("opacity", .2); + document.getElementById("oceanPattern").setAttribute("opacity", 0.2); } - }() + })(); if (version < 1.62) { // v 1.62 changed grid data gridOverlay.attr("size", null); } - void function checkDataIntegrity() { + void (function checkDataIntegrity() { const cells = pack.cells; if (pack.cells.i.length !== pack.cells.state.length) { @@ -1212,28 +1251,28 @@ function parseLoadedData(data) { const invalidStates = [...new Set(cells.state)].filter(s => !pack.states[s] || pack.states[s].removed); invalidStates.forEach(s => { const invalidCells = cells.i.filter(i => cells.state[i] === s); - invalidCells.forEach(i => cells.state[i] = 0); + invalidCells.forEach(i => (cells.state[i] = 0)); ERROR && console.error("Data Integrity Check. Invalid state", s, "is assigned to cells", invalidCells); }); const invalidProvinces = [...new Set(cells.province)].filter(p => p && (!pack.provinces[p] || pack.provinces[p].removed)); invalidProvinces.forEach(p => { const invalidCells = cells.i.filter(i => cells.province[i] === p); - invalidCells.forEach(i => cells.province[i] = 0); + invalidCells.forEach(i => (cells.province[i] = 0)); ERROR && console.error("Data Integrity Check. Invalid province", p, "is assigned to cells", invalidCells); }); const invalidCultures = [...new Set(cells.culture)].filter(c => !pack.cultures[c] || pack.cultures[c].removed); invalidCultures.forEach(c => { const invalidCells = cells.i.filter(i => cells.culture[i] === c); - invalidCells.forEach(i => cells.province[i] = 0); + invalidCells.forEach(i => (cells.province[i] = 0)); ERROR && console.error("Data Integrity Check. Invalid culture", c, "is assigned to cells", invalidCells); }); const invalidReligions = [...new Set(cells.religion)].filter(r => !pack.religions[r] || pack.religions[r].removed); invalidReligions.forEach(r => { const invalidCells = cells.i.filter(i => cells.religion[i] === r); - invalidCells.forEach(i => cells.religion[i] = 0); + invalidCells.forEach(i => (cells.religion[i] = 0)); ERROR && console.error("Data Integrity Check. Invalid religion", c, "is assigned to cells", invalidCells); }); @@ -1247,26 +1286,29 @@ function parseLoadedData(data) { const invalidBurgs = [...new Set(cells.burg)].filter(b => b && (!pack.burgs[b] || pack.burgs[b].removed)); invalidBurgs.forEach(b => { const invalidCells = cells.i.filter(i => cells.burg[i] === b); - invalidCells.forEach(i => cells.burg[i] = 0); + invalidCells.forEach(i => (cells.burg[i] = 0)); ERROR && console.error("Data Integrity Check. Invalid burg", b, "is assigned to cells", invalidCells); }); const invalidRivers = [...new Set(cells.r)].filter(r => r && !pack.rivers.find(river => river.i === r)); invalidRivers.forEach(r => { const invalidCells = cells.i.filter(i => cells.r[i] === r); - invalidCells.forEach(i => cells.r[i] = 0); - rivers.select("river"+r).remove(); + invalidCells.forEach(i => (cells.r[i] = 0)); + rivers.select("river" + r).remove(); ERROR && console.error("Data Integrity Check. Invalid river", r, "is assigned to cells", invalidCells); }); pack.burgs.forEach(b => { if (!b.i || b.removed) return; - if (b.port < 0) {ERROR && console.error("Data Integrity Check. Burg", b.i, "has invalid port value", b.port); b.port = 0;} + if (b.port < 0) { + ERROR && console.error("Data Integrity Check. Burg", b.i, "has invalid port value", b.port); + b.port = 0; + } if (b.cell >= cells.i.length) { ERROR && console.error("Data Integrity Check. Burg", b.i, "is linked to invalid cell", b.cell); b.cell = findCell(b.x, b.y); - cells.i.filter(i => cells.burg[i] === b.i).forEach(i => cells.burg[i] = 0); + cells.i.filter(i => cells.burg[i] === b.i).forEach(i => (cells.burg[i] = 0)); cells.burg[b.cell] = b.i; } @@ -1282,7 +1324,7 @@ function parseLoadedData(data) { ERROR && console.error("Data Integrity Check. Province", p.i, "is linked to removed state", p.state); p.removed = true; // remove incorrect province }); - }() + })(); changeMapSize(); @@ -1301,12 +1343,11 @@ function parseLoadedData(data) { focusOn(); // based on searchParams focus on point, cell or burg invokeActiveZooming(); - WARN && console.warn(`TOTAL: ${rn((performance.now()-uploadMap.timeStart)/1000,2)}s`); + WARN && console.warn(`TOTAL: ${rn((performance.now() - uploadMap.timeStart) / 1000, 2)}s`); showStatistics(); INFO && console.groupEnd("Loaded Map " + seed); tip("Map is successfully loaded", true, "success", 7000); - } - catch(error) { + } catch (error) { ERROR && console.error(error); clearMainTip(); @@ -1314,12 +1355,23 @@ function parseLoadedData(data) {
generate a new random map or cancel the loading

${parseError(error)}

`; $("#alert").dialog({ - resizable: false, title: "Loading error", maxWidth:"50em", buttons: { - "Select file": function() {$(this).dialog("close"); mapToLoad.click();}, - "New map": function() {$(this).dialog("close"); regenerateMap();}, - Cancel: function() {$(this).dialog("close")} - }, position: {my: "center", at: "center", of: "svg"} + resizable: false, + title: "Loading error", + maxWidth: "50em", + buttons: { + "Select file": function () { + $(this).dialog("close"); + mapToLoad.click(); + }, + "New map": function () { + $(this).dialog("close"); + regenerateMap(); + }, + Cancel: function () { + $(this).dialog("close"); + } + }, + position: {my: "center", at: "center", of: "svg"} }); } - } diff --git a/modules/ui/heightmap-editor.js b/modules/ui/heightmap-editor.js index f62d2ab0..0b7ef617 100644 --- a/modules/ui/heightmap-editor.js +++ b/modules/ui/heightmap-editor.js @@ -2,7 +2,7 @@ "use strict"; function editHeightmap() { - void function selectEditMode() { + void (function selectEditMode() { alertMessage.innerHTML = `Heightmap is a core element on which all other data (rivers, burgs, states etc) is based. So the best edit approach is to erase the secondary data and let the system automatically regenerate it on edit completion.

Erase mode also allows you Convert an Image into a heightmap or use Template Editor.

@@ -11,15 +11,26 @@ function editHeightmap() {

Please save the map before editing the heightmap!

Check out ${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Heightmap-customization", "wiki")} for guidance.

`; - $("#alert").dialog({resizable: false, title: "Edit Heightmap", width: "28em", + $("#alert").dialog({ + resizable: false, + title: "Edit Heightmap", + width: "28em", buttons: { - Erase: function() {enterHeightmapEditMode("erase");}, - Keep: function() {enterHeightmapEditMode("keep");}, - Risk: function() {enterHeightmapEditMode("risk");}, - Cancel: function() {$(this).dialog("close");} + Erase: function () { + enterHeightmapEditMode("erase"); + }, + Keep: function () { + enterHeightmapEditMode("keep"); + }, + Risk: function () { + enterHeightmapEditMode("risk"); + }, + Cancel: function () { + $(this).dialog("close"); + } } }); - }() + })(); restartHistory(); viewbox.insert("g", "#terrs").attr("id", "heights"); @@ -35,8 +46,8 @@ function editHeightmap() { document.getElementById("heightmap3DView").addEventListener("click", changeViewMode); document.getElementById("finalizeHeightmap").addEventListener("click", finalizeHeightmap); document.getElementById("renderOcean").addEventListener("click", mockHeightmap); - document.getElementById("templateUndo").addEventListener("click", () => restoreHistory(edits.n-1)); - document.getElementById("templateRedo").addEventListener("click", () => restoreHistory(edits.n+1)); + document.getElementById("templateUndo").addEventListener("click", () => restoreHistory(edits.n - 1)); + document.getElementById("templateRedo").addEventListener("click", () => restoreHistory(edits.n + 1)); function enterHeightmapEditMode(type) { editHeightmap.layers = Array.from(mapLayers.querySelectorAll("li:not(.buttonoff)")).map(node => node.id); // store layers preset @@ -77,9 +88,7 @@ function editHeightmap() { exitCustomization.style.bottom = svgHeight / 2 + "px"; exitCustomization.style.transform = "scale(2)"; exitCustomization.style.display = "block"; - d3.select("#exitCustomization") - .transition().duration(1000).style("opacity", 1) - .transition().duration(2000).ease(d3.easeSinInOut).style("right", "10px").style("bottom", "10px").style("transform", "scale(1)"); + d3.select("#exitCustomization").transition().duration(1000).style("opacity", 1).transition().duration(2000).ease(d3.easeSinInOut).style("right", "10px").style("bottom", "10px").style("transform", "scale(1)"); } else exitCustomization.style.display = "block"; openBrushesPanel(); @@ -91,7 +100,8 @@ function editHeightmap() { } function moveCursor() { - const p = d3.mouse(this), cell = findGridCell(p[0], p[1]); + const p = d3.mouse(this), + cell = findGridCell(p[0], p[1]); heightmapInfoX.innerHTML = rn(p[0]); heightmapInfoY.innerHTML = rn(p[1]); heightmapInfoCell.innerHTML = cell; @@ -108,12 +118,13 @@ function editHeightmap() { function getHeight(h) { const unit = heightUnit.value; let unitRatio = 3.281; // default calculations are in feet - if (unit === "m") unitRatio = 1; // if meter + if (unit === "m") unitRatio = 1; + // if meter else if (unit === "f") unitRatio = 0.5468; // if fathom let height = -990; if (h >= 20) height = Math.pow(h - 18, +heightExponentInput.value); - else if (h < 20 && h > 0) height = (h - 20) / h * 50; + else if (h < 20 && h > 0) height = ((h - 20) / h) * 50; return rn(height * unitRatio) + " " + unit; } @@ -156,10 +167,14 @@ function editHeightmap() { //viewbox.select("#heights").remove(); document.getElementById("heights").remove(); turnButtonOff("toggleHeight"); - document.getElementById("mapLayers").querySelectorAll("li").forEach(function(e) { - if (editHeightmap.layers.includes(e.id) && !layerIsOn(e.id)) e.click(); // turn on - else if (!editHeightmap.layers.includes(e.id) && layerIsOn(e.id)) e.click(); // turn off - }); + document + .getElementById("mapLayers") + .querySelectorAll("li") + .forEach(function (e) { + if (editHeightmap.layers.includes(e.id) && !layerIsOn(e.id)) e.click(); + // turn on + else if (!editHeightmap.layers.includes(e.id) && layerIsOn(e.id)) e.click(); // turn off + }); getCurrentPreset(); } @@ -169,7 +184,7 @@ function editHeightmap() { const change = changeHeights.checked; markFeatures(); - getSignedDistanceField(); + markupGridOcean(); if (change) openNearSeaLakes(); OceanLayers(); calculateTemperatures(); @@ -275,7 +290,7 @@ function editHeightmap() { } // recalculate zones to grid - zones.selectAll("g").each(function() { + zones.selectAll("g").each(function () { const zone = d3.select(this); const dataCells = zone.attr("data-cells"); const cells = dataCells ? dataCells.split(",").map(i => +i) : []; @@ -285,7 +300,7 @@ function editHeightmap() { }); markFeatures(); - getSignedDistanceField(); + markupGridOcean(); OceanLayers(); calculateTemperatures(); generatePrecipitation(); @@ -339,14 +354,12 @@ function editHeightmap() { } // find closest land cell to burg - const findBurgCell = function(x, y) { + const findBurgCell = function (x, y) { let i = findCell(x, y); if (pack.cells.h[i] >= 20) return i; - const dist = pack.cells.c[i].map(c => - pack.cells.h[c] < 20 ? Infinity : (pack.cells.p[c][0] - x) ** 2 + (pack.cells.p[c][1] - y) ** 2 - ); + const dist = pack.cells.c[i].map(c => (pack.cells.h[c] < 20 ? Infinity : (pack.cells.p[c][0] - x) ** 2 + (pack.cells.p[c][1] - y) ** 2)); return pack.cells.c[i][d3.scan(dist)]; - } + }; // find best cell for burgs for (const b of pack.burgs) { @@ -366,13 +379,16 @@ function editHeightmap() { const state = p.state; const stateProvs = pack.states[state].provinces; if (stateProvs.includes(p.i)) pack.states[state].provinces.splice(stateProvs.indexOf(p), 1); - + p.removed = true; continue; } if (p.burg && !pack.burgs[p.burg].removed) p.center = pack.burgs[p.burg].cell; - else {p.center = provCells[0]; p.burg = pack.cells.burg[p.center];} + else { + p.center = provCells[0]; + p.burg = pack.cells.burg[p.center]; + } } for (const c of pack.cultures) { @@ -390,7 +406,7 @@ function editHeightmap() { } // restore zones from grid - zones.selectAll("g").each(function() { + zones.selectAll("g").each(function () { const zone = d3.select(this); const g = zone.attr("data-cells"); const gCells = g ? g.split(",").map(i => +i) : []; @@ -399,8 +415,13 @@ function editHeightmap() { zone.attr("data-cells", cells); zone.selectAll("*").remove(); const base = zone.attr("id") + "_"; // id generic part - zone.selectAll("*").data(cells).enter().append("polygon") - .attr("points", d => getPackPolygon(d)).attr("id", d => base + d); + zone + .selectAll("*") + .data(cells) + .enter() + .append("polygon") + .attr("points", d => getPackPolygon(d)) + .attr("id", d => base + d); }); TIME && console.timeEnd("restoreRiskedData"); @@ -410,7 +431,7 @@ function editHeightmap() { // trigger heightmap redraw and history update if at least 1 cell is changed function updateHeightmap() { const prev = last(edits); - const changed = grid.cells.h.reduce((s, h, i) => h !== prev[i] ? s+1 : s, 0); + const changed = grid.cells.h.reduce((s, h, i) => (h !== prev[i] ? s + 1 : s), 0); tip("Cells changed: " + changed); if (!changed) return; @@ -429,8 +450,13 @@ function editHeightmap() { function mockHeightmap() { const data = renderOcean.checked ? grid.cells.i : grid.cells.i.filter(i => grid.cells.h[i] >= 20); const scheme = getColorScheme(); - viewbox.select("#heights").selectAll("polygon").data(data).join("polygon") - .attr("points", d => getGridPolygon(d)).attr("id", d => "cell"+d) + viewbox + .select("#heights") + .selectAll("polygon") + .data(data) + .join("polygon") + .attr("points", d => getGridPolygon(d)) + .attr("id", d => "cell" + d) .attr("fill", d => getColor(grid.cells.h[d], scheme)); } @@ -439,18 +465,26 @@ function editHeightmap() { const ocean = renderOcean.checked; const scheme = getColorScheme(); - selection.forEach(function(i) { - let cell = viewbox.select("#heights").select("#cell"+i); - if (!ocean && grid.cells.h[i] < 20) {cell.remove(); return;} - if (!cell.size()) cell = viewbox.select("#heights").append("polygon").attr("points", getGridPolygon(i)).attr("id", "cell"+i); + selection.forEach(function (i) { + let cell = viewbox.select("#heights").select("#cell" + i); + if (!ocean && grid.cells.h[i] < 20) { + cell.remove(); + return; + } + if (!cell.size()) + cell = viewbox + .select("#heights") + .append("polygon") + .attr("points", getGridPolygon(i)) + .attr("id", "cell" + i); cell.attr("fill", getColor(grid.cells.h[i], scheme)); }); } function updateStatistics() { - const landCells = grid.cells.h.reduce((s, h) => h >= 20 ? s+1 : s); - landmassCounter.innerHTML = `${landCells} (${rn(landCells/grid.cells.i.length*100)}%)`; - landmassAverage.innerHTML = rn(d3.mean(grid.cells.h)); + const landCells = grid.cells.h.reduce((s, h) => (h >= 20 ? s + 1 : s)); + landmassCounter.innerHTML = `${landCells} (${rn((landCells / grid.cells.i.length) * 100)}%)`; + landmassAverage.innerHTML = rn(d3.mean(grid.cells.h)); } function updateHistory(noStat) { @@ -477,7 +511,7 @@ function editHeightmap() { grid.cells.h = edits[edits.n - 1].slice(); mockHeightmap(); updateStatistics(); - + if (document.getElementById("preview")) drawHeightmapPreview(); // update heightmap preview if opened if (document.getElementById("canvas3d")) ThreeD.redraw(); // update 3d heightmap preview if opened } @@ -493,10 +527,13 @@ function editHeightmap() { function openBrushesPanel() { if ($("#brushesPanel").is(":visible")) return; - $("#brushesPanel").dialog({ - title: "Paint Brushes", resizable: false, - position: {my: "right top", at: "right-10 top+10", of: "svg"} - }).on('dialogclose', exitBrushMode); + $("#brushesPanel") + .dialog({ + title: "Paint Brushes", + resizable: false, + position: {my: "right top", at: "right-10 top+10", of: "svg"} + }) + .on("dialogclose", exitBrushMode); if (modules.openBrushesPanel) return; modules.openBrushesPanel = true; @@ -504,8 +541,8 @@ function editHeightmap() { // add listeners document.getElementById("brushesButtons").addEventListener("click", e => toggleBrushMode(e)); document.getElementById("changeOnlyLand").addEventListener("click", e => changeOnlyLandClick(e)); - document.getElementById("undo").addEventListener("click", () => restoreHistory(edits.n-1)); - document.getElementById("redo").addEventListener("click", () => restoreHistory(edits.n+1)); + document.getElementById("undo").addEventListener("click", () => restoreHistory(edits.n - 1)); + document.getElementById("redo").addEventListener("click", () => restoreHistory(edits.n + 1)); document.getElementById("rescaleShow").addEventListener("click", () => { document.getElementById("modifyButtons").style.display = "none"; document.getElementById("rescaleSection").style.display = "block"; @@ -513,8 +550,8 @@ function editHeightmap() { document.getElementById("rescaleHide").addEventListener("click", () => { document.getElementById("modifyButtons").style.display = "block"; document.getElementById("rescaleSection").style.display = "none"; - }); - document.getElementById("rescaler").addEventListener("change", (e) => rescale(e.target.valueAsNumber)); + }); + document.getElementById("rescaler").addEventListener("change", e => rescale(e.target.valueAsNumber)); document.getElementById("rescaleCondShow").addEventListener("click", () => { document.getElementById("modifyButtons").style.display = "none"; document.getElementById("rescaleCondSection").style.display = "block"; @@ -527,7 +564,7 @@ function editHeightmap() { document.getElementById("smoothHeights").addEventListener("click", smoothAllHeights); document.getElementById("disruptHeights").addEventListener("click", disruptAllHeights); document.getElementById("brushClear").addEventListener("click", startFromScratch); - + function exitBrushMode() { const pressed = document.querySelector("#brushesButtons > button.pressed"); if (!pressed) return; @@ -539,7 +576,10 @@ function editHeightmap() { } function toggleBrushMode(e) { - if (e.target.classList.contains("pressed")) {exitBrushMode(); return;} + if (e.target.classList.contains("pressed")) { + exitBrushMode(); + return; + } exitBrushMode(); document.getElementById("brushesSliders").style.display = "block"; e.target.classList.add("pressed"); @@ -568,17 +608,19 @@ function editHeightmap() { const power = brushPower.valueAsNumber; const interpolate = d3.interpolateRound(power, 1); const land = changeOnlyLand.checked; - function lim(v) {return Math.max(Math.min(v, 100), land ? 20 : 0);} + function lim(v) { + return Math.max(Math.min(v, 100), land ? 20 : 0); + } const h = grid.cells.h; const brush = document.querySelector("#brushesButtons > button.pressed").id; - if (brush === "brushRaise") s.forEach(i => h[i] = h[i] < 20 ? 20 : lim(h[i] + power)); else - if (brush === "brushElevate") s.forEach((i,d) => h[i] = lim(h[i] + interpolate(d/Math.max(s.length-1, 1)))); else - if (brush === "brushLower") s.forEach(i => h[i] = lim(h[i] - power)); else - if (brush === "brushDepress") s.forEach((i,d) => h[i] = lim(h[i] - interpolate(d/Math.max(s.length-1, 1)))); else - if (brush === "brushAlign") s.forEach(i => h[i] = lim(h[start])); else - if (brush === "brushSmooth") s.forEach(i => h[i] = rn((d3.mean(grid.cells.c[i].filter(i => land ? h[i] >= 20 : 1).map(c => h[c])) + h[i]*(10-power) + .6) / (11-power),1)); else - if (brush === "brushDisrupt") s.forEach(i => h[i] = h[i] < 15 ? h[i] : lim(h[i] + power/1.6 - Math.random()*power)); + if (brush === "brushRaise") s.forEach(i => (h[i] = h[i] < 20 ? 20 : lim(h[i] + power))); + else if (brush === "brushElevate") s.forEach((i, d) => (h[i] = lim(h[i] + interpolate(d / Math.max(s.length - 1, 1))))); + else if (brush === "brushLower") s.forEach(i => (h[i] = lim(h[i] - power))); + else if (brush === "brushDepress") s.forEach((i, d) => (h[i] = lim(h[i] - interpolate(d / Math.max(s.length - 1, 1))))); + else if (brush === "brushAlign") s.forEach(i => (h[i] = lim(h[start]))); + else if (brush === "brushSmooth") s.forEach(i => (h[i] = rn((d3.mean(grid.cells.c[i].filter(i => (land ? h[i] >= 20 : 1)).map(c => h[c])) + h[i] * (10 - power) + 0.6) / (11 - power), 1))); + else if (brush === "brushDisrupt") s.forEach(i => (h[i] = h[i] < 15 ? h[i] : lim(h[i] + power / 1.6 - Math.random() * power))); mockHeightmapSelection(s); // updateHistory(); uncomment to update history every step @@ -592,7 +634,7 @@ function editHeightmap() { function rescale(v) { const land = changeOnlyLand.checked; - grid.cells.h = grid.cells.h.map(h => land && (h < 20 || h+v < 20) ? h : lim(h+v)); + grid.cells.h = grid.cells.h.map(h => (land && (h < 20 || h + v < 20) ? h : lim(h + v))); updateHeightmap(); document.getElementById("rescaler").value = 0; } @@ -601,15 +643,21 @@ function editHeightmap() { const range = rescaleLower.value + "-" + rescaleHigher.value; const operator = conditionSign.value; const operand = rescaleModifier.valueAsNumber; - if (Number.isNaN(operand)) {tip("Operand should be a number", false, "error"); return;} - if ((operator === "add" || operator === "subtract") && !Number.isInteger(operand)) {tip("Operand should be an integer", false, "error"); return;} + if (Number.isNaN(operand)) { + tip("Operand should be a number", false, "error"); + return; + } + if ((operator === "add" || operator === "subtract") && !Number.isInteger(operand)) { + tip("Operand should be an integer", false, "error"); + return; + } + + if (operator === "multiply") HeightmapGenerator.modify(range, 0, operand, 0); + else if (operator === "divide") HeightmapGenerator.modify(range, 0, 1 / operand, 0); + else if (operator === "add") HeightmapGenerator.modify(range, operand, 1, 0); + else if (operator === "subtract") HeightmapGenerator.modify(range, -1 * operand, 1, 0); + else if (operator === "exponent") HeightmapGenerator.modify(range, 0, 1, operand); - if (operator === "multiply") HeightmapGenerator.modify(range, 0, operand, 0); else - if (operator === "divide") HeightmapGenerator.modify(range, 0, 1 / operand, 0); else - if (operator === "add") HeightmapGenerator.modify(range, operand, 1, 0); else - if (operator === "subtract") HeightmapGenerator.modify(range, -1 * operand, 1, 0); else - if (operator === "exponent") HeightmapGenerator.modify(range, 0, 1, operand); - updateHeightmap(); } @@ -619,19 +667,24 @@ function editHeightmap() { } function disruptAllHeights() { - grid.cells.h = grid.cells.h.map(h => h < 15 ? h : lim(h + 2.5 - Math.random() * 4)); + grid.cells.h = grid.cells.h.map(h => (h < 15 ? h : lim(h + 2.5 - Math.random() * 4))); updateHeightmap(); } - + function startFromScratch() { - if (changeOnlyLand.checked) {tip("Not allowed when 'Change only land cells' mode is set", false, "error"); return;} + if (changeOnlyLand.checked) { + tip("Not allowed when 'Change only land cells' mode is set", false, "error"); + return; + } const someHeights = grid.cells.h.some(h => h); - if (!someHeights) {tip("Heightmap is already cleared, please do not click twice if not required", false, "error"); return;} + if (!someHeights) { + tip("Heightmap is already cleared, please do not click twice if not required", false, "error"); + return; + } grid.cells.h = new Uint8Array(grid.cells.i.length); viewbox.select("#heights").selectAll("*").remove(); updateHistory(); } - } function openTemplateEditor() { @@ -639,22 +692,25 @@ function editHeightmap() { const body = document.getElementById("templateBody"); $("#templateEditor").dialog({ - title: "Template Editor", minHeight: "auto", width: "fit-content", resizable: false, + title: "Template Editor", + minHeight: "auto", + width: "fit-content", + resizable: false, position: {my: "right top", at: "right-10 top+10", of: "svg"} }); if (modules.openTemplateEditor) return; modules.openTemplateEditor = true; - + $("#templateBody").sortable({items: "> div", handle: ".icon-resize-vertical", containment: "#templateBody", axis: "y"}); // add listeners - body.addEventListener("click", function(ev) { + body.addEventListener("click", function (ev) { const el = ev.target; if (el.classList.contains("icon-check")) { el.classList.remove("icon-check"); el.classList.add("icon-check-empty"); - el.parentElement.style.opacity = .5; + el.parentElement.style.opacity = 0.5; body.dataset.changed = 1; return; } @@ -665,7 +721,8 @@ function editHeightmap() { return; } if (el.classList.contains("icon-trash-empty")) { - el.parentElement.remove(); return; + el.parentElement.remove(); + return; } }); @@ -674,7 +731,9 @@ function editHeightmap() { document.getElementById("templateRun").addEventListener("click", executeTemplate); document.getElementById("templateSave").addEventListener("click", downloadTemplate); document.getElementById("templateLoad").addEventListener("click", () => templateToLoad.click()); - document.getElementById("templateToLoad").addEventListener("change", function() {uploadFile(this, uploadTemplate)}); + document.getElementById("templateToLoad").addEventListener("change", function () { + uploadFile(this, uploadTemplate); + }); function addStepOnClick(e) { if (e.target.tagName !== "BUTTON") return; @@ -684,12 +743,14 @@ function editHeightmap() { } function addStep(type, count, dist, arg4, arg5) { - const body = document.getElementById("templateBody"); + const body = document.getElementById("templateBody"); body.insertAdjacentHTML("beforeend", getStepHTML(type, count, dist, arg4, arg5)); const elDist = body.querySelector("div:last-child").querySelector(".templateDist"); if (elDist) elDist.addEventListener("change", setRange); if (dist && elDist && elDist.tagName === "SELECT") { - for (const o of elDist.options) {if (o.value === dist) elDist.value = dist;} + for (const o of elDist.options) { + if (o.value === dist) elDist.value = dist; + } if (elDist.value !== dist) { const opt = document.createElement("option"); opt.value = opt.innerHTML = dist; @@ -705,23 +766,23 @@ function editHeightmap() { const Reorder = ``; const common = `
${Hide}
${type}
${Trash}${Reorder}`; - const TempY = `y:`; - const TempX = `x:`; - const Height = `h:`; - const Count = `n:`; + const TempY = `y:`; + const TempX = `x:`; + const Height = `h:`; + const Count = `n:`; const blob = `${common}${TempY}${TempX}${Height}${Count}
`; if (type === "Hill" || type === "Pit" || type === "Range" || type === "Trough") return blob; - if (type === "Strait") return `${common}d:w:`; - if (type === "Add") return `${common}to:v:`; - if (type === "Multiply") return `${common}to:v:`; - if (type === "Smooth") return `${common}f:`; + if (type === "Strait") return `${common}d:w:`; + if (type === "Add") return `${common}to:v:`; + if (type === "Multiply") return `${common}to:v:`; + if (type === "Smooth") return `${common}f:`; } function setRange(event) { if (event.target.value !== "interval") return; - prompt("Set a height interval. Avoid space, use hyphen as a separator", {default:"17-20"}, v => { + prompt("Set a height interval. Avoid space, use hyphen as a separator", {default: "17-20"}, v => { const opt = document.createElement("option"); opt.value = opt.innerHTML = v; event.target.add(opt); @@ -734,13 +795,24 @@ function editHeightmap() { const steps = body.querySelectorAll("div").length; const changed = +body.getAttribute("data-changed"); const template = e.target.value; - if (!steps || !changed) {changeTemplate(template); return;} + if (!steps || !changed) { + changeTemplate(template); + return; + } alertMessage.innerHTML = "Are you sure you want to select a different template? All changes will be lost."; - $("#alert").dialog({resizable: false, title: "Change Template", + $("#alert").dialog({ + resizable: false, + title: "Change Template", buttons: { - Change: function() {changeTemplate(template); $(this).dialog("close");}, - Cancel: function() {$(this).dialog("close");}} + Change: function () { + changeTemplate(template); + $(this).dialog("close"); + }, + Cancel: function () { + $(this).dialog("close"); + } + } }); } @@ -751,15 +823,13 @@ function editHeightmap() { if (template === "templateVolcano") { addStep("Hill", "1", "90-100", "44-56", "40-60"); - addStep("Multiply", .8, "50-100"); + addStep("Multiply", 0.8, "50-100"); addStep("Range", "1.5", "30-55", "45-55", "40-60"); addStep("Smooth", 2); addStep("Hill", "1.5", "25-35", "25-30", "20-75"); addStep("Hill", "1", "25-35", "75-80", "25-75"); addStep("Hill", "0.5", "20-25", "10-15", "20-25"); - } - - else if (template === "templateHighIsland") { + } else if (template === "templateHighIsland") { addStep("Hill", "1", "90-100", "65-75", "47-53"); addStep("Add", 5, "all"); addStep("Hill", "6", "20-23", "25-55", "45-55"); @@ -769,13 +839,11 @@ function editHeightmap() { addStep("Trough", "2-3", "20-30", "60-80", "70-80"); addStep("Hill", "1", "10-15", "60-60", "50-50"); addStep("Hill", "1.5", "13-16", "15-20", "20-75"); - addStep("Multiply", .8, "20-100"); + addStep("Multiply", 0.8, "20-100"); addStep("Range", "1.5", "30-40", "15-85", "30-40"); addStep("Range", "1.5", "30-40", "15-85", "60-70"); addStep("Pit", "2-3", "10-15", "15-85", "20-80"); - } - - else if (template === "templateLowIsland") { + } else if (template === "templateLowIsland") { addStep("Hill", "1", "90-99", "60-80", "45-55"); addStep("Hill", "4-5", "25-35", "20-65", "40-60"); addStep("Range", "1", "40-50", "45-55", "45-55"); @@ -785,13 +853,11 @@ function editHeightmap() { addStep("Hill", "1.5", "10-15", "5-15", "20-80"); addStep("Hill", "1", "10-15", "85-95", "70-80"); addStep("Pit", "3-5", "10-15", "15-85", "20-80"); - addStep("Multiply", .4, "20-100"); - } - - else if (template === "templateContinents") { + addStep("Multiply", 0.4, "20-100"); + } else if (template === "templateContinents") { addStep("Hill", "1", "80-85", "75-80", "40-60"); addStep("Hill", "1", "80-85", "20-25", "40-60"); - addStep("Multiply", .22, "20-100"); + addStep("Multiply", 0.22, "20-100"); addStep("Hill", "5-6", "15-20", "25-75", "20-82"); addStep("Range", ".8", "30-60", "5-15", "20-45"); addStep("Range", ".8", "30-60", "5-15", "55-80"); @@ -802,9 +868,7 @@ function editHeightmap() { addStep("Trough", "1-2", "5-10", "45-55", "45-55"); addStep("Pit", "3-4", "10-15", "15-85", "20-80"); addStep("Hill", "1", "5-10", "40-60", "40-60"); - } - - else if (template === "templateArchipelago") { + } else if (template === "templateArchipelago") { addStep("Add", 11, "all"); addStep("Range", "2-3", "40-60", "20-80", "20-80"); addStep("Hill", "5", "15-20", "10-90", "30-70"); @@ -814,18 +878,14 @@ function editHeightmap() { addStep("Trough", "10", "20-30", "5-95", "5-95"); addStep("Strait", "2", "vertical"); addStep("Strait", "2", "horizontal"); - } - - else if (template === "templateAtoll") { + } else if (template === "templateAtoll") { addStep("Hill", "1", "75-80", "50-60", "45-55"); addStep("Hill", "1.5", "30-50", "25-75", "30-70"); addStep("Hill", ".5", "30-50", "25-35", "30-70"); addStep("Smooth", 1); - addStep("Multiply", .2, "25-100"); - addStep("Hill", ".5", "10-20", "50-55", "48-52"); - } - - else if (template === "templateMediterranean") { + addStep("Multiply", 0.2, "25-100"); + addStep("Hill", ".5", "10-20", "50-55", "48-52"); + } else if (template === "templateMediterranean") { addStep("Range", "3-4", "30-50", "0-100", "0-10"); addStep("Range", "3-4", "30-50", "0-100", "90-100"); addStep("Hill", "5-6", "30-70", "0-100", "0-5"); @@ -833,12 +893,10 @@ function editHeightmap() { addStep("Smooth", 1); addStep("Hill", "2-3", "30-70", "0-5", "20-80"); addStep("Hill", "2-3", "30-70", "95-100", "20-80"); - addStep("Multiply", .8, "land"); + addStep("Multiply", 0.8, "land"); addStep("Trough", "3-5", "40-50", "0-100", "0-10"); addStep("Trough", "3-5", "40-50", "0-100", "90-100"); - } - - else if (template === "templatePeninsula") { + } else if (template === "templatePeninsula") { addStep("Range", "2-3", "20-35", "40-50", "0-15"); addStep("Add", 5, "all"); addStep("Hill", "1", "90-100", "10-90", "0-5"); @@ -847,22 +905,18 @@ function editHeightmap() { addStep("Hill", "1-2", "3-5", "5-95", "40-60"); addStep("Trough", "5-6", "10-25", "5-95", "5-95"); addStep("Smooth", 3); - } - - else if (template === "templatePangea") { + } else if (template === "templatePangea") { addStep("Hill", "1-2", "25-40", "15-50", "0-10"); addStep("Hill", "1-2", "5-40", "50-85", "0-10"); addStep("Hill", "1-2", "25-40", "50-85", "90-100"); addStep("Hill", "1-2", "5-40", "15-50", "90-100"); addStep("Hill", "8-12", "20-40", "20-80", "48-52"); addStep("Smooth", 2); - addStep("Multiply", .7, "land"); + addStep("Multiply", 0.7, "land"); addStep("Trough", "3-4", "25-35", "5-95", "10-20"); addStep("Trough", "3-4", "25-35", "5-95", "80-90"); addStep("Range", "5-6", "30-40", "10-90", "35-65"); - } - - else if (template === "templateIsthmus") { + } else if (template === "templateIsthmus") { addStep("Hill", "5-10", "15-30", "0-30", "0-20"); addStep("Hill", "5-10", "15-30", "10-50", "20-40"); addStep("Hill", "5-10", "15-30", "30-70", "40-60"); @@ -874,15 +928,12 @@ function editHeightmap() { addStep("Trough", "4-8", "15-30", "30-70", "40-60"); addStep("Trough", "4-8", "15-30", "50-90", "60-80"); addStep("Trough", "4-8", "15-30", "70-100", "80-100"); - } - - else if (template === "templateShattered") { + } else if (template === "templateShattered") { addStep("Hill", "8", "35-40", "15-85", "30-70"); addStep("Trough", "10-20", "40-50", "5-95", "5-95"); addStep("Range", "5-7", "30-40", "10-90", "20-80"); addStep("Pit", "12-20", "30-40", "15-85", "20-80"); } - } function executeTemplate() { @@ -893,7 +944,7 @@ function editHeightmap() { grid.cells.h = new Uint8Array(grid.cells.i.length); // clean all heights for (const s of steps) { - if (s.style.opacity == .5) continue; + if (s.style.opacity == 0.5) continue; const type = s.getAttribute("data-type"); const elCount = s.querySelector(".templateCount") || ""; const elHeight = s.querySelector(".templateHeight") || ""; @@ -906,14 +957,14 @@ function editHeightmap() { const templateY = s.querySelector(".templateY"); const y = templateY ? templateY.value : null; - if (type === "Hill") HeightmapGenerator.addHill(elCount.value, elHeight.value, x, y); else - if (type === "Pit") HeightmapGenerator.addPit(elCount.value, elHeight.value, x, y); else - if (type === "Range") HeightmapGenerator.addRange(elCount.value, elHeight.value, x, y); else - if (type === "Trough") HeightmapGenerator.addTrough(elCount.value, elHeight.value, x, y); else - if (type === "Strait") HeightmapGenerator.addStrait(elCount.value, dist); else - if (type === "Add") HeightmapGenerator.modify(dist, +elCount.value, 1); else - if (type === "Multiply") HeightmapGenerator.modify(dist, 0, +elCount.value); else - if (type === "Smooth") HeightmapGenerator.smooth(+elCount.value); + if (type === "Hill") HeightmapGenerator.addHill(elCount.value, elHeight.value, x, y); + else if (type === "Pit") HeightmapGenerator.addPit(elCount.value, elHeight.value, x, y); + else if (type === "Range") HeightmapGenerator.addRange(elCount.value, elHeight.value, x, y); + else if (type === "Trough") HeightmapGenerator.addTrough(elCount.value, elHeight.value, x, y); + else if (type === "Strait") HeightmapGenerator.addStrait(elCount.value, dist); + else if (type === "Add") HeightmapGenerator.modify(dist, +elCount.value, 1); + else if (type === "Multiply") HeightmapGenerator.modify(dist, 0, +elCount.value); + else if (type === "Smooth") HeightmapGenerator.smooth(+elCount.value); updateHistory("noStat"); // update history every step } @@ -932,7 +983,7 @@ function editHeightmap() { let data = ""; for (const s of steps) { - if (s.style.opacity == .5) continue; + if (s.style.opacity == 0.5) continue; const type = s.getAttribute("data-type"); const elCount = s.querySelector(".templateCount"); const count = elCount ? elCount.value : "0"; @@ -952,14 +1003,19 @@ function editHeightmap() { function uploadTemplate(dataLoaded) { const steps = dataLoaded.split("\r\n"); - if (!steps.length) {tip("Cannot parse the template, please check the file", false, "error"); return;} + if (!steps.length) { + tip("Cannot parse the template, please check the file", false, "error"); + return; + } templateBody.innerHTML = ""; for (const s of steps) { const step = s.split(" "); - if (step.length !== 5) {ERROR && console.error("Cannot parse step, wrong arguments count", s); continue;} + if (step.length !== 5) { + ERROR && console.error("Cannot parse step, wrong arguments count", s); + continue; + } addStep(step[0], step[1], step[2], step[3], step[4]); } - } } @@ -969,7 +1025,10 @@ function editHeightmap() { closeDialogs("#imageConverter"); $("#imageConverter").dialog({ - title: "Image Converter", maxHeight: svgHeight*.8, minHeight: "auto", width: "20em", + title: "Image Converter", + maxHeight: svgHeight * 0.8, + minHeight: "auto", + width: "20em", position: {my: "right top", at: "right-10 top+10", of: "svg"}, beforeClose: closeImageConverter }); @@ -983,7 +1042,7 @@ function editHeightmap() { setOverlayOpacity(0); clearMainTip(); - tip('Image Converter is opened. Upload image and assign height value for each color', false, "warn"); // main tip + tip("Image Converter is opened. Upload image and assign height value for each color", false, "warn"); // main tip // remove all heights grid.cells.h = new Uint8Array(grid.cells.i.length); @@ -994,13 +1053,18 @@ function editHeightmap() { modules.openImageConverter = true; // add color pallete - void function createColorPallete() { - d3.select("#imageConverterPalette").selectAll("div").data(d3.range(101)) - .enter().append("div").attr("data-color", i => i) - .style("background-color", i => color(1-(i < 20 ? i-5 : i) / 100)) - .style("width", i => i < 40 || i > 68 ? ".2em" : ".1em") - .on("touchmove mousemove", showPalleteHeight).on("click", assignHeight); - }() + void (function createColorPallete() { + d3.select("#imageConverterPalette") + .selectAll("div") + .data(d3.range(101)) + .enter() + .append("div") + .attr("data-color", i => i) + .style("background-color", i => color(1 - (i < 20 ? i - 5 : i) / 100)) + .style("width", i => (i < 40 || i > 68 ? ".2em" : ".1em")) + .on("touchmove mousemove", showPalleteHeight) + .on("click", assignHeight); + })(); // add listeners document.getElementById("convertImageLoad").addEventListener("click", () => imageToLoad.click()); @@ -1011,14 +1075,18 @@ function editHeightmap() { document.getElementById("convertColorsButton").addEventListener("click", setConvertColorsNumber); document.getElementById("convertComplete").addEventListener("click", applyConversion); document.getElementById("convertCancel").addEventListener("click", cancelConversion); - document.getElementById("convertOverlay").addEventListener("input", function() {setOverlayOpacity(this.value)}); - document.getElementById("convertOverlayNumber").addEventListener("input", function() {setOverlayOpacity(this.value)}); + document.getElementById("convertOverlay").addEventListener("input", function () { + setOverlayOpacity(this.value); + }); + document.getElementById("convertOverlayNumber").addEventListener("input", function () { + setOverlayOpacity(this.value); + }); function showPalleteHeight() { const height = +this.getAttribute("data-color"); colorsSelectValue.innerHTML = height; colorsSelectFriendly.innerHTML = getHeight(height); - const former = imageConverterPalette.querySelector(".hoveredColor") + const former = imageConverterPalette.querySelector(".hoveredColor"); if (former) former.className = ""; this.className = "hoveredColor"; } @@ -1028,15 +1096,15 @@ function editHeightmap() { this.value = ""; // reset input value to get triggered if the file is re-uploaded const reader = new FileReader(); - const img = new Image; - img.onload = function() { + const img = new Image(); + img.onload = function () { const ctx = document.getElementById("canvas").getContext("2d"); ctx.drawImage(img, 0, 0, graphWidth, graphHeight); heightsFromImage(+convertColors.value); resetZoom(); }; - reader.onloadend = () => img.src = reader.result; + reader.onloadend = () => (img.src = reader.result); reader.readAsDataURL(file); } @@ -1045,9 +1113,9 @@ function editHeightmap() { const sampleCanvas = document.createElement("canvas"); sampleCanvas.width = grid.cellsX; sampleCanvas.height = grid.cellsY; - sampleCanvas.getContext('2d').drawImage(sourceImage, 0, 0, grid.cellsX, grid.cellsY); + sampleCanvas.getContext("2d").drawImage(sourceImage, 0, 0, grid.cellsX, grid.cellsY); - const q = new RgbQuant({colors:count}); + const q = new RgbQuant({colors: count}); q.sample(sampleCanvas); const data = q.reduce(sampleCanvas); const pallete = q.palette(true); @@ -1059,15 +1127,26 @@ function editHeightmap() { colorsAssigned.style.display = "none"; sampleCanvas.remove(); // no need to keep - viewbox.select("#heights").selectAll("polygon").data(grid.cells.i).join("polygon") - .attr("points", d => getGridPolygon(d)).attr("id", d => "cell"+d) - .attr("fill", d => `rgb(${data[d*4]}, ${data[d*4+1]}, ${data[d*4+2]})`) + viewbox + .select("#heights") + .selectAll("polygon") + .data(grid.cells.i) + .join("polygon") + .attr("points", d => getGridPolygon(d)) + .attr("id", d => "cell" + d) + .attr("fill", d => `rgb(${data[d * 4]}, ${data[d * 4 + 1]}, ${data[d * 4 + 2]})`) .on("click", mapClicked); const colors = pallete.map(p => `rgb(${p[0]}, ${p[1]}, ${p[2]})`); - d3.select("#colorsUnassigned").selectAll("div").data(colors).enter().append("div") - .attr("data-color", i => i).style("background-color", i => i) - .attr("class", "color-div").on("click", colorClicked); + d3.select("#colorsUnassigned") + .selectAll("div") + .data(colors) + .enter() + .append("div") + .attr("data-color", i => i) + .style("background-color", i => i) + .attr("class", "color-div") + .on("click", colorClicked); document.getElementById("colorsUnassignedNumber").innerHTML = colors.length; } @@ -1100,21 +1179,27 @@ function editHeightmap() { const color = this.getAttribute("data-color"); viewbox.select("#heights").selectAll("polygon.selectedCell").classed("selectedCell", 0); - viewbox.select("#heights").selectAll("polygon[fill='" + color + "']").classed("selectedCell", 1); + viewbox + .select("#heights") + .selectAll("polygon[fill='" + color + "']") + .classed("selectedCell", 1); } function assignHeight() { const height = +this.dataset.color; - const rgb = color(1 - (height < 20 ? height-5 : height) / 100); + const rgb = color(1 - (height < 20 ? height - 5 : height) / 100); const selectedColor = imageConverter.querySelector("div.selectedColor"); selectedColor.style.backgroundColor = rgb; selectedColor.setAttribute("data-color", rgb); selectedColor.setAttribute("data-height", height); - viewbox.select("#heights").selectAll(".selectedCell").each(function() { - this.setAttribute("fill", rgb); - this.setAttribute("data-height", height); - }); + viewbox + .select("#heights") + .selectAll(".selectedCell") + .each(function () { + this.setAttribute("fill", rgb); + this.setAttribute("data-height", height); + }); if (selectedColor.parentNode.id === "colorsUnassigned") { colorsAssigned.appendChild(selectedColor); @@ -1123,7 +1208,6 @@ function editHeightmap() { document.getElementById("colorsUnassignedNumber").innerHTML = colorsUnassigned.childElementCount - 2; document.getElementById("colorsAssignedNumber").innerHTML = colorsAssigned.childElementCount - 2; } - } // auto assign color based on luminosity or hue @@ -1133,42 +1217,49 @@ function editHeightmap() { heightsFromImage(+convertColors.value); unassigned = colorsUnassigned.querySelectorAll("div"); if (!unassigned.length) { - tip("No unassigned colors. Please load an image and click the button again", false, "error"); + tip("No unassigned colors. Please load an image and click the button again", false, "error"); return; } } - const getHeightByHue = function(color) { + const getHeightByHue = function (color) { let hue = d3.hsl(color).h; if (hue > 300) hue -= 360; - if (hue > 170) return Math.abs(hue-250) / 3 |0; // water - return Math.abs(hue-250+20) / 3 |0; // land - } + if (hue > 170) return (Math.abs(hue - 250) / 3) | 0; // water + return (Math.abs(hue - 250 + 20) / 3) | 0; // land + }; - const getHeightByLum = function(color) { + const getHeightByLum = function (color) { let lum = d3.lab(color).l; - if (lum < 13) return lum / 13 * 20 |0; // water - return lum|0; // land - } + if (lum < 13) return ((lum / 13) * 20) | 0; // water + return lum | 0; // land + }; const scheme = d3.range(101).map(i => getColor(i, color())); - const hues = scheme.map(rgb => d3.hsl(rgb).h|0); - const getHeightByScheme = function(color) { + const hues = scheme.map(rgb => d3.hsl(rgb).h | 0); + const getHeightByScheme = function (color) { let height = scheme.indexOf(color); if (height !== -1) return height; // exact match const hue = d3.hsl(color).h; const closest = hues.reduce((prev, curr) => (Math.abs(curr - hue) < Math.abs(prev - hue) ? curr : prev)); return hues.indexOf(closest); - } + }; const assinged = []; // store assigned heights unassigned.forEach(el => { const clr = el.dataset.color; const height = type === "hue" ? getHeightByHue(clr) : type === "lum" ? getHeightByLum(clr) : getHeightByScheme(clr); - const colorTo = color(1 - (height < 20 ? (height-5) / 100 : height / 100)); - viewbox.select("#heights").selectAll("polygon[fill='" + clr + "']").attr("fill", colorTo).attr("data-height", height); + const colorTo = color(1 - (height < 20 ? (height - 5) / 100 : height / 100)); + viewbox + .select("#heights") + .selectAll("polygon[fill='" + clr + "']") + .attr("fill", colorTo) + .attr("data-height", height); - if (assinged[height]) {el.remove(); return;} // if color is already added, remove it + if (assinged[height]) { + el.remove(); + return; + } // if color is already added, remove it el.style.backgroundColor = el.dataset.color = colorTo; el.dataset.height = height; colorsAssigned.appendChild(el); @@ -1186,8 +1277,7 @@ function editHeightmap() { } function setConvertColorsNumber() { - prompt(`Please set maximum number of colors.
An actual number is usually lower and depends on color scheme`, - {default:+convertColors.value, step:1, min:3, max:255}, number => { + prompt(`Please set maximum number of colors.
An actual number is usually lower and depends on color scheme`, {default: +convertColors.value, step: 1, min: 3, max: 255}, number => { convertColors.value = number; heightsFromImage(number); }); @@ -1204,11 +1294,14 @@ function editHeightmap() { return; } - viewbox.select("#heights").selectAll("polygon").each(function() { - const height = +this.dataset.height || 0; - const i = +this.id.slice(4); - grid.cells.h[i] = height; - }); + viewbox + .select("#heights") + .selectAll("polygon") + .each(function () { + const height = +this.dataset.height || 0; + const i = +this.id.slice(4); + grid.cells.h[i] = height; + }); viewbox.select("#heights").selectAll("polygon").remove(); updateHeightmap(); @@ -1218,7 +1311,7 @@ function editHeightmap() { function cancelConversion() { restoreImageConverterState(); viewbox.select("#heights").selectAll("polygon").remove(); - restoreHistory(edits.n-1); + restoreHistory(edits.n - 1); } function restoreImageConverterState() { @@ -1244,20 +1337,22 @@ function editHeightmap() { Click "Complete" to apply the conversion. Click "Close" to exit conversion mode and restore previous heightmap`; - $("#alert").dialog({resizable: false, title: "Close Image Converter", + $("#alert").dialog({ + resizable: false, + title: "Close Image Converter", buttons: { - Cancel: function() { + Cancel: function () { $(this).dialog("close"); }, - Complete: function() { + Complete: function () { $(this).dialog("close"); applyConversion(); }, - Close: function() { + Close: function () { $(this).dialog("close"); restoreImageConverterState(); viewbox.select("#heights").selectAll("polygon").remove(); - restoreHistory(edits.n-1); + restoreHistory(edits.n - 1); } } }); @@ -1285,11 +1380,11 @@ function editHeightmap() { grid.cells.h.forEach((height, i) => { let h = height < 20 ? Math.max(height / 1.5, 0) : height; - const v = h / 100 * 255; - imageData.data[i*4] = v; - imageData.data[i*4 + 1] = v; - imageData.data[i*4 + 2] = v; - imageData.data[i*4 + 3] = 255; + const v = (h / 100) * 255; + imageData.data[i * 4] = v; + imageData.data[i * 4 + 1] = v; + imageData.data[i * 4 + 2] = v; + imageData.data[i * 4 + 3] = 255; }); ctx.putImageData(imageData, 0, 0); @@ -1302,7 +1397,7 @@ function editHeightmap() { const img = new Image(); img.src = dataURL; - img.onload = function() { + img.onload = function () { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); canvas.width = svgWidth; @@ -1315,7 +1410,6 @@ function editHeightmap() { link.href = imgBig; link.click(); canvas.remove(); - } + }; } - } diff --git a/modules/ui/tools.js b/modules/ui/tools.js index 1ab8f192..16b8fa87 100644 --- a/modules/ui/tools.js +++ b/modules/ui/tools.js @@ -1,44 +1,57 @@ // module to control the Tools options (click to edit, to re-geenerate, tp add) "use strict"; -toolsContent.addEventListener("click", function(event) { - if (customization) {tip("Please exit the customization mode first", false, "warning"); return;} +toolsContent.addEventListener("click", function (event) { + if (customization) { + tip("Please exit the customization mode first", false, "warning"); + return; + } if (event.target.tagName !== "BUTTON") return; const button = event.target.id; // Click to open Editor buttons - if (button === "editHeightmapButton") editHeightmap(); else - if (button === "editBiomesButton") editBiomes(); else - if (button === "editStatesButton") editStates(); else - if (button === "editProvincesButton") editProvinces(); else - if (button === "editDiplomacyButton") editDiplomacy(); else - if (button === "editCulturesButton") editCultures(); else - if (button === "editReligions") editReligions(); else - if (button === "editEmblemButton") openEmblemEditor(); else - if (button === "editNamesBaseButton") editNamesbase(); else - if (button === "editUnitsButton") editUnits(); else - if (button === "editNotesButton") editNotes(); else - if (button === "editZonesButton") editZones(); else - if (button === "overviewBurgsButton") overviewBurgs(); else - if (button === "overviewRiversButton") overviewRivers(); else - if (button === "overviewMilitaryButton") overviewMilitary(); else - if (button === "overviewCellsButton") viewCellDetails(); + if (button === "editHeightmapButton") editHeightmap(); + else if (button === "editBiomesButton") editBiomes(); + else if (button === "editStatesButton") editStates(); + else if (button === "editProvincesButton") editProvinces(); + else if (button === "editDiplomacyButton") editDiplomacy(); + else if (button === "editCulturesButton") editCultures(); + else if (button === "editReligions") editReligions(); + else if (button === "editEmblemButton") openEmblemEditor(); + else if (button === "editNamesBaseButton") editNamesbase(); + else if (button === "editUnitsButton") editUnits(); + else if (button === "editNotesButton") editNotes(); + else if (button === "editZonesButton") editZones(); + else if (button === "overviewBurgsButton") overviewBurgs(); + else if (button === "overviewRiversButton") overviewRivers(); + else if (button === "overviewMilitaryButton") overviewMilitary(); + else if (button === "overviewCellsButton") viewCellDetails(); // Click to Regenerate buttons if (event.target.parentNode.id === "regenerateFeature") { - if (sessionStorage.getItem("regenerateFeatureDontAsk")) {processFeatureRegeneration(event, button); return;} + if (sessionStorage.getItem("regenerateFeatureDontAsk")) { + processFeatureRegeneration(event, button); + return; + } - alertMessage.innerHTML = `Regeneration will remove all the custom changes for the element.

Are you sure you want to proceed?` - $("#alert").dialog({resizable: false, title: "Regenerate element", + alertMessage.innerHTML = `Regeneration will remove all the custom changes for the element.

Are you sure you want to proceed?`; + $("#alert").dialog({ + resizable: false, + title: "Regenerate element", buttons: { - Proceed: function() {processFeatureRegeneration(event, button); $(this).dialog("close");}, - Cancel: function() {$(this).dialog("close");} + Proceed: function () { + processFeatureRegeneration(event, button); + $(this).dialog("close"); + }, + Cancel: function () { + $(this).dialog("close"); + } }, - open: function() { + open: function () { const pane = $(this).dialog("widget").find(".ui-dialog-buttonpane"); $('').prependTo(pane); }, - close: function() { + close: function () { const box = $(this).dialog("widget").find(".checkbox")[0]; if (!box) return; if (box.checked) sessionStorage.setItem("regenerateFeatureDontAsk", true); @@ -48,29 +61,35 @@ toolsContent.addEventListener("click", function(event) { } // Click to Add buttons - if (button === "addLabel") toggleAddLabel(); else - if (button === "addBurgTool") toggleAddBurg(); else - if (button === "addRiver") toggleAddRiver(); else - if (button === "addRoute") toggleAddRoute(); else - if (button === "addMarker") toggleAddMarker(); + if (button === "addLabel") toggleAddLabel(); + else if (button === "addBurgTool") toggleAddBurg(); + else if (button === "addRiver") toggleAddRiver(); + else if (button === "addRoute") toggleAddRoute(); + else if (button === "addMarker") toggleAddMarker(); }); function processFeatureRegeneration(event, button) { - if (button === "regenerateStateLabels") {BurgsAndStates.drawStateLabels(); if (!layerIsOn("toggleLabels")) toggleLabels();} else - if (button === "regenerateReliefIcons") {ReliefIcons(); if (!layerIsOn("toggleRelief")) toggleRelief();} else - if (button === "regenerateRoutes") {Routes.regenerate(); if (!layerIsOn("toggleRoutes")) toggleRoutes();} else - if (button === "regenerateRivers") regenerateRivers(); else - if (button === "regeneratePopulation") recalculatePopulation(); else - if (button === "regenerateStates") regenerateStates(); else - if (button === "regenerateProvinces") regenerateProvinces(); else - if (button === "regenerateBurgs") regenerateBurgs(); else - if (button === "regenerateEmblems") regenerateEmblems(); else - if (button === "regenerateReligions") regenerateReligions(); else - if (button === "regenerateCultures") regenerateCultures(); else - if (button === "regenerateMilitary") regenerateMilitary(); else - if (button === "regenerateIce") regenerateIce(); else - if (button === "regenerateMarkers") regenerateMarkers(event); else - if (button === "regenerateZones") regenerateZones(event); + if (button === "regenerateStateLabels") { + BurgsAndStates.drawStateLabels(); + if (!layerIsOn("toggleLabels")) toggleLabels(); + } else if (button === "regenerateReliefIcons") { + ReliefIcons(); + if (!layerIsOn("toggleRelief")) toggleRelief(); + } else if (button === "regenerateRoutes") { + Routes.regenerate(); + if (!layerIsOn("toggleRoutes")) toggleRoutes(); + } else if (button === "regenerateRivers") regenerateRivers(); + else if (button === "regeneratePopulation") recalculatePopulation(); + else if (button === "regenerateStates") regenerateStates(); + else if (button === "regenerateProvinces") regenerateProvinces(); + else if (button === "regenerateBurgs") regenerateBurgs(); + else if (button === "regenerateEmblems") regenerateEmblems(); + else if (button === "regenerateReligions") regenerateReligions(); + else if (button === "regenerateCultures") regenerateCultures(); + else if (button === "regenerateMilitary") regenerateMilitary(); + else if (button === "regenerateIce") regenerateIce(); + else if (button === "regenerateMarkers") regenerateMarkers(event); + else if (button === "regenerateZones") regenerateZones(event); } async function openEmblemEditor() { @@ -106,10 +125,10 @@ function recalculatePopulation() { if (!b.i || b.removed || b.lock) return; const i = b.cell; - b.population = rn(Math.max((pack.cells.s[i] + pack.cells.road[i] / 2) / 8 + b.i / 1000 + i % 100 / 1000, .1), 3); + b.population = rn(Math.max((pack.cells.s[i] + pack.cells.road[i] / 2) / 8 + b.i / 1000 + (i % 100) / 1000, 0.1), 3); if (b.capital) b.population = b.population * 1.3; // increase capital population if (b.port) b.population = b.population * 1.3; // increase port population - b.population = rn(b.population * gauss(2,3,.6,20,3), 3); + b.population = rn(b.population * gauss(2, 3, 0.6, 20, 3), 3); }); } @@ -126,14 +145,19 @@ function regenerateStates() { } // burg local ids sorted by a bit randomized population: - const sorted = burgs.map((b, i) => [i, b.population * Math.random()]).sort((a, b) => b[1] - a[1]).map(b => b[0]); + const sorted = burgs + .map((b, i) => [i, b.population * Math.random()]) + .sort((a, b) => b[1] - a[1]) + .map(b => b[0]); const capitalsTree = d3.quadtree(); // turn all old capitals into towns - burgs.filter(b => b.capital).forEach(b => { - moveBurgToGroup(b.i, "towns"); - b.capital = 0; - }); + burgs + .filter(b => b.capital) + .forEach(b => { + moveBurgToGroup(b.i, "towns"); + b.capital = 0; + }); // remove emblems document.querySelectorAll("[id^=stateCOA]").forEach(el => el.remove()); @@ -145,7 +169,7 @@ function regenerateStates() { // if desired states number is 0 if (regionsInput.value == 0) { tip(`Cannot generate zero states. Please check the States Number option`, false, "warn"); - pack.states = pack.states.slice(0,1); // remove all except of neutrals + pack.states = pack.states.slice(0, 1); // remove all except of neutrals pack.states[0].diplomacy = []; // clear diplomacy pack.provinces = [0]; // remove all provinces pack.cells.state = new Uint16Array(pack.cells.i.length); // reset cells data @@ -165,10 +189,12 @@ function regenerateStates() { pack.states = d3.range(count).map(i => { if (!i) return {i, name: neutral}; - let capital = null, x = 0, y = 0; + let capital = null, + x = 0, + y = 0; for (const i of sorted) { capital = burgs[i]; - x = capital.x, y = capital.y; + (x = capital.x), (y = capital.y); if (capitalsTree.find(x, y, spacing) === undefined) break; spacing = Math.max(spacing - 1, 1); } @@ -178,17 +204,17 @@ function regenerateStates() { moveBurgToGroup(capital.i, "cities"); const culture = capital.culture; - const basename = capital.name.length < 9 && capital.cell%5 === 0 ? capital.name : Names.getCulture(culture, 3, 6, "", 0); + const basename = capital.name.length < 9 && capital.cell % 5 === 0 ? capital.name : Names.getCulture(culture, 3, 6, "", 0); const name = Names.getState(basename, culture); const nomadic = [1, 2, 3, 4].includes(pack.cells.biome[capital.cell]); const type = nomadic ? "Nomadic" : pack.cultures[culture].type === "Nomadic" ? "Generic" : pack.cultures[culture].type; const expansionism = rn(Math.random() * powerInput.value + 1, 1); const cultureType = pack.cultures[culture].type; - const coa = COA.generate(capital.coa, .3, null, cultureType); + const coa = COA.generate(capital.coa, 0.3, null, cultureType); coa.shield = capital.coa.shield; - return {i, name, type, capital:capital.i, center:capital.cell, culture, expansionism, coa}; + return {i, name, type, capital: capital.i, center: capital.cell, culture, expansionism, coa}; }); BurgsAndStates.expandStates(); @@ -199,8 +225,10 @@ function regenerateStates() { BurgsAndStates.generateDiplomacy(); BurgsAndStates.defineStateForms(); BurgsAndStates.generateProvinces(true); - if (!layerIsOn("toggleStates")) toggleStates(); else drawStates(); - if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders(); + if (!layerIsOn("toggleStates")) toggleStates(); + else drawStates(); + if (!layerIsOn("toggleBorders")) toggleBorders(); + else drawBorders(); BurgsAndStates.drawStateLabels(); Military.generate(); if (layerIsOn("toggleEmblems")) drawEmblems(); // redrawEmblems @@ -224,43 +252,52 @@ function regenerateProvinces() { } function regenerateBurgs() { - const cells = pack.cells, states = pack.states, Lockedburgs = pack.burgs.filter(b =>b.lock); + const cells = pack.cells, + states = pack.states, + Lockedburgs = pack.burgs.filter(b => b.lock); rankCells(); cells.burg = new Uint16Array(cells.i.length); - const burgs = pack.burgs = [0]; // clear burgs array - states.filter(s => s.i).forEach(s => s.capital = 0); // clear state capitals - pack.provinces.filter(p => p.i).forEach(p => p.burg = 0); // clear province capitals + const burgs = (pack.burgs = [0]); // clear burgs array + states.filter(s => s.i).forEach(s => (s.capital = 0)); // clear state capitals + pack.provinces.filter(p => p.i).forEach(p => (p.burg = 0)); // clear province capitals const burgsTree = d3.quadtree(); const score = new Int16Array(cells.s.map(s => s * Math.random())); // cell score for capitals placement const sorted = cells.i.filter(i => score[i] > 0 && cells.culture[i]).sort((a, b) => score[b] - score[a]); // filtered and sorted array of indexes - const burgsCount = manorsInput.value == 1000 ? rn(sorted.length / 5 / (grid.points.length / 10000) ** .8) + states.length : +manorsInput.value + states.length; - const spacing = (graphWidth + graphHeight) / 150 / (burgsCount ** .7 / 66); // base min distance between towns + const burgsCount = manorsInput.value == 1000 ? rn(sorted.length / 5 / (grid.points.length / 10000) ** 0.8) + states.length : +manorsInput.value + states.length; + const spacing = (graphWidth + graphHeight) / 150 / (burgsCount ** 0.7 / 66); // base min distance between towns //clear locked list since ids will change //burglock.selectAll("text").remove(); - for (let j=0; j < Lockedburgs.length; j++) { + for (let j = 0; j < Lockedburgs.length; j++) { const id = burgs.length; const oldBurg = Lockedburgs[j]; oldBurg.i = id; burgs.push(oldBurg); burgsTree.add([oldBurg.x, oldBurg.y]); cells.burg[oldBurg.cell] = id; - if (oldBurg.capital) {states[oldBurg.state].capital = id; states[oldBurg.state].center = oldBurg.cell;} + if (oldBurg.capital) { + states[oldBurg.state].capital = id; + states[oldBurg.state].center = oldBurg.cell; + } //burglock.append("text").attr("data-id", id); } - for (let i=0; i < sorted.length && burgs.length < burgsCount; i++) { + for (let i = 0; i < sorted.length && burgs.length < burgsCount; i++) { const id = burgs.length; const cell = sorted[i]; - const x = cells.p[cell][0], y = cells.p[cell][1]; + const x = cells.p[cell][0], + y = cells.p[cell][1]; - const s = spacing * gauss(1, .3, .2, 2, 2); // randomize to make the placement not uniform + const s = spacing * gauss(1, 0.3, 0.2, 2, 2); // randomize to make the placement not uniform if (burgsTree.find(x, y, s) !== undefined) continue; // to close to existing burg const state = cells.state[cell]; const capital = state && !states[state].capital; // if state doesn't have capital, make this burg a capital, no capital for neutral lands - if (capital) {states[state].capital = id; states[state].center = cell;} + if (capital) { + states[state].capital = id; + states[state].center = cell; + } const culture = cells.culture[cell]; const name = Names.getCulture(culture); @@ -270,16 +307,20 @@ function regenerateBurgs() { } // add a capital at former place for states without added capitals - states.filter(s => s.i && !s.removed && !s.capital).forEach(s => { - const burg = addBurg([cells.p[s.center][0], cells.p[s.center][1]]); // add new burg - s.capital = burg; - s.center = pack.burgs[burg].cell; - pack.burgs[burg].capital = 1; - pack.burgs[burg].state = s.i; - moveBurgToGroup(burg, "cities"); - }); + states + .filter(s => s.i && !s.removed && !s.capital) + .forEach(s => { + const burg = addBurg([cells.p[s.center][0], cells.p[s.center][1]]); // add new burg + s.capital = burg; + s.center = pack.burgs[burg].cell; + pack.burgs[burg].capital = 1; + pack.burgs[burg].state = s.i; + moveBurgToGroup(burg, "cities"); + }); - pack.features.forEach(f => {if (f.port) f.port = 0}); // reset features ports counter + pack.features.forEach(f => { + if (f.port) f.port = 0; + }); // reset features ports counter BurgsAndStates.specifyBurgs(); BurgsAndStates.defineBurgFeatures(); BurgsAndStates.drawBurgs(); @@ -313,10 +354,10 @@ function regenerateEmblems() { if (!burg.i || burg.removed) return; const state = pack.states[burg.state]; - let kinship = state ? .25 : 0; - if (burg.capital) kinship += .1; - else if (burg.port) kinship -= .1; - if (state && burg.culture !== state.culture) kinship -= .25; + let kinship = state ? 0.25 : 0; + if (burg.capital) kinship += 0.1; + else if (burg.port) kinship -= 0.1; + if (state && burg.culture !== state.culture) kinship -= 0.25; burg.coa = COA.generate(state ? state.coa : null, kinship, null, burg.type); burg.coa.shield = COA.getShield(burg.culture, state ? burg.state : 0); }); @@ -327,16 +368,16 @@ function regenerateEmblems() { let dominion = false; if (!province.burg) { - dominion = P(.2); - if (province.formName === "Colony") dominion = P(.95); else - if (province.formName === "Island") dominion = P(.6); else - if (province.formName === "Islands") dominion = P(.5); else - if (province.formName === "Territory") dominion = P(.4); else - if (province.formName === "Land") dominion = P(.3); + dominion = P(0.2); + if (province.formName === "Colony") dominion = P(0.95); + else if (province.formName === "Island") dominion = P(0.6); + else if (province.formName === "Islands") dominion = P(0.5); + else if (province.formName === "Territory") dominion = P(0.4); + else if (province.formName === "Land") dominion = P(0.3); } const nameByBurg = province.burg && province.name.slice(0, 3) === parent.name.slice(0, 3); - const kinship = dominion ? 0 : nameByBurg ? .8 : .4; + const kinship = dominion ? 0 : nameByBurg ? 0.8 : 0.4; const culture = pack.cells.culture[province.center]; const type = BurgsAndStates.getType(province.center, parent.port); province.coa = COA.generate(parent.coa, kinship, dominion, type); @@ -348,7 +389,8 @@ function regenerateEmblems() { function regenerateReligions() { Religions.generate(); - if (!layerIsOn("toggleReligions")) toggleReligions(); else drawReligions(); + if (!layerIsOn("toggleReligions")) toggleReligions(); + else drawReligions(); } function regenerateCultures() { @@ -356,7 +398,8 @@ function regenerateCultures() { Cultures.expand(); BurgsAndStates.updateCultures(); Religions.updateCultures(); - if (!layerIsOn("toggleCultures")) toggleCultures(); else drawCultures(); + if (!layerIsOn("toggleCultures")) toggleCultures(); + else drawCultures(); refreshAllEditors(); } @@ -373,15 +416,18 @@ function regenerateIce() { } function regenerateMarkers(event) { - if (isCtrlClick(event)) prompt("Please provide markers number multiplier", {default:1, step:.01, min:0, max:100}, v => addNumberOfMarkers(v)); - else addNumberOfMarkers(gauss(1, .5, .3, 5, 2)); + if (isCtrlClick(event)) prompt("Please provide markers number multiplier", {default: 1, step: 0.01, min: 0, max: 100}, v => addNumberOfMarkers(v)); + else addNumberOfMarkers(gauss(1, 0.5, 0.3, 5, 2)); function addNumberOfMarkers(number) { // remove existing markers and assigned notes - markers.selectAll("use").each(function() { - const index = notes.findIndex(n => n.id === this.id); - if (index != -1) notes.splice(index, 1); - }).remove(); + markers + .selectAll("use") + .each(function () { + const index = notes.findIndex(n => n.id === this.id); + if (index != -1) notes.splice(index, 1); + }) + .remove(); addMarkers(number); if (!layerIsOn("toggleMarkers")) toggleMarkers(); @@ -389,8 +435,8 @@ function regenerateMarkers(event) { } function regenerateZones(event) { - if (isCtrlClick(event)) prompt("Please provide zones number multiplier", {default:1, step:.01, min:0, max:100}, v => addNumberOfZones(v)); - else addNumberOfZones(gauss(1, .5, .6, 5, 2)); + if (isCtrlClick(event)) prompt("Please provide zones number multiplier", {default: 1, step: 0.01, min: 0, max: 100}, v => addNumberOfZones(v)); + else addNumberOfZones(gauss(1, 0.5, 0.6, 5, 2)); function addNumberOfZones(number) { zones.selectAll("g").remove(); // remove existing zones @@ -408,10 +454,13 @@ function unpressClickToAddButton() { function toggleAddLabel() { const pressed = document.getElementById("addLabel").classList.contains("pressed"); - if (pressed) {unpressClickToAddButton(); return;} + if (pressed) { + unpressClickToAddButton(); + return; + } addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed")); - addLabel.classList.add('pressed'); + addLabel.classList.add("pressed"); closeDialogs(".stable"); viewbox.style("cursor", "crosshair").on("click", addLabelOnClick); tip("Click on map to place label. Hold Shift to add multiple", true); @@ -428,10 +477,7 @@ function addLabelOnClick() { const id = getNextId("label"); let group = labels.select("#addedLabels"); - if (!group.size()) group = labels.append("g").attr("id", "addedLabels") - .attr("fill", "#3e3e4b").attr("opacity", 1).attr("stroke", "#3a3a3a") - .attr("stroke-width", 0).attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC") - .attr("font-size", 18).attr("data-size", 18).attr("filter", null); + if (!group.size()) group = labels.append("g").attr("id", "addedLabels").attr("fill", "#3e3e4b").attr("opacity", 1).attr("stroke", "#3a3a3a").attr("stroke-width", 0).attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC").attr("font-size", 18).attr("data-size", 18).attr("filter", null); const example = group.append("text").attr("x", 0).attr("x", 0).text(name); const width = example.node().getBBox().width; @@ -439,13 +485,22 @@ function addLabelOnClick() { example.remove(); group.classed("hidden", false); - group.append("text").attr("id", id) - .append("textPath").attr("xlink:href", "#textPath_"+id).attr("startOffset", "50%").attr("font-size", "100%") - .append("tspan").attr("x", x).text(name); + group + .append("text") + .attr("id", id) + .append("textPath") + .attr("xlink:href", "#textPath_" + id) + .attr("startOffset", "50%") + .attr("font-size", "100%") + .append("tspan") + .attr("x", x) + .text(name); - defs.select("#textPaths") - .append("path").attr("id", "textPath_"+id) - .attr("d", `M${point[0]-width},${point[1]} h${width*2}`); + defs + .select("#textPaths") + .append("path") + .attr("id", "textPath_" + id) + .attr("d", `M${point[0] - width},${point[1]} h${width * 2}`); if (d3.event.shiftKey === false) unpressClickToAddButton(); } @@ -466,7 +521,7 @@ function toggleAddRiver() { } addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed")); - addRiver.classList.add('pressed'); + addRiver.classList.add("pressed"); document.getElementById("addNewRiver").classList.add("pressed"); closeDialogs(".stable"); viewbox.style("cursor", "crosshair").on("click", addRiverOnClick); @@ -486,22 +541,27 @@ function addRiverOnClick() { // height with added t value to make map less depressed const h = Array.from(cells.h) - .map((h, i) => h < 20 || cells.t[i] < 1 ? h : h + cells.t[i] / 100) - .map((h, i) => h < 20 || cells.t[i] < 1 ? h : h + d3.mean(cells.c[i].map(c => cells.t[c])) / 10000); - Rivers.resolveDepressions(h); + .map((h, i) => (h < 20 || cells.t[i] < 1 ? h : h + cells.t[i] / 100)) + .map((h, i) => (h < 20 || cells.t[i] < 1 ? h : h + d3.mean(cells.c[i].map(c => cells.t[c])) / 10000)); + Rivers.resolveDepressions(h, 200); while (i) { cells.r[i] = river; - const x = cells.p[i][0], y = cells.p[i][1]; - dataRiver.push({x, y, cell:i}); + const x = cells.p[i][0], + y = cells.p[i][1]; + dataRiver.push({x, y, cell: i}); const min = cells.c[i][d3.scan(cells.c[i], (a, b) => h[a] - h[b])]; // downhill cell - if (h[i] <= h[min]) {tip(`Cell ${i} is depressed, river cannot flow further`, false, "error"); return;} - const tx = cells.p[min][0], ty = cells.p[min][1]; + if (h[i] <= h[min]) { + tip(`Cell ${i} is depressed, river cannot flow further`, false, "error"); + return; + } + const tx = cells.p[min][0], + ty = cells.p[min][1]; if (h[min] < 20) { // pour to water body - dataRiver.push({x: tx, y: ty, cell:i}); + dataRiver.push({x: tx, y: ty, cell: i}); break; } @@ -526,19 +586,22 @@ function addRiverOnClick() { } // extend old river - rivers.select("#river"+r).remove(); - cells.i.filter(i => cells.r[i] === river).forEach(i => cells.r[i] = r); - riverCells.forEach(i => cells.r[i] = 0); + rivers.select("#river" + r).remove(); + cells.i.filter(i => cells.r[i] === river).forEach(i => (cells.r[i] = r)); + riverCells.forEach(i => (cells.r[i] = 0)); river = r; cells.fl[min] = cells.fl[i] + grid.cells.prec[cells.g[min]]; i = min; } - const points = Rivers.addMeandering(dataRiver, 1, .5); - const widthFactor = rn(.8 + Math.random() * .4, 1); // river width modifier [.8, 1.2] - const sourceWidth = .1; + const points = Rivers.addMeandering(dataRiver, 1, 0.5); + const widthFactor = rn(0.8 + Math.random() * 0.4, 1); // river width modifier [.8, 1.2] + const sourceWidth = 0.1; const [path, length, offset] = Rivers.getPath(points, widthFactor, sourceWidth); - rivers.append("path").attr("d", path).attr("id", "river"+river); + rivers + .append("path") + .attr("d", path) + .attr("id", "river" + river); // add new river to data or change extended river attributes const r = pack.rivers.find(r => r.i === river); @@ -555,10 +618,10 @@ function addRiverOnClick() { const source = dataRiver[0].cell; const width = rn(offset ** 2, 2); // mounth width in km const name = Rivers.getName(mouth); - const smallLength = pack.rivers.map(r => r.length||0).sort((a,b) => a-b)[Math.ceil(pack.rivers.length * .15)]; - const type = length < smallLength ? rw({"Creek":9, "River":3, "Brook":3, "Stream":1}) : "River"; + const smallLength = pack.rivers.map(r => r.length || 0).sort((a, b) => a - b)[Math.ceil(pack.rivers.length * 0.15)]; + const type = length < smallLength ? rw({Creek: 9, River: 3, Brook: 3, Stream: 1}) : "River"; - pack.rivers.push({i:river, source, mouth, discharge, length, width, widthFactor, sourceWidth, parent, basin, name, type}); + pack.rivers.push({i: river, source, mouth, discharge, length, width, widthFactor, sourceWidth, parent, basin, name, type}); } if (d3.event.shiftKey === false) { @@ -570,10 +633,13 @@ function addRiverOnClick() { function toggleAddRoute() { const pressed = document.getElementById("addRoute").classList.contains("pressed"); - if (pressed) {unpressClickToAddButton(); return;} + if (pressed) { + unpressClickToAddButton(); + return; + } addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed")); - addRoute.classList.add('pressed'); + addRoute.classList.add("pressed"); closeDialogs(".stable"); viewbox.style("cursor", "crosshair").on("click", addRouteOnClick); tip("Click on map to add a first control point", true); @@ -590,10 +656,13 @@ function addRouteOnClick() { function toggleAddMarker() { const pressed = document.getElementById("addMarker").classList.contains("pressed"); - if (pressed) {unpressClickToAddButton(); return;} + if (pressed) { + unpressClickToAddButton(); + return; + } addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed")); - addMarker.classList.add('pressed'); + addMarker.classList.add("pressed"); closeDialogs(".stable"); viewbox.style("cursor", "crosshair").on("click", addMarkerOnClick); tip("Click on map to add a marker. Hold Shift to add multiple", true); @@ -602,27 +671,44 @@ function toggleAddMarker() { function addMarkerOnClick() { const point = d3.mouse(this); - const x = rn(point[0], 2), y = rn(point[1], 2); + const x = rn(point[0], 2), + y = rn(point[1], 2); const id = getNextId("markerElement"); const selected = markerSelectGroup.value; - const valid = selected && d3.select("#defs-markers").select("#"+selected).size(); - const symbol = valid ? "#"+selected : "#marker0"; + const valid = + selected && + d3 + .select("#defs-markers") + .select("#" + selected) + .size(); + const symbol = valid ? "#" + selected : "#marker0"; const added = markers.select("[data-id='" + symbol + "']").size(); let desired = valid && added ? markers.select("[data-id='" + symbol + "']").attr("data-size") : 1; if (isNaN(desired)) desired = 1; const size = desired * 5 + 25 / scale; - markers.append("use").attr("id", id).attr("xlink:href", symbol).attr("data-id", symbol) - .attr("data-x", x).attr("data-y", y).attr("x", x - size / 2).attr("y", y - size) - .attr("data-size", desired).attr("width", size).attr("height", size); + markers + .append("use") + .attr("id", id) + .attr("xlink:href", symbol) + .attr("data-id", symbol) + .attr("data-x", x) + .attr("data-y", y) + .attr("x", x - size / 2) + .attr("y", y - size) + .attr("data-size", desired) + .attr("width", size) + .attr("height", size); if (d3.event.shiftKey === false) unpressClickToAddButton(); } function viewCellDetails() { $("#cellInfo").dialog({ - resizable: false, width: "22em", title: "Cell Details", + resizable: false, + width: "22em", + title: "Cell Details", position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"} }); }