diff --git a/index.css b/index.css index 1cd18ea7..5308deed 100644 --- a/index.css +++ b/index.css @@ -114,7 +114,7 @@ button, select, a, .pointer { fill-rule: evenodd; } -#oceanLayers { +#oceanLayers, #terrs { fill-rule: evenodd; } @@ -1028,10 +1028,10 @@ div#regimentSelectorBody > div > div { } .color-div { - width: 2.5em; + width: 3em; height: 1em; display: inline-block; - margin: .1em .2em; + margin: 0 .16em; border: 1px #c5c5c5 groove; cursor: pointer; } @@ -2101,6 +2101,16 @@ svg.button { text-shadow: 0px 1px 4px #4c3a35; } +.epgrid line { + stroke: lightgrey; + stroke-opacity: .7; + shape-rendering: crispEdges; +} + +.epgrid path { + stroke-width: 0; +} + #debug { font-size: 1px; opacity: .8; diff --git a/index.html b/index.html index 7c0e1c5f..9be8fea0 100644 --- a/index.html +++ b/index.html @@ -1883,8 +1883,8 @@ - - + +
@@ -2135,6 +2135,22 @@
@@ -2658,17 +2674,18 @@
- - + + + - +
-
Overlay opacity:
- - +
Overlay opacity:
+ +
@@ -2853,7 +2870,7 @@ - + diff --git a/main.js b/main.js index a2b40674..1e44a558 100644 --- a/main.js +++ b/main.js @@ -343,7 +343,8 @@ function showWelcomeMessage() { diff --git a/modules/military-generator.js b/modules/military-generator.js index 009b4b0d..f0821400 100644 --- a/modules/military-generator.js +++ b/modules/military-generator.js @@ -304,7 +304,7 @@ const station = base ? `${r.name} is ${r.n ? "based" : "stationed"} in ${base}. ` : ""; const composition = r.a ? Object.keys(r.u).map(t => `— ${t}: ${r.u[t]}`).join("\r\n") : null; - const troops = composition ? `\r\n\r\nRegiment composition:\r\n${composition}.` : ""; + const troops = composition ? `\r\n\r\nRegiment composition in ${options.year} ${options.eraShort}:\r\n${composition}.` : ""; const campaign = s.campaigns ? ra(s.campaigns) : null; const year = campaign ? rand(campaign.start, campaign.end) : gauss(options.year-100, 150, 1, options.year-6); diff --git a/modules/save-and-load.js b/modules/save-and-load.js index 607e1d7e..77da271f 100644 --- a/modules/save-and-load.js +++ b/modules/save-and-load.js @@ -351,7 +351,8 @@ function saveGeoJSON_Cells() { data += " \"state\": \""+cells.state[i]+"\",\n"; data += " \"province\": \""+cells.province[i]+"\",\n"; data += " \"culture\": \""+cells.culture[i]+"\",\n"; - data += " \"religion\": \""+cells.religion[i]+"\"\n"; + data += " \"religion\": \""+cells.religion[i]+"\",\n"; + data += " \"neighbors\": ["+cells.c[i]+"]\n"; data +=" }\n},\n"; }); diff --git a/modules/ui/battle-screen.js b/modules/ui/battle-screen.js index de9882f2..70375f3e 100644 --- a/modules/ui/battle-screen.js +++ b/modules/ui/battle-screen.js @@ -11,8 +11,8 @@ class Battle { this.y = defender.y; this.name = this.getBattleName(); this.iteration = 0; - this.attackers = {regiments:[], distances:[], morale:100}; - this.defenders = {regiments:[], distances:[], morale:100}; + this.attackers = {regiments:[], distances:[], morale:100, casualties:0}; + this.defenders = {regiments:[], distances:[], morale:100, casualties:0}; this.addHeaders(); this.addRegiment("attackers", attacker); @@ -260,6 +260,8 @@ class Battle { this.calculateCasualties("attackers", casualtiesA); this.calculateCasualties("defenders", casualtiesD); + this.attackers.casualties += casualtiesA; + this.defenders.casualties += casualtiesD; // change morale this.attackers.morale = Math.max(this.attackers.morale - casualtiesA * 100, 0); @@ -326,12 +328,79 @@ class Battle { } applyResults() { - this.attackers.regiments.concat(this.defenders.regiments).forEach(r => { + const battleName = this.name; + const maxCasualties = Math.max(this.attackers.casualties, this.attackers.casualties); + const relativeCasualties = this.defenders.casualties / (this.attackers.casualties + this.attackers.casualties); + const battleStatus = getBattleStatus(relativeCasualties, maxCasualties); + function getBattleStatus(relative, max) { + if (isNaN(relative)) return ["standoff", "standoff"]; // if no casualties at all + if (max < .05) return ["minor skirmishes", "minor skirmishes"]; + if (relative > 95) return ["attackers flawless victory", "disorderly retreat of defenders"]; + if (relative > .7) return ["attackers decisive victory", "defenders disastrous defeat"]; + if (relative > .6) return ["attackers victory", "defenders defeat"]; + if (relative > .4) return ["stalemate", "stalemate"]; + if (relative > .3) return ["attackers defeat", "defenders victory"]; + if (relative > 0.5) return ["attackers disastrous defeat", "decisive victory of defenders"]; + if (relative >= 0) return ["attackers disorderly retreat", "flawless victory of defenders"]; + return ["stalemate", "stalemate"]; // exception + } + + this.attackers.regiments.forEach(r => applyResultForSide(r, "attackers")); + this.defenders.regiments.forEach(r => applyResultForSide(r, "defenders")); + + function applyResultForSide(r, side) { + const id = "regiment" + r.state + "-" + r.i; + + // add result to regiment note + const note = notes.find(n => n.id === id); + if (note) { + const status = side === "attackers" ? battleStatus[0] : battleStatus[1]; + const losses = r.a ? Math.abs(d3.sum(Object.values(r.casualties))) / r.a : 1; + const regStatus = + losses === 1 ? "is destroyed" : + losses > .8 ? "is almost completely destroyed" : + losses > .5 ? "suffered terrible losses" : + losses > .3 ? "suffered severe losses" : + losses > .2 ? "suffered heavy losses" : + losses > .05 ? "suffered significant losses" : + losses > 0 ? "suffered unsignificant losses" : + "left the battle without loss"; + const casualties = Object.keys(r.casualties).map(t => r.casualties[t] ? `${Math.abs(r.casualties[t])} ${t}` : null).filter(c => c).join(", "); + const casualtiesText = casualties ? " Casualties: " + casualties : ""; + const legend = `\r\n\r\n${battleName} (${options.year} ${options.eraShort}): ${status}. The regiment ${regStatus}.${casualtiesText}`; + note.legend += legend; + } + r.u = Object.assign({}, r.survivors); r.a = d3.sum(Object.values(r.u)); // reg total - armies.select(`g#regiment${r.state}-${r.i} > text`).text(Military.getTotal(r)); // update reg box - Military.moveRegiment(r, r.x + rand(30) - 15, r.y + rand(30) - 15); - }); + armies.select(`g#${id} > text`).text(Military.getTotal(r)); // update reg box + Military.moveRegiment(r, r.x + rand(20) - 10, r.y + rand(20) - 10); + } + + // append battlefield marker + void function addMarkerSymbol() { + if (svg.select("#defs-markers").select("#marker_battlefield").size()) return; + const symbol = svg.select("#defs-markers").append("symbol").attr("id", "marker_battlefield").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", "50%").attr("y", "52%").attr("fill", "#000000").attr("stroke", "#3200ff").attr("stroke-width", 0) + .attr("font-size", "12px").attr("dominant-baseline", "central").text("⚔️"); + }() + + const getSide = (regs, n) => regs.length > 1 ? + `${n ? "regiments" : "forces"} of ${[... new Set(regs.map(r => pack.states[r.state].name))].join(", ")}` : + getAdjective(pack.states[regs[0].state].name) + " " + regs[0].name; + const getLosses = casualties => Math.min(rn(casualties * 100), 100); + + const legend = `${this.name} took place in ${options.year} ${options.eraShort}. It was fought between ${getSide(this.attackers.regiments, 1)} and ${getSide(this.defenders.regiments, 0)}. The battle ended in ${battleStatus[+P(.7)]}. + \r\nAttackers losses: ${getLosses(this.attackers.casualties)}%, defenders losses: ${getLosses(this.defenders.casualties)}%`; + const id = getNextId("markerElement"); + notes.push({id, name:this.name, legend}); + + markers.append("use").attr("id", id) + .attr("xlink:href", "#marker_battlefield").attr("data-id", "#marker_battlefield") + .attr("data-x", this.x).attr("data-y", this.y).attr("x", this.x - 15).attr("y", this.y - 30) + .attr("data-size", 1).attr("width", 30).attr("height", 30); $("#battleScreen").dialog("destroy"); this.cleanData(); diff --git a/modules/ui/burg-editor.js b/modules/ui/burg-editor.js index f58dee11..da68ad20 100644 --- a/modules/ui/burg-editor.js +++ b/modules/ui/burg-editor.js @@ -318,7 +318,17 @@ function editBurg(id) { if (deg < 0) deg += 360; let norm = rn(normalize(deg, 0, 360) * 8) / 4; if (norm === 2) norm = 0; - return "sea="+norm; + switch(norm) { + case 0 : return "&southSea=1"; + case 0.25 : return "&southSea=1&westSea=1"; + case 0.50 : return "&westSea=1"; + case 0.75 : return "&westSea=1&northSea=1"; + case 1 : return "&northSea=1"; + case 1.25 : return "&northSea=1&eastSea=1"; + case 1.5 : return "&eastSea=1"; + case 1.75 : return "&eastSea=1&southSea=1"; + } + return "&sea="+norm; // debug.selectAll("*").remove(); // pack.burgs.filter(b => b.port).forEach(b => { // var p1 = pack.cells.p[b.cell]; @@ -450,4 +460,4 @@ function editBurg(id) { unselect(); } -} \ No newline at end of file +} diff --git a/modules/ui/elevation-profile.js b/modules/ui/elevation-profile.js index 073bf25a..f816ce07 100644 --- a/modules/ui/elevation-profile.js +++ b/modules/ui/elevation-profile.js @@ -1,4 +1,5 @@ "use strict"; + function showEPForRoute(node) { const points = []; debug.select("#controlPoints").selectAll("circle").each(function() { @@ -21,19 +22,20 @@ function showEPForRiver(node) { showElevationProfile(points, riverLen, true); } -function resizeElevationProfile() { -} - function showElevationProfile(data, routeLen, isRiver) { - // data is an array of cell indexes, routeLen is the distance, isRiver should be true for rivers, false otherwise - document.getElementById("elevationGraph").innerHTML = ""; + // data is an array of cell indexes, routeLen is the distance (in actual metres/feet), isRiver should be true for rivers, false otherwise + + document.getElementById("epScaleRange").addEventListener("change", draw); + document.getElementById("epCurve").addEventListener("change", draw); + document.getElementById("epSave").addEventListener("click", downloadCSV); $("#elevationProfile").dialog({ title: "Elevation profile", resizable: false, width: window.width, - position: {my: "left top", at: "left+20 bottom-240", of: window, collision: "fit"} + close: closeElevationProfile, + position: {my: "left top", at: "left+20 bottom-500", of: window, collision: "fit"} }); - // prevent river graphs from showing rivers as flowing uphill + // prevent river graphs from showing rivers as flowing uphill - remember the general slope var slope = 0; if (isRiver) { if (pack.cells.h[data[0]] < pack.cells.h[data[data.length-1]]) { @@ -43,12 +45,22 @@ function showElevationProfile(data, routeLen, isRiver) { } } - const points = []; - var prevB=0, prevH=-1, i=0, j=0, cell=0, b=0, ma=0, mi=100, h=0; - for (var i=0; i d.x * w / points.length + xOffset).y(d => h-d.y + yOffset); - chart.append("path").attr("d", lineFunc(points)).attr("stroke", "purple").attr("fill", "none").attr("id", "elevationLine"); - - // y-axis labels for starting and ending heights - chart.append("text").attr("id", "epy0").attr("x", xOffset-10).attr("y", h-points[0].y + yOffset).attr("text-anchor", "end"); - document.getElementById("epy0").innerHTML = getHeight(points[0].y); - chart.append("text").attr("id", "epy1").attr("x", w+100).attr("y", h-points[points.length-1].y + yOffset).attr("text-anchor", "start"); - document.getElementById("epy1").innerHTML = getHeight(points[points.length-1].y); - - // y-axis labels for minimum and maximum heights (if not too close to start/end heights) - if (Math.abs(ma - points[0].y) > 3 && Math.abs(ma - points[points.length-1].y) > 3) { - chart.append("text").attr("id", "epy2").attr("x", xOffset-10).attr("y", h-ma + yOffset).attr("text-anchor", "end"); - document.getElementById("epy2").innerHTML = getHeight(ma); + if (lastBurgIndex != 0 && lastBurgCell == chartData.cell[data.length-1] && lastBurgIndex < data.length) { + chartData.burg[data.length-1] = chartData.burg[lastBurgIndex]; + chartData.burg[lastBurgIndex] = 0; } - if (Math.abs(mi - points[0].y) > 3 && Math.abs(mi - points[points.length-1].y) > 3) { - chart.append("text").attr("id", "epy3").attr("x", xOffset-10).attr("y", h-mi + yOffset).attr("text-anchor", "end"); - document.getElementById("epy3").innerHTML = getHeight(mi); - } - - // x-axis label for start, quarter, halfway and three-quarter, and end - chart.append("text").attr("id", "epx1").attr("x", xOffset).attr("y", h+yOffset).attr("text-anchor", "middle"); - chart.append("text").attr("id", "epx2").attr("x", w / 4 + xOffset).attr("y", h+yOffset).attr("text-anchor", "middle"); - chart.append("text").attr("id", "epx3").attr("x", w / 2 + xOffset).attr("y", h+yOffset).attr("text-anchor", "middle"); - chart.append("text").attr("id", "epx4").attr("x", w / 4*3 + xOffset).attr("y", h+yOffset).attr("text-anchor", "middle"); - chart.append("text").attr("id", "epx5").attr("x", w + xOffset).attr("y", h+yOffset).attr("text-anchor", "middle"); - document.getElementById("epx1").innerHTML = "0 " + distanceUnitInput.value; - document.getElementById("epx2").innerHTML = rn(routeLen / 4) + " " + distanceUnitInput.value; - document.getElementById("epx3").innerHTML = rn(routeLen / 2) + " " + distanceUnitInput.value; - document.getElementById("epx4").innerHTML = rn(routeLen / 4*3) + " " + distanceUnitInput.value; - document.getElementById("epx5").innerHTML = rn(routeLen) + " " + distanceUnitInput.value; - chart.append("path").attr("id", "epx11").attr("d", "M" + (xOffset).toString() + ",0L" + (xOffset).toString() +"," + (h+yOffset-15).toString()).attr("stroke", "lightgray").attr("stroke-width", "1"); - chart.append("path").attr("id", "epx12").attr("d", "M" + (w / 4 + xOffset).toString() + "," + (h+yOffset-15).toString() + "L" + (w / 4 + xOffset).toString() + ",0").attr("stroke", "lightgray").attr("stroke-width", "1"); - chart.append("path").attr("id", "epx13").attr("d", "M" + (w / 2 + xOffset).toString() + "," + (h+yOffset-15).toString() + "L" + (w / 2 + xOffset).toString() + ",0").attr("stroke", "lightgray").attr("stroke-width", "1"); - chart.append("path").attr("id", "epx14").attr("d", "M" + (w / 4*3 + xOffset).toString() + "," + (h+yOffset-15).toString() + "L" + (w / 4*3 + xOffset).toString() + ",0").attr("stroke", "lightgray").attr("stroke-width", "1"); - chart.append("path").attr("id", "epx15").attr("d", "M" + (w + xOffset).toString() + ",0L" + (w + xOffset).toString() +"," + (h+yOffset-15).toString()).attr("stroke", "lightgray").attr("stroke-width", "1"); + draw(); - // draw city labels - try to avoid putting labels over one another - var y1 = 0; - var add = 15; - points.forEach(function(p) { - if (p.b > 0) { - var x1 = p.x * w / points.length + xOffset; - y1+=add; - if (y1 >= yOffset) { y1 = add; } - var d1 = 0; + function downloadCSV() { + let data = "Point,X,Y,Cell,Height,Height value,Population,Burg,Burg population,Biome,Biome color,Culture,Culture color,Religion,Religion color,Province,Province color,State,State color\n"; // headers - // burg name - chart.append("text").attr("id", "ep" + p.b).attr("x", x1).attr("y", y1).attr("text-anchor", "middle"); - document.getElementById("ep" + p.b).innerHTML = pack.burgs[p.b].name; + for (let k=0; k= chartData.mih; k--) { + let perc = 1 - (k - chartData.mih) / (chartData.mah - chartData.mih); + landdef.append("stop").attr("offset", perc*100 + "%").attr("style", "stop-color:" + getColor(k, colors) + ";stop-opacity:1"); + } + } + + // land + let curve = d3.line().curve(d3.curveBasis); // see https://github.com/d3/d3-shape#curves + let epCurveIndex = parseInt(epCurve.selectedIndex); + switch(epCurveIndex) { + case 0 : curve = d3.line().curve(d3.curveLinear); break; + case 1 : curve = d3.line().curve(d3.curveBasis); break; + case 2 : curve = d3.line().curve(d3.curveBundle.beta(1)); break; + case 3 : curve = d3.line().curve(d3.curveCatmullRom.alpha(0.5)); break; + case 4 : curve = d3.line().curve(d3.curveMonotoneX); break; + case 5 : curve = d3.line().curve(d3.curveNatural); break; + } + + // copy the points so that we can add extra straight pieces, else we get curves at the ends of the chart + let extra = chartData.points.slice(); + var path = curve(extra); + // this completes the right-hand side and bottom of our land "polygon" + path += " L" + parseInt(xscale(extra.length) + +xOffset) + "," + parseInt(extra[extra.length-1][1]); + path += " L" + parseInt(xscale(extra.length) + +xOffset) + "," + parseInt(yscale(0) + +yOffset); + path += " L" + parseInt(xscale(0) + +xOffset) +"," + parseInt(yscale(0) + +yOffset); + path += "Z"; + chart.append("g").attr("id", "epland").append("path").attr("d", path).attr("stroke", "purple").attr("stroke-width", "0").attr("fill", "url(#landdef)"); + + // biome / heights + let g = chart.append("g").attr("id", "epbiomes"); + const hu = heightUnit.value; + for(var k=0; k 0) { + let b = chartData.burg[k]; + + let x1 = chartData.points[k][0]; // left side of graph by default + if (k > 0) x1 += xwidth/2; // center it if not first + if (k == chartData.points.length-1) + x1 = chartWidth + xOffset; // right part of graph + y1+=add; + if (y1 >= yOffset) { y1 = add; } + var d1 = 0; + + // burg name + g.append("text").attr("id", "ep" + b).attr("class", "epburglabel").attr("x", x1).attr("y", y1).attr("text-anchor", "middle"); + document.getElementById("ep" + b).innerHTML = pack.burgs[b].name; + + // arrow from burg name to graph line + g.append("path").attr("id", "eparrow" + b).attr("d", "M" + x1.toString() + "," + (y1+3).toString() + "L" + x1.toString() + "," + parseInt(chartData.points[k][1]-3).toString()).attr("stroke", "darkgray").attr("fill", "lightgray").attr("stroke-width", "1").attr("marker-end", "url(#arrowhead)"); + } + } + } + + function closeElevationProfile() { + document.getElementById("epScaleRange").removeEventListener("change", draw); + document.getElementById("epCurve").removeEventListener("change", draw); + document.getElementById("epSave").removeEventListener("click", downloadCSV); + + document.getElementById("elevationGraph").innerHTML = ""; + + modules.elevation = false; + } } diff --git a/modules/ui/heightmap-editor.js b/modules/ui/heightmap-editor.js index 87278555..f574f84e 100644 --- a/modules/ui/heightmap-editor.js +++ b/modules/ui/heightmap-editor.js @@ -3,13 +3,13 @@ function editHeightmap() { 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. -

You can also keep all the data, but you won't be able to change the coastline.

-

If you need to change the coastline and keep the data, you may try the risk edit option. - The data will be restored as much as possible, but the coastline change can cause unexpected fluctuations and errors.

-

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

-

Please save the map before editing the heightmap!

`; + 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.

+

You can keep the data, but you won't be able to change the coastline.

+

Try risk mode to change the coastline and keep the data. The data will be restored as much as possible, but it can cause unpredictable errors.

+

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", buttons: { @@ -61,9 +61,9 @@ function editHeightmap() { changeOnlyLand.checked = false; } - // hide convert and template buttons for the Keep mode - applyTemplate.style.display = type === "keep" ? "none" : "inline-block"; - convertImage.style.display = type === "keep" ? "none" : "inline-block"; + // show convert and template buttons for Erase mode only + applyTemplate.style.display = type === "erase" ? "inline-block" : "none"; + convertImage.style.display = type === "erase" ? "inline-block" : "none"; // hide erosion checkbox if mode is Keep changeHeightsBox.style.display = type === "keep" ? "none" : "inline-block"; @@ -963,10 +963,11 @@ function editHeightmap() { function openImageConverter() { if ($("#imageConverter").is(":visible")) return; + imageToLoad.click(); closeDialogs("#imageConverter"); $("#imageConverter").dialog({ - title: "Image Converter", maxHeight: svgHeight*.75, minHeight: "auto", width: "19.5em", resizable: false, + title: "Image Converter", maxHeight: svgHeight*.8, minHeight: "auto", width: "20em", position: {my: "right top", at: "right-10 top+10", of: "svg"}, beforeClose: closeImageConverter }); @@ -978,15 +979,9 @@ function editHeightmap() { canvas.height = graphHeight; document.body.insertBefore(canvas, optionsContainer); - const img = new Image; - img.id = "image"; - img.style.display = "none"; - document.body.appendChild(img); - setOverlayOpacity(0); - - document.getElementById("convertImageLoad").classList.add("glow"); // add glow effect - tip('Image Converter is opened. Upload the image and assign height value for each of the colors', true, "warn"); // main tip + clearMainTip(); + 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); @@ -1001,7 +996,7 @@ function editHeightmap() { 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 < 20 || i > 70 ? ".2em" : ".1em") + .style("width", i => i < 40 || i > 68 ? ".2em" : ".1em") .on("touchmove mousemove", showPalleteHeight).on("click", assignHeight); }() @@ -1010,6 +1005,7 @@ function editHeightmap() { document.getElementById("imageToLoad").addEventListener("change", loadImage); document.getElementById("convertAutoLum").addEventListener("click", () => autoAssing("lum")); document.getElementById("convertAutoHue").addEventListener("click", () => autoAssing("hue")); + document.getElementById("convertAutoFMG").addEventListener("click", () => autoAssing("scheme")); document.getElementById("convertColorsButton").addEventListener("click", setConvertColorsNumber); document.getElementById("convertComplete").addEventListener("click", applyConversion); document.getElementById("convertCancel").addEventListener("click", cancelConversion); @@ -1030,12 +1026,12 @@ 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 ctx = document.getElementById("canvas").getContext("2d"); ctx.drawImage(img, 0, 0, graphWidth, graphHeight); heightsFromImage(+convertColors.value); resetZoom(); - convertImageLoad.classList.remove("glow"); }; reader.onloadend = () => img.src = reader.result; @@ -1043,37 +1039,35 @@ function editHeightmap() { } function heightsFromImage(count) { - const ctx = document.getElementById("canvas").getContext("2d"); + const sourceImage = document.getElementById("canvas"); + const sampleCanvas = document.createElement("canvas"); + sampleCanvas.width = grid.cellsX; + sampleCanvas.height = grid.cellsY; + sampleCanvas.getContext('2d').drawImage(sourceImage, 0, 0, grid.cellsX, grid.cellsY); + const q = new RgbQuant({colors:count}); - q.sample(ctx); - const data = q.reduce(ctx); + q.sample(sampleCanvas); + const data = q.reduce(sampleCanvas); + const pallete = q.palette(true); viewbox.select("#heights").selectAll("*").remove(); d3.select("#imageConverter").selectAll("div.color-div").remove(); colorsSelect.style.display = "block"; colorsUnassigned.style.display = "block"; colorsAssigned.style.display = "none"; - - let usedColors = new Set(); - let gridColors = grid.points.map(p => { - const x = Math.floor(p[0]-.01), y = Math.floor(p[1]-.01); - const i = (x + y * graphWidth) * 4; - const r = data[i], g = data[i+1], b = data[i+2]; - usedColors.add(`rgb(${r},${g},${b})`); - return [r, g, b]; - }); + 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(${gridColors[d].join(",")})`) + .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 unassigned = [...usedColors].sort((a, b) => d3.lab(a).l - d3.lab(b).l); - d3.select("#colorsUnassigned").selectAll("div").data(unassigned).enter().append("div") + 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); - convertColors.value = unassigned.length; + document.getElementById("colorsUnassignedNumber").innerHTML = colors.length; } function mapClicked() { @@ -1123,34 +1117,60 @@ function editHeightmap() { if (selectedColor.parentNode.id === "colorsUnassigned") { colorsAssigned.appendChild(selectedColor); colorsAssigned.style.display = "block"; + + document.getElementById("colorsUnassignedNumber").innerHTML = colorsUnassigned.childElementCount - 2; + document.getElementById("colorsAssignedNumber").innerHTML = colorsAssigned.childElementCount - 2; } } // auto assign color based on luminosity or hue function autoAssing(type) { - const unassigned = colorsUnassigned.querySelectorAll("div"); - if (!unassigned.length) {tip("No unassigned colors. Please load an image and click the button again", false, "error"); return;} + let unassigned = colorsUnassigned.querySelectorAll("div"); + if (!unassigned.length) { + 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"); + return; + } + } - const assinged = []; // assigned heights + 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 + } + + const getHeightByLum = function(color) { + let lum = d3.lab(color).l; + 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) { + 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 colorFrom = el.dataset.color; - const lab = d3.lab(colorFrom); - const normalized = type === "hue" ? rn(normalize(lab.b + lab.a / 2, -50, 200), 2) : rn(normalize(lab.l, -15, 100), 2); - let heightTo = rn(normalized * 100); - if (assinged[heightTo] && heightTo < 100) heightTo += 1; // if height is already added, try increased one - if (assinged[heightTo] && heightTo < 100) heightTo += 1; // if height is already added, try increased one - if (assinged[heightTo] && heightTo > 3) heightTo -= 3; // if increased one is also added, try decreased one - if (assinged[heightTo] && heightTo > 1) heightTo -= 1; // if increased one is also added, try decreased one + 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 - (heightTo < 20 ? (heightTo-5)/100 : heightTo/100)); - viewbox.select("#heights").selectAll("polygon[fill='" + colorFrom + "']").attr("fill", colorTo).attr("data-height", heightTo); - - if (assinged[heightTo]) {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 = heightTo; + el.dataset.height = height; colorsAssigned.appendChild(el); - assinged[heightTo] = true; + assinged[height] = true; }); // sort assigned colors by height @@ -1160,10 +1180,11 @@ function editHeightmap() { colorsAssigned.style.display = "block"; colorsUnassigned.style.display = "none"; + document.getElementById("colorsAssignedNumber").innerHTML = colorsAssigned.childElementCount - 2; } function setConvertColorsNumber() { - prompt(`Please set maximum number of colors.
An actual number is lower and depends on color scheme`, + 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); @@ -1176,6 +1197,11 @@ function editHeightmap() { } function applyConversion() { + if (colorsAssigned.childElementCount < 3) { + tip("Please do the assignment first", false, "error"); + return; + } + viewbox.select("#heights").selectAll("polygon").each(function() { const height = +this.dataset.height || 0; const i = +this.id.slice(4); @@ -1195,9 +1221,7 @@ function editHeightmap() { function restoreImageConverterState() { const canvas = document.getElementById("canvas"); - if (canvas) canvas.remove(); else return; - const img = document.getElementById("image"); - if (img) img.remove(); else return; + if (canvas) canvas.remove(); d3.select("#imageConverter").selectAll("div.color-div").remove(); colorsAssigned.style.display = "none"; @@ -1206,12 +1230,18 @@ function editHeightmap() { viewbox.style("cursor", "default").on(".drag", null); tip('Heightmap edit mode is active. Click on "Exit Customization" to finalize the heightmap', true); $("#imageConverter").dialog("destroy"); + openBrushesPanel(); } function closeImageConverter(event) { event.preventDefault(); event.stopPropagation(); - alertMessage.innerHTML = 'Are you sure you want to close the Image Converter? Click "Cancel" to geck back to convertion. Click "Complete" to apply the conversion. Click "Close" to exit conversion mode and restore previous heightmap'; + alertMessage.innerHTML = ` + Are you sure you want to close the Image Converter? + Click "Cancel" to geck back to convertion. + Click "Complete" to apply the conversion. + Click "Close" to exit conversion mode and restore previous heightmap`; + $("#alert").dialog({resizable: false, title: "Close Image Converter", buttons: { Cancel: function() { diff --git a/modules/ui/rivers-editor.js b/modules/ui/rivers-editor.js index 85551976..cf25f04f 100644 --- a/modules/ui/rivers-editor.js +++ b/modules/ui/rivers-editor.js @@ -55,7 +55,7 @@ function editRiver(id) { function drawControlPoints(node) { const l = node.getTotalLength() / 2; - const segments = Math.ceil(l / 8); + const segments = Math.ceil(l / 4); const increment = rn(l / segments * 1e5); for (let i=increment*segments, c=i; i >= 0; i -= increment, c += increment) { const p1 = node.getPointAtLength(i / 1e5); @@ -183,7 +183,7 @@ function editRiver(id) { function showElevationProfile() { modules.elevation = true; - showEPForRiver(node); + showEPForRiver(elSelected.node()); } function showRiverWidth() { diff --git a/modules/ui/routes-editor.js b/modules/ui/routes-editor.js index 98974f29..78c84f8d 100644 --- a/modules/ui/routes-editor.js +++ b/modules/ui/routes-editor.js @@ -47,7 +47,7 @@ function editRoute(onClick) { function drawControlPoints(node) { const l = node.getTotalLength(); - const increment = l / Math.ceil(l / 8); + const increment = l / Math.ceil(l / 4); for (let i=0; i <= l; i += increment) {addControlPoint(node.getPointAtLength(i));} routeLength.innerHTML = rn(l * distanceScaleInput.value) + " " + distanceUnitInput.value; } @@ -101,16 +101,14 @@ function editRoute(onClick) { elSelected.attr("d", round(lineGen(points))); const l = elSelected.node().getTotalLength(); - routeLength.innerHTML = rn(l * distanceScaleInput.value) + " " + distanceUnitInput.value; - - if (modules.elevation) { - showEPForRoute(elSelected.node()); - } + routeLength.innerHTML = rn(l * distanceScaleInput.value) + " " + distanceUnitInput.value; + + if (modules.elevation) showEPForRoute(elSelected.node()); } function showElevationProfile() { modules.elevation = true; - showEPForRoute(node); + showEPForRoute(elSelected.node()); } function showGroupSection() { diff --git a/modules/ui/states-editor.js b/modules/ui/states-editor.js index 2f1d5800..de0da9e9 100644 --- a/modules/ui/states-editor.js +++ b/modules/ui/states-editor.js @@ -215,8 +215,7 @@ function editStates() { } function editStateName(state) { - - //Reset input value and close add mode + // reset input value and close add mode stateNameEditorCustomForm.value = ""; const addModeActive = stateNameEditorCustomForm.style.display === "inline-block"; if (addModeActive) { @@ -244,6 +243,7 @@ function editStates() { document.getElementById("stateNameEditorShortCulture").addEventListener("click", regenerateShortNameCuture); document.getElementById("stateNameEditorShortRandom").addEventListener("click", regenerateShortNameRandom); document.getElementById("stateNameEditorAddForm").addEventListener("click", addCustomForm); + document.getElementById("stateNameEditorCustomForm").addEventListener("change", addCustomForm); document.getElementById("stateNameEditorFullRegenerate").addEventListener("click", regenerateFullName); function regenerateShortNameCuture() { @@ -264,7 +264,7 @@ function editStates() { const addModeActive = stateNameEditorCustomForm.style.display === "inline-block"; stateNameEditorCustomForm.style.display = addModeActive ? "none" : "inline-block"; stateNameEditorSelectForm.style.display = addModeActive ? "inline-block" : "none"; - if (addModeActive) applyOption(stateNameEditorSelectForm, value); + if (value && addModeActive) applyOption(stateNameEditorSelectForm, value); stateNameEditorCustomForm.value = ""; } diff --git a/modules/ui/tools.js b/modules/ui/tools.js index 20b64977..357163b3 100644 --- a/modules/ui/tools.js +++ b/modules/ui/tools.js @@ -158,8 +158,8 @@ function regenerateStates() { tip(`Not enought burgs to generate ${regionsInput.value} states. Will generate only ${burgs.length} states`, false, "warn"); } - // burg ids sorted by a bit randomized population: - const sorted = burgs.map(b => [b.i, b.population * Math.random()]).sort((a, b) => b[1] - a[1]).map(b => b[0]); + // 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 capitalsTree = d3.quadtree(); // turn all old capitals into towns @@ -194,8 +194,8 @@ function regenerateStates() { if (!i) return {i, name: neutral}; let capital = null, x = 0, y = 0; - for (let i=0; i < sorted.length; i++) { - capital = burgs[sorted[i]]; + for (const i of sorted) { + capital = burgs[i]; x = capital.x, y = capital.y; if (capitalsTree.find(x, y, spacing) === undefined) break; spacing = Math.max(spacing - 1, 1);