diff --git a/charges/agnusDei.svg b/charges/agnusDei.svg
new file mode 100644
index 00000000..52f55f24
--- /dev/null
+++ b/charges/agnusDei.svg
@@ -0,0 +1,135 @@
+
\ No newline at end of file
diff --git a/charges/anvil.svg b/charges/anvil.svg
new file mode 100644
index 00000000..2b6b0868
--- /dev/null
+++ b/charges/anvil.svg
@@ -0,0 +1,13 @@
+
\ No newline at end of file
diff --git a/charges/apple.svg b/charges/apple.svg
new file mode 100644
index 00000000..867113e5
--- /dev/null
+++ b/charges/apple.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/charges/basilisk.svg b/charges/basilisk.svg
new file mode 100644
index 00000000..1e680e29
--- /dev/null
+++ b/charges/basilisk.svg
@@ -0,0 +1,220 @@
+
diff --git a/charges/cannon.svg b/charges/cannon.svg
new file mode 100644
index 00000000..a6a36cee
--- /dev/null
+++ b/charges/cannon.svg
@@ -0,0 +1,68 @@
+
\ No newline at end of file
diff --git a/charges/chain.svg b/charges/chain.svg
new file mode 100644
index 00000000..4fea5d56
--- /dev/null
+++ b/charges/chain.svg
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/charges/crown2.svg b/charges/crown2.svg
new file mode 100644
index 00000000..81f00543
--- /dev/null
+++ b/charges/crown2.svg
@@ -0,0 +1,73 @@
+
\ No newline at end of file
diff --git a/charges/falchion.svg b/charges/falchion.svg
new file mode 100644
index 00000000..6bb3b0ec
--- /dev/null
+++ b/charges/falchion.svg
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/charges/foot.svg b/charges/foot.svg
new file mode 100644
index 00000000..c9988d9b
--- /dev/null
+++ b/charges/foot.svg
@@ -0,0 +1,14 @@
+
\ No newline at end of file
diff --git a/charges/greyhoundSejant.svg b/charges/greyhoundSejant.svg
new file mode 100644
index 00000000..b26cce80
--- /dev/null
+++ b/charges/greyhoundSejant.svg
@@ -0,0 +1,108 @@
+
diff --git a/charges/horsePassant.svg b/charges/horsePassant.svg
new file mode 100644
index 00000000..f235ce7e
--- /dev/null
+++ b/charges/horsePassant.svg
@@ -0,0 +1,119 @@
+
\ No newline at end of file
diff --git a/charges/lambPassantReguardant.svg b/charges/lambPassantReguardant.svg
new file mode 100644
index 00000000..4d90b210
--- /dev/null
+++ b/charges/lambPassantReguardant.svg
@@ -0,0 +1,119 @@
+
\ No newline at end of file
diff --git a/charges/mastiffStatant.svg b/charges/mastiffStatant.svg
new file mode 100644
index 00000000..83947142
--- /dev/null
+++ b/charges/mastiffStatant.svg
@@ -0,0 +1,157 @@
+
\ No newline at end of file
diff --git a/charges/plough.svg b/charges/plough.svg
new file mode 100644
index 00000000..831ddead
--- /dev/null
+++ b/charges/plough.svg
@@ -0,0 +1,16 @@
+
\ No newline at end of file
diff --git a/charges/porcupine.svg b/charges/porcupine.svg
new file mode 100644
index 00000000..1462eee1
--- /dev/null
+++ b/charges/porcupine.svg
@@ -0,0 +1,137 @@
+
\ No newline at end of file
diff --git a/charges/sabre2.svg b/charges/sabre2.svg
new file mode 100644
index 00000000..e16c45fe
--- /dev/null
+++ b/charges/sabre2.svg
@@ -0,0 +1,15 @@
+
\ No newline at end of file
diff --git a/charges/snake.svg b/charges/snake.svg
new file mode 100644
index 00000000..49f84fd1
--- /dev/null
+++ b/charges/snake.svg
@@ -0,0 +1,73 @@
+
\ No newline at end of file
diff --git a/images/preview.png b/images/preview.png
index 7ae64245..2b150732 100644
Binary files a/images/preview.png and b/images/preview.png differ
diff --git a/index.css b/index.css
index a5768367..9cdcbbe6 100644
--- a/index.css
+++ b/index.css
@@ -36,6 +36,12 @@ textarea {
width: 100%;
}
+iframe {
+ border: 0;
+ pointer-events: none;
+ width: 100%;
+}
+
#map {
background-color: #000000;
mask-mode: alpha;
@@ -222,18 +228,12 @@ t,
cursor: pointer;
}
-#options .pressed {
- background-color: #896c77 !important;
- font-style: italic;
-}
-
i.icon-lock {
cursor: pointer;
}
#routeEditor > *,
-#labelEditor div,
-#markerEditor div {
+#labelEditor div {
display: inline-block;
}
@@ -323,10 +323,21 @@ text.drag {
text-shadow: 0 0 1px red;
}
+#dialogs {
+ background-color: var(--bg-dialogs);
+}
+
.draggable {
cursor: move;
}
+.ui-widget-header {
+ border-bottom: 1px solid var(--dark-solid);
+ background: var(--header);
+ color: #ffffff;
+ font-weight: bold;
+}
+
.ui-dialog,
#optionsContainer {
user-select: none;
@@ -338,6 +349,7 @@ text.drag {
border: solid 1px #5e4fa2;
margin: 10px;
padding-bottom: 0.3em;
+ background: var(--bg-light);
}
#options input,
@@ -356,7 +368,7 @@ text.drag {
}
.tab {
- border-bottom: 1px solid #5d4651;
+ border-bottom: 1px solid var(--dark-solid);
height: 2.2em;
display: flex;
justify-content: space-between;
@@ -370,19 +382,19 @@ div.tab > button#optionsHide {
button.options {
width: 100%;
- background-color: #997b89;
+ background-color: var(--bg-main);
font-weight: bold;
border: none;
transition: 0.2s;
}
-button.options:hover {
- background-color: #806070 !important;
- color: white !important;
+button.active {
+ background-color: var(--header);
+ color: white;
}
-button.active {
- background-color: #916e7f;
+button.options:hover {
+ background-color: var(--header-active);
color: white;
}
@@ -412,10 +424,35 @@ button.active {
#options i {
color: #31272c;
- font-size: 0.8em;
+ font-size: 0.85em;
cursor: pointer;
}
+#options button i.icon-cog {
+ position: absolute;
+ padding: 0.1em 0.3em;
+ background-color: var(--bg-lighter);
+ border-radius: 50%;
+ visibility: hidden;
+ opacity: 0;
+ transition: 0.4s ease-in-out;
+}
+
+#options button i.icon-cog:hover {
+ color: #111;
+ background-color: var(--bg-light);
+ transform: rotateZ(180deg);
+}
+
+#options button i.icon-cog:active {
+ transform: translateY(1px);
+}
+
+#options button:hover i.icon-cog {
+ visibility: visible;
+ opacity: 1;
+}
+
input[type="color"] {
-webkit-appearance: none;
cursor: pointer;
@@ -461,11 +498,11 @@ input[type="color"]::-webkit-color-swatch-wrapper {
border-radius: 15%;
width: 0.91em;
height: 0.91em;
- background: #a58394;
- border: 1px solid #5d4651;
+ background: var(--light-solid);
+ border: 1px solid var(--dark-solid);
cursor: pointer;
margin-top: -0.4em;
- box-shadow: 0.5px 0.5px 0px #5d4651;
+ box-shadow: 0.5px 0.5px 0px var(--dark-solid);
}
#options input[type="range"]::-moz-range-thumb {
@@ -473,10 +510,10 @@ input[type="color"]::-webkit-color-swatch-wrapper {
border-radius: 15%;
width: 0.73em;
height: 0.73em;
- background: #a58394;
- border: 1px solid #5d4651;
+ background: var(--light-solid);
+ border: 1px solid var(--dark-solid);
cursor: pointer;
- box-shadow: 0.5px 0.5px 0px #5d4651;
+ box-shadow: 0.5px 0.5px 0px var(--dark-solid);
}
#options input[type="range"]::-webkit-slider-runnable-track {
@@ -520,7 +557,7 @@ input[type="color"]::-webkit-color-swatch-wrapper {
}
#optionsContent input[type="number"]:hover {
- outline: 1px solid #5d4651;
+ outline: 1px solid var(--dark-solid);
}
#optionsContent input.paired {
@@ -542,10 +579,9 @@ input[type="color"]::-webkit-color-swatch-wrapper {
width: 100%;
}
-#optionsSeedGenerate:before {
- content: "✓";
- margin-left: -2px;
- font-weight: bold;
+#options input[type="color"] {
+ width: 2em;
+ padding: 1px;
}
.tabcontent button.sideButton {
@@ -601,7 +637,7 @@ input[type="color"]::-webkit-color-swatch-wrapper {
#exitCustomization > div {
width: 12em;
- background: #5d4651;
+ background: var(--dark-solid);
cursor: move;
}
@@ -637,7 +673,7 @@ input[type="color"]::-webkit-color-swatch-wrapper {
}
.tabcontent button {
- background-color: #916e7f;
+ background-color: var(--bg-lighter);
border: none;
padding: 0.45em 0.75em;
margin: 0.35em 0;
@@ -645,8 +681,13 @@ input[type="color"]::-webkit-color-swatch-wrapper {
font-size: 1em;
}
+.tabcontent button.pressed {
+ background-color: var(--header);
+ font-style: italic;
+}
+
.tabcontent button:hover {
- background-color: #a8879d !important;
+ background-color: var(--header-active);
}
#toolsContent div {
@@ -679,12 +720,12 @@ input[type="color"]::-webkit-color-swatch-wrapper {
}
fieldset {
- border: 1px solid #5d4651;
+ border: 1px solid var(--dark-solid);
}
.tabcontent li {
list-style-type: none;
- background-color: #997b89;
+ background-color: var(--bg-main);
cursor: pointer;
padding: 0.35em;
margin: 0.2em 0.3em;
@@ -693,14 +734,17 @@ fieldset {
text-align: center;
}
-#options .buttonoff {
- background-color: #b6b4b440 !important;
- color: #666;
+.tabcontent .buttonoff {
+ background-color: var(--bg-disabled);
+ color: #444444aa;
+}
+
+.tabcontent li:hover {
+ box-shadow: 0 0 2px 2px var(--dark-solid) 17;
}
-.tabcontent li:hover,
.tabcontent button:hover {
- box-shadow: 0 0 2px 2px #5d465117;
+ background-color: var(--header);
}
#optionsContainer span {
@@ -799,7 +843,7 @@ fieldset {
table.matrix-table th,
table.matrix-table td {
- border: 1px solid #5d4651;
+ border: 1px solid var(--dark-solid);
height: 2em;
padding: 0.2em;
position: relative;
@@ -815,7 +859,7 @@ table.matrix-table tr:hover th {
}
table.matrix-table td:hover {
- outline: 2px solid #5d4651;
+ outline: 2px solid var(--dark-solid);
outline-offset: -1px;
z-index: 1;
}
@@ -1142,7 +1186,7 @@ i.resetButton {
}
i.resetButton:active {
- color: #5d4651;
+ color: var(--dark-solid);
}
.ui-dialog button.pressed {
@@ -1183,7 +1227,7 @@ i.resetButton:active {
}
.ui-dialog input[type="number"] {
- width: 3.5em;
+ width: 4.5em;
}
.ui-dialog .disabled {
@@ -1266,16 +1310,19 @@ div.slider .ui-slider-handle {
scrollbar-width: thin;
}
+#alertMessage::-webkit-scrollbar,
.table::-webkit-scrollbar {
width: 6px;
background-color: transparent;
}
+#alertMessage::-webkit-scrollbar-thumb,
.table::-webkit-scrollbar-thumb {
background-color: #aaa;
border-radius: 6px;
}
+#alertMessage::-webkit-scrollbar-thumb:hover,
.table::-webkit-scrollbar-thumb:hover {
background: #666;
}
@@ -1364,11 +1411,16 @@ div.states > .statePopulation {
width: 3em;
}
+div.states:hover > .hiddenIcon {
+ visibility: visible !important;
+}
+
div.states .icon-pencil,
div.states .icon-trash-empty,
div.states .icon-eye,
div.states .icon-pin,
-div.states .icon-flag-empty {
+div.states .icon-flag-empty,
+div.states .icon-cw {
cursor: pointer;
}
@@ -1519,9 +1571,11 @@ div.states > .coaIcon > use {
#stateNameEditor div.label,
#provinceNameEditor div.label,
-#regimentBody div.label {
+#regimentBody div.label,
+#markerEditor div.label {
display: inline-block;
width: 5.5em;
+ padding: 0.3em 0;
}
#saveTilesScreen div.label {
@@ -1529,10 +1583,6 @@ div.states > .coaIcon > use {
width: 5em;
}
-#regimentBody div {
- margin: 0.1em 0;
-}
-
#regimentBody input[type="number"] {
width: 5em;
}
@@ -1677,8 +1727,8 @@ div.editorLine {
}
#pickerHeader {
- fill: #916e7f;
- stroke: #5d4651;
+ fill: var(--header);
+ stroke: var(--dark-solid);
cursor: move;
}
@@ -1692,7 +1742,7 @@ div.editorLine {
#pickerCloseRect {
cursor: pointer;
- fill: #916e7f;
+ fill: var(--header);
stroke: #f8ffff;
}
@@ -1918,7 +1968,7 @@ input[type="checkbox"] {
.checkbox + .checkbox-label:before {
content: "";
display: inline-block;
- vertical-align: text-top;
+ vertical-align: middle;
width: 0.6em;
height: 0.6em;
padding: 0.2em;
@@ -1965,10 +2015,7 @@ div.textual span,
font-family: monospace;
user-select: none;
text-anchor: middle;
-}
-
-#markerEditor > button {
- vertical-align: top;
+ dominant-baseline: central;
}
.highlighted {
@@ -2198,14 +2245,14 @@ svg.button {
color: #920303;
background-color: #dabdbd91;
padding: 2px;
- border: 1px solid #916e7f;
+ border: 1px solid var(--header);
}
.announcement {
background-color: #a18888;
color: white;
padding: 0.4em 0.5em;
- border: dashed 1px #5d4651;
+ border: dashed 1px var(--dark-solid);
}
.speaker {
diff --git a/index.html b/index.html
index b35cdbea..3a355ff2 100644
--- a/index.html
+++ b/index.html
@@ -16,7 +16,7 @@
"
+ );
+
+ // add xlink: for href to support svg1.1
+ if (type === "svg") {
+ cloneEl.querySelectorAll("[href]").forEach(el => {
+ const href = el.getAttribute("href");
+ el.removeAttribute("href");
+ el.setAttribute("xlink:href", href);
+ });
+ }
+
+ const usedFonts = getUsedFonts(cloneEl);
+ const fontsToLoad = usedFonts.filter(font => font.src);
+ if (fontsToLoad.length) {
+ const dataURLfonts = await loadFontsAsDataURI(fontsToLoad);
+
+ const fontFaces = dataURLfonts
+ .map(({family, src, unicodeRange = "", variant = "normal"}) => {
+ return `@font-face {font-family: "${family}"; src: ${src}; unicode-range: ${unicodeRange}; font-variant: ${variant};}`;
+ })
+ .join("\n");
+
+ const style = document.createElement("style");
+ style.setAttribute("type", "text/css");
+ style.innerHTML = fontFaces;
+ cloneEl.querySelector("defs").appendChild(style);
+ }
+
+ clone.remove();
+
+ 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;
+}
+
+// remove hidden g elements and g elements without children to make downloaded svg smaller in size
+function removeUnusedElements(clone) {
+ if (!terrain.selectAll("use").size()) clone.select("#defs-relief")?.remove();
+
+ 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();
+ }
+ if (this.hasAttribute("display") && this.style.display === "inline") this.removeAttribute("display");
+ });
+ }
+}
+
+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));
+}
+
+// for each g element get inline style
+function inlineStyle(clone) {
+ const emptyG = clone.append("g").node();
+ const defaultStyles = window.getComputedStyle(emptyG);
+
+ clone.selectAll("g, #ruler *, #scaleBar > text").each(function () {
+ const compStyle = window.getComputedStyle(this);
+ let style = "";
+
+ for (let i = 0; i < compStyle.length; i++) {
+ const key = compStyle[i];
+ const value = compStyle.getPropertyValue(key);
+
+ // Firefox mask hack
+ if (key === "mask-image" && value !== defaultStyles.getPropertyValue(key)) {
+ style += "mask-image: url('#land');";
+ continue;
+ }
+
+ 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 + ";";
+ }
+
+ for (const key in compStyle) {
+ const value = compStyle.getPropertyValue(key);
+
+ 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 + ";";
+ }
+
+ if (style != "") this.setAttribute("style", style);
+ });
+
+ emptyG.remove();
+}
+
+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]]));
+
+ cells.i.forEach(i => {
+ const coordinates = getCellCoordinates(cells.v[i]);
+ const height = getHeight(i);
+ const biome = cells.biome[i];
+ const type = pack.features[cells.f[i]].type;
+ const population = getPopulation(i);
+ const state = cells.state[i];
+ const province = cells.province[i];
+ const culture = cells.culture[i];
+ const religion = cells.religion[i];
+ const neighbors = cells.c[i];
+
+ 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);
+ });
+
+ const name = getFileName("Cells") + ".geojson";
+ downloadFile(JSON.stringify(json), name, "application/json");
+}
+
+function saveGeoJSON_Routes() {
+ const json = {type: "FeatureCollection", features: []};
+
+ routes.selectAll("g > path").each(function () {
+ const coordinates = getRoutePoints(this);
+ const id = this.id;
+ const type = this.parentElement.id;
+
+ const feature = {type: "Feature", geometry: {type: "LineString", coordinates}, properties: {id, type}};
+ json.features.push(feature);
+ });
+
+ const name = getFileName("Routes") + ".geojson";
+ downloadFile(JSON.stringify(json), name, "application/json");
+}
+
+function saveGeoJSON_Rivers() {
+ const json = {type: "FeatureCollection", features: []};
+
+ rivers.selectAll("path").each(function () {
+ const coordinates = getRiverPoints(this);
+ const id = this.id;
+ const width = +this.dataset.increment;
+ const increment = +this.dataset.increment;
+ const river = pack.rivers.find(r => r.i === +id.slice(5));
+ const name = river ? river.name : "";
+ const type = river ? river.type : "";
+ const i = river ? river.i : "";
+ const basin = river ? river.basin : "";
+
+ const feature = {type: "Feature", geometry: {type: "LineString", coordinates}, properties: {id, i, basin, name, type, width, increment}};
+ json.features.push(feature);
+ });
+
+ const name = getFileName("Rivers") + ".geojson";
+ downloadFile(JSON.stringify(json), name, "application/json");
+}
+
+function saveGeoJSON_Markers() {
+ const features = pack.markers.map(marker => {
+ const {i, type, icon, x, y, size, fill, stroke} = marker;
+ const coordinates = getCoordinates(x, y, 4);
+ const id = `marker${i}`;
+ const note = notes.find(note => note.id === id);
+ const properties = {id, type, icon, ...note, size, fill, stroke};
+ return {type: "Feature", geometry: {type: "Point", coordinates}, properties};
+ });
+
+ const json = {type: "FeatureCollection", features};
+
+ const fileName = getFileName("Markers") + ".geojson";
+ downloadFile(JSON.stringify(json), fileName, "application/json");
+}
+
+function getCellCoordinates(vertices) {
+ const p = pack.vertices.p;
+ const coordinates = vertices.map(n => getCoordinates(p[n][0], p[n][1], 2));
+ return [coordinates.concat([coordinates[0]])];
+}
+
+function getRoutePoints(node) {
+ let points = [];
+ const l = node.getTotalLength();
+ const increment = l / Math.ceil(l / 2);
+ for (let i = 0; i <= l; i += increment) {
+ const p = node.getPointAtLength(i);
+ points.push(getCoordinates(p.x, p.y, 4));
+ }
+ return points;
+}
+
+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) {
+ const p1 = node.getPointAtLength(i);
+ const p2 = node.getPointAtLength(c);
+ const [x, y] = getCoordinates((p1.x + p2.x) / 2, (p1.y + p2.y) / 2, 4);
+ points.push([x, y]);
+ }
+ return points;
+}
diff --git a/modules/load.js b/modules/load.js
index ba53b72d..ad3211f3 100644
--- a/modules/load.js
+++ b/modules/load.js
@@ -1,5 +1,5 @@
-// Functions to save and load the map
"use strict";
+// Functions to load and parse .map files
function quickLoad() {
ldb.get("lastMap", blob => {
@@ -221,8 +221,8 @@ function parseLoadedData(data) {
if (settings[11]) barPosY.value = settings[11];
if (settings[12]) populationRate = populationRateInput.value = populationRateOutput.value = settings[12];
if (settings[13]) urbanization = urbanizationInput.value = urbanizationOutput.value = settings[13];
- if (settings[14]) mapSizeInput.value = mapSizeOutput.value = Math.max(Math.min(settings[14], 100), 1);
- if (settings[15]) latitudeInput.value = latitudeOutput.value = Math.max(Math.min(settings[15], 100), 0);
+ if (settings[14]) mapSizeInput.value = mapSizeOutput.value = minmax(settings[14], 1, 100);
+ if (settings[15]) latitudeInput.value = latitudeOutput.value = minmax(settings[15], 0, 100);
if (settings[16]) temperatureEquatorInput.value = temperatureEquatorOutput.value = settings[16];
if (settings[17]) temperaturePoleInput.value = temperaturePoleOutput.value = settings[17];
if (settings[18]) precInput.value = precOutput.value = settings[18];
@@ -230,7 +230,12 @@ function parseLoadedData(data) {
if (settings[20]) mapName.value = settings[20];
if (settings[21]) hideLabels.checked = +settings[21];
if (settings[22]) stylePreset.value = settings[22];
- if (settings[23]) rescaleLabels.checked = settings[23];
+ if (settings[23]) rescaleLabels.checked = +settings[23];
+ if (settings[24]) urbanDensity = urbanDensityInput.value = urbanDensityOutput.value = +settings[24];
+ })();
+
+ void (function applyOptionsToUI() {
+ stateLabelsModeInput.value = options.stateLabelsMode;
})();
void (function parseConfiguration() {
@@ -340,6 +345,7 @@ function parseLoadedData(data) {
pack.religions = data[29] ? JSON.parse(data[29]) : [{i: 0, name: "No religion"}];
pack.provinces = data[30] ? JSON.parse(data[30]) : [0];
pack.rivers = data[32] ? JSON.parse(data[32]) : [];
+ pack.markers = data[35] ? JSON.parse(data[35]) : [];
const cells = pack.cells;
cells.biome = Uint8Array.from(data[16].split(","));
@@ -405,7 +411,7 @@ function parseLoadedData(data) {
if (notHidden(labels)) turnOn("toggleLabels");
if (notHidden(icons)) turnOn("toggleIcons");
if (hasChildren(armies) && notHidden(armies)) turnOn("toggleMilitary");
- if (hasChildren(markers) && notHidden(markers)) turnOn("toggleMarkers");
+ if (hasChildren(markers)) turnOn("toggleMarkers");
if (notHidden(ruler)) turnOn("toggleRulers");
if (notHidden(scaleBar)) turnOn("toggleScaleBar");
@@ -431,12 +437,27 @@ function parseLoadedData(data) {
// 1.0 adds a legend box
legend = svg.append("g").attr("id", "legend");
- legend.attr("font-family", "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("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);
+ 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", 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");
@@ -460,7 +481,7 @@ function parseLoadedData(data) {
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();
+ Markers.generate();
turnButtonOn("toggleMarkers");
}
@@ -815,6 +836,7 @@ function parseLoadedData(data) {
const riverPoints = [];
const length = node.getTotalLength() / 2;
+ if (!length) continue;
const segments = Math.ceil(length / 6);
const increment = length / segments;
@@ -848,6 +870,64 @@ function parseLoadedData(data) {
rivers.attr("style", null);
borders.attr("style", null);
}
+
+ if (version < 1.7) {
+ // v 1.7 changed markers data
+ const defs = document.getElementById("defs-markers");
+ const markersGroup = document.getElementById("markers");
+
+ if (defs && markersGroup) {
+ const markerElements = markersGroup.querySelectorAll("use");
+ const rescale = +markersGroup.getAttribute("rescale");
+
+ pack.markers = Array.from(markerElements).map((el, i) => {
+ const id = el.getAttribute("id");
+ const note = notes.find(note => note.id === id);
+ if (note) note.id = `marker${i}`;
+
+ let x = +el.dataset.x;
+ let y = +el.dataset.y;
+
+ const transform = el.getAttribute("transform");
+ if (transform) {
+ const [dx, dy] = parseTransform(transform);
+ if (dx) x += +dx;
+ if (dy) y += +dy;
+ }
+ const cell = findCell(x, y);
+ const size = rn(rescale ? el.dataset.size * 30 : el.getAttribute("width"), 1);
+
+ const href = el.href.baseVal;
+ const type = href.replace("#marker_", "");
+ const symbol = defs?.querySelector(`symbol${href}`);
+ const text = symbol?.querySelector("text");
+ const circle = symbol?.querySelector("circle");
+
+ const icon = text?.innerHTML;
+ const px = text && Number(text.getAttribute("font-size")?.replace("px", ""));
+ const dx = text && Number(text.getAttribute("x")?.replace("%", ""));
+ const dy = text && Number(text.getAttribute("y")?.replace("%", ""));
+ const fill = circle && circle.getAttribute("fill");
+ const stroke = circle && circle.getAttribute("stroke");
+
+ const marker = {i, icon, type, x, y, size, cell};
+ if (size && size !== 30) marker.size = size;
+ if (!isNaN(px) && px !== 12) marker.px = px;
+ if (!isNaN(dx) && dx !== 50) marker.dx = dx;
+ if (!isNaN(dy) && dy !== 50) marker.dy = dy;
+ if (fill && fill !== "#ffffff") marker.fill = fill;
+ if (stroke && stroke !== "#000000") marker.stroke = stroke;
+ if (circle?.getAttribute("opacity") === "0") marker.pin = "no";
+
+ return marker;
+ });
+
+ markersGroup.style.display = null;
+ defs?.remove();
+ markerElements.forEach(el => el.remove());
+ if (layerIsOn("markers")) drawMarkers();
+ }
+ }
})();
void (function checkDataIntegrity() {
@@ -975,7 +1055,7 @@ function parseLoadedData(data) {
},
"New map": function () {
$(this).dialog("close");
- regenerateMap();
+ regenerateMap("loading error");
},
Cancel: function () {
$(this).dialog("close");
diff --git a/modules/markers-generator.js b/modules/markers-generator.js
new file mode 100644
index 00000000..b60c3599
--- /dev/null
+++ b/modules/markers-generator.js
@@ -0,0 +1,881 @@
+"use strict";
+
+window.Markers = (function () {
+ let config = [];
+ let occupied = [];
+
+ function getDefaultConfig() {
+ const culturesSet = document.getElementById("culturesSet").value;
+ const isFantasy = culturesSet.includes("Fantasy");
+
+ return [
+ {type: "volcanoes", icon: "🌋", multiplier: 1, fn: addVolcanoes},
+ {type: "hot-springs", icon: "♨️", multiplier: 1, fn: addHotSprings},
+ {type: "mines", icon: "⛏️", multiplier: 1, fn: addMines},
+ {type: "bridges", icon: "🌉", multiplier: 1, fn: addBridges},
+ {type: "inns", icon: "🍻", multiplier: 1, fn: addInns},
+ {type: "lighthouses", icon: "🚨", multiplier: 1, fn: addLighthouses},
+ {type: "waterfalls", icon: "⟱", multiplier: 1, fn: addWaterfalls},
+ {type: "battlefields", icon: "⚔️", multiplier: 1, fn: addBattlefields},
+ {type: "dungeons", icon: "🗝️", multiplier: 1, fn: addDungeons},
+ {type: "lake-monsters", icon: "🐉", multiplier: 1, fn: addLakeMonsters},
+ {type: "sea-monsters", icon: "🦑", multiplier: 1, fn: addSeaMonsters},
+ {type: "hill-monsters", icon: "👹", multiplier: 1, fn: addHillMonsters},
+ {type: "sacred-mountains", icon: "🗻", multiplier: 1, fn: addSacredMountains},
+ {type: "sacred-forests", icon: "🌳", multiplier: 1, fn: addSacredForests},
+ {type: "sacred-pineries", icon: "🌲", multiplier: 1, fn: addSacredPineries},
+ {type: "sacred-palm-groves", icon: "🌴", multiplier: 1, fn: addSacredPalmGroves},
+ {type: "brigands", icon: "💰", multiplier: 1, fn: addBrigands},
+ {type: "pirates", icon: "🏴☠️", multiplier: 1, fn: addPirates},
+ {type: "statues", icon: "🗿", multiplier: 1, fn: addStatues},
+ {type: "ruines", icon: "🏺", multiplier: 1, fn: addRuines},
+ {type: "portals", icon: "🌀", multiplier: +isFantasy, fn: addPortals}
+ ];
+ }
+
+ const getConfig = () => config;
+
+ const setConfig = newConfig => {
+ config = newConfig;
+ };
+
+ const generate = function () {
+ setConfig(getDefaultConfig());
+ pack.markers = [];
+ generateTypes();
+ };
+
+ const regenerate = () => {
+ pack.markers = pack.markers.filter(({i, lock, cell}) => {
+ if (lock) {
+ occupied[cell] = true;
+ return true;
+ }
+ const id = `marker${i}`;
+ document.getElementById(id)?.remove();
+ const index = notes.findIndex(note => note.id === id);
+ if (index != -1) notes.splice(index, 1);
+ return false;
+ });
+
+ generateTypes();
+ };
+
+ function generateTypes() {
+ TIME && console.time("addMarkers");
+
+ config.forEach(({type, icon, multiplier, fn}) => {
+ if (multiplier === 0) return;
+ fn(type, icon, multiplier);
+ });
+
+ occupied = [];
+ TIME && console.timeEnd("addMarkers");
+ }
+
+ function getQuantity(array, min, each, multiplier) {
+ if (!array.length || array.length < min / multiplier) return 0;
+ const requestQty = Math.ceil((array.length / each) * multiplier);
+ return array.length < requestQty ? array.length : requestQty;
+ }
+
+ function extractAnyElement(array) {
+ const index = Math.floor(Math.random() * array.length);
+ return array.splice(index, 1);
+ }
+
+ function getMarkerCoordinates(cell) {
+ const {cells, burgs} = pack;
+ const burgId = cells.burg[cell];
+
+ if (burgId) {
+ const {x, y} = burgs[burgId];
+ return [x, y];
+ }
+
+ return cells.p[cell];
+ }
+
+ function addMarker({cell, type, icon, dx, dy, px}) {
+ const i = pack.markers.length;
+ const [x, y] = getMarkerCoordinates(cell);
+ const marker = {i, icon, type, x, y, cell};
+ if (dx) marker.dx = dx;
+ if (dy) marker.dy = dy;
+ if (px) marker.px = px;
+ pack.markers.push(marker);
+ occupied[cell] = true;
+ return "marker" + i;
+ }
+
+ function addVolcanoes(type, icon, multiplier) {
+ const {cells} = pack;
+
+ let mountains = Array.from(cells.i.filter(i => !occupied[i] && cells.h[i] >= 70).sort((a, b) => cells.h[b] - cells.h[a]));
+ let quantity = getQuantity(mountains, 10, 500, multiplier);
+ if (!quantity) return;
+
+ while (quantity) {
+ const [cell] = extractAnyElement(mountains);
+ const id = addMarker({cell, icon, type, dx: 52, px: 13});
+ const proper = Names.getCulture(cells.culture[cell]);
+ const name = P(0.3) ? "Mount " + proper : Math.random() > 0.3 ? proper + " Volcano" : proper;
+ notes.push({id, name, legend: `Active volcano. Height: ${getFriendlyHeight(cells.p[cell])}`});
+ quantity--;
+ }
+ }
+
+ function addHotSprings(type, icon, multiplier) {
+ const {cells} = pack;
+
+ let springs = Array.from(cells.i.filter(i => !occupied[i] && cells.h[i] > 50).sort((a, b) => cells.h[b] - cells.h[a]));
+ let quantity = getQuantity(springs, 30, 1200, multiplier);
+ if (!quantity) return;
+
+ while (quantity) {
+ const [cell] = extractAnyElement(springs);
+ const id = addMarker({cell, icon, type, dy: 52});
+ const proper = Names.getCulture(cells.culture[cell]);
+ const temp = convertTemperature(gauss(35, 15, 20, 100));
+ notes.push({id, name: proper + " Hot Springs", legend: `A hot springs area. Average temperature: ${temp}`});
+ quantity--;
+ }
+ }
+
+ function addMines(type, icon, multiplier) {
+ const {cells} = pack;
+
+ let hillyBurgs = Array.from(cells.i.filter(i => !occupied[i] && cells.h[i] > 47 && cells.burg[i]));
+ let quantity = getQuantity(hillyBurgs, 1, 15, multiplier);
+ if (!quantity) return;
+
+ const resources = {salt: 5, gold: 2, silver: 4, copper: 2, iron: 3, lead: 1, tin: 1};
+
+ while (quantity && hillyBurgs.length) {
+ const [cell] = extractAnyElement(hillyBurgs);
+ const id = addMarker({cell, icon, type, dx: 48, px: 13});
+ const resource = rw(resources);
+ const burg = pack.burgs[cells.burg[cell]];
+ const name = `${burg.name} — ${resource} mining town`;
+ const population = rn(burg.population * populationRate * urbanization);
+ const legend = `${burg.name} is a mining town of ${population} people just nearby the ${resource} mine`;
+ notes.push({id, name, legend});
+ quantity--;
+ }
+ }
+
+ function addBridges(type, icon, multiplier) {
+ const {cells, burgs} = pack;
+
+ const meanFlux = d3.mean(cells.fl.filter(fl => fl));
+ let bridges = Array.from(
+ cells.i.filter(i => !occupied[i] && cells.burg[i] && cells.t[i] !== 1 && burgs[cells.burg[i]].population > 20 && cells.r[i] && cells.fl[i] > meanFlux)
+ );
+ let quantity = getQuantity(bridges, 1, 5, multiplier);
+ if (!quantity) return;
+
+ while (quantity) {
+ const [cell] = extractAnyElement(bridges);
+ const id = addMarker({cell, icon, type, px: 14});
+ const burg = pack.burgs[cells.burg[cell]];
+ const river = pack.rivers.find(r => r.i === pack.cells.r[cell]);
+ const riverName = river ? `${river.name} ${river.type}` : "river";
+ const name = river && P(0.2) ? river.name : burg.name;
+ const weightedAdjectives = {
+ stone: 10,
+ wooden: 1,
+ lengthy: 2,
+ formidable: 2,
+ rickety: 1,
+ beaten: 1,
+ weathered: 1
+ };
+ notes.push({id, name: `${name} Bridge`, legend: `A ${rw(weightedAdjectives)} bridge spans over the ${riverName} near ${burg.name}`});
+ quantity--;
+ }
+ }
+
+ function addInns(type, icon, multiplier) {
+ const {cells} = pack;
+
+ let taverns = Array.from(cells.i.filter(i => !occupied[i] && cells.h[i] >= 20 && cells.road[i] > 4 && cells.pop[i] > 10));
+ let quantity = getQuantity(taverns, 1, 100, multiplier);
+ if (!quantity) return;
+
+ const colors = [
+ "Dark",
+ "Light",
+ "Bright",
+ "Golden",
+ "White",
+ "Black",
+ "Red",
+ "Pink",
+ "Purple",
+ "Blue",
+ "Green",
+ "Yellow",
+ "Amber",
+ "Orange",
+ "Brown",
+ "Grey"
+ ];
+ const animals = [
+ "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 adjectives = [
+ "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"
+ ];
+ const methods = [
+ "Boiled",
+ "Grilled",
+ "Roasted",
+ "Spit-roasted",
+ "Stewed",
+ "Stuffed",
+ "Jugged",
+ "Mashed",
+ "Baked",
+ "Braised",
+ "Poached",
+ "Marinated",
+ "Pickled",
+ "Smoked",
+ "Dried",
+ "Dry-aged",
+ "Corned",
+ "Fried",
+ "Pan-fried",
+ "Deep-fried",
+ "Dressed",
+ "Steamed",
+ "Cured",
+ "Syrupped",
+ "Flame-Broiled"
+ ];
+ const courses = [
+ "beef",
+ "pork",
+ "bacon",
+ "chicken",
+ "lamb",
+ "chevon",
+ "hare",
+ "rabbit",
+ "hart",
+ "deer",
+ "antlers",
+ "bear",
+ "buffalo",
+ "badger",
+ "beaver",
+ "turkey",
+ "pheasant",
+ "duck",
+ "goose",
+ "teal",
+ "quail",
+ "pigeon",
+ "seal",
+ "carp",
+ "bass",
+ "pike",
+ "catfish",
+ "sturgeon",
+ "escallop",
+ "pie",
+ "cake",
+ "pottage",
+ "pudding",
+ "onions",
+ "carrot",
+ "potato",
+ "beet",
+ "garlic",
+ "cabbage",
+ "eggplant",
+ "eggs",
+ "broccoli",
+ "zucchini",
+ "pepper",
+ "olives",
+ "pumpkin",
+ "spinach",
+ "peas",
+ "chickpea",
+ "beans",
+ "rice",
+ "pasta",
+ "bread",
+ "apples",
+ "peaches",
+ "pears",
+ "melon",
+ "oranges",
+ "mango",
+ "tomatoes",
+ "cheese",
+ "corn",
+ "rat tails",
+ "pig ears"
+ ];
+ const types = ["hot", "cold", "fire", "ice", "smoky", "misty", "shiny", "sweet", "bitter", "salty", "sour", "sparkling", "smelly"];
+ const drinks = [
+ "wine",
+ "brandy",
+ "jinn",
+ "whisky",
+ "rom",
+ "beer",
+ "cider",
+ "mead",
+ "liquor",
+ "spirit",
+ "vodka",
+ "tequila",
+ "absinthe",
+ "nectar",
+ "milk",
+ "kvass",
+ "kumis",
+ "tea",
+ "water",
+ "juice",
+ "sap"
+ ];
+
+ while (quantity) {
+ const [cell] = extractAnyElement(taverns);
+ const id = addMarker({cell, icon, type, px: 14});
+ const typeName = P(0.3) ? "inn" : "tavern";
+ const isAnimalThemed = P(0.7);
+ const animal = ra(animals);
+ const name = isAnimalThemed ? (P(0.6) ? ra(colors) + " " + animal : ra(adjectives) + " " + animal) : ra(adjectives) + " " + capitalize(type);
+ const meal = isAnimalThemed && P(0.3) ? animal : ra(courses);
+ const course = `${ra(methods)} ${meal}`.toLowerCase();
+ const drink = `${P(0.5) ? ra(types) : ra(colors)} ${ra(drinks)}`.toLowerCase();
+ const legend = `A big and famous roadside ${typeName}. Delicious ${course} with ${drink} is served here`;
+ notes.push({id, name: "The " + name, legend});
+ quantity--;
+ }
+ }
+
+ function addLighthouses(type, icon, multiplier) {
+ const {cells} = pack;
+
+ const lighthouses = Array.from(cells.i.filter(i => !occupied[i] && cells.harbor[i] > 6 && cells.c[i].some(c => cells.h[c] < 20 && cells.road[c])));
+ let quantity = getQuantity(lighthouses, 1, 2, multiplier);
+ if (!quantity) return;
+
+ while (quantity) {
+ const [cell] = extractAnyElement(lighthouses);
+ const id = addMarker({cell, icon, type, px: 14});
+ 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 serve as a beacon for ships in the open sea`});
+ quantity--;
+ }
+ }
+
+ function addWaterfalls(type, icon, multiplier) {
+ const {cells} = pack;
+
+ const waterfalls = Array.from(cells.i.filter(i => cells.r[i] && !occupied[i] && cells.h[i] >= 50 && cells.c[i].some(c => cells.h[c] < 40 && cells.r[c])));
+ const quantity = getQuantity(waterfalls, 1, 5, multiplier);
+ if (!quantity) return;
+
+ const descriptions = [
+ "A gorgeous waterfall flows here",
+ "The rapids of an exceptionally beautiful waterfall",
+ "An impressive waterfall has cut through the land",
+ "The cascades of a stunning waterfall",
+ "A river drops down from a great height forming a wonderous waterfall",
+ "A breathtaking waterfall cuts through the landscape"
+ ];
+ for (let i = 0; i < waterfalls.length && i < quantity; i++) {
+ const cell = waterfalls[i];
+ const id = addMarker({cell, icon, type, dy: 54, px: 16});
+ 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: `${ra(descriptions)}`});
+ }
+ }
+
+ function addBattlefields(type, icon, multiplier) {
+ const {cells, states} = pack;
+
+ let battlefields = Array.from(cells.i.filter(i => !occupied[i] && cells.state[i] && cells.pop[i] > 2 && cells.h[i] < 50 && cells.h[i] > 25));
+ let quantity = getQuantity(battlefields, 50, 700, multiplier);
+ if (!quantity) return;
+
+ while (quantity && battlefields.length) {
+ const [cell] = extractAnyElement(battlefields);
+ const id = addMarker({cell, icon, type, dy: 52});
+ const state = states[cells.state[cell]];
+ if (!state.campaigns) state.campaigns = BurgsAndStates.generateCampaign(state);
+ const campaign = ra(state.campaigns);
+ const date = generateDate(campaign.start, campaign.end);
+ const name = Names.getCulture(cells.culture[cell]) + " Battlefield";
+ const legend = `A historical battle of the ${campaign.name}. \r\nDate: ${date} ${options.era}`;
+ notes.push({id, name, legend});
+ quantity--;
+ }
+ }
+
+ function addDungeons(type, icon, multiplier) {
+ const {cells} = pack;
+
+ let dungeons = Array.from(cells.i.filter(i => !occupied[i] && cells.pop[i] && cells.pop[i] < 3));
+ let quantity = getQuantity(dungeons, 30, 200, multiplier);
+ if (!quantity) return;
+
+ while (quantity) {
+ const [cell] = extractAnyElement(dungeons);
+ const id = addMarker({cell, icon, type, dy: 51, px: 13});
+
+ const dungeonSeed = `${seed}${cell}`;
+ const name = "Dungeon";
+ const legend = `
`;
+ notes.push({id, name, legend});
+ quantity--;
+ }
+ }
+
+ function addLakeMonsters(type, icon, multiplier) {
+ const {features} = pack;
+
+ const lakes = features.filter(feature => feature.type === "lake" && feature.group === "freshwater" && !occupied[feature.firstCell]);
+ let quantity = getQuantity(lakes, 2, 10, multiplier);
+ if (!quantity) return;
+
+ while (quantity) {
+ const [lake] = extractAnyElement(lakes);
+ const cell = lake.firstCell;
+ const id = addMarker({cell, icon, type, dy: 48});
+ const name = `${lake.name} Monster`;
+ const length = gauss(10, 5, 5, 100);
+ const legend = `Rumors say a relic monster of ${length} ${heightUnit.value} long inhabits ${lake.name} Lake. Truth or lie, folks are afraid to fish in the lake`;
+ notes.push({id, name, legend});
+ quantity--;
+ }
+ }
+
+ function addSeaMonsters(type, icon, multiplier) {
+ const {cells, features} = pack;
+
+ const sea = Array.from(cells.i.filter(i => !occupied[i] && cells.h[i] < 20 && cells.road[i] && features[cells.f[i]].type === "ocean"));
+ let quantity = getQuantity(sea, 50, 700, multiplier);
+ if (!quantity) return;
+
+ while (quantity) {
+ const [cell] = extractAnyElement(sea);
+ const id = addMarker({cell, icon, type});
+ const name = `${Names.getCultureShort(0)} Monster`;
+ const length = gauss(25, 10, 10, 100);
+ const legend = `Old sailors tell stories of a gigantic sea monster inhabiting these dangerous waters. Rumors say it can be ${length} ${heightUnit.value} long`;
+ notes.push({id, name, legend});
+ quantity--;
+ }
+ }
+
+ function addHillMonsters(type, icon, multiplier) {
+ const {cells} = pack;
+
+ const hills = Array.from(cells.i.filter(i => !occupied[i] && cells.h[i] >= 50 && cells.pop[i]));
+ let quantity = getQuantity(hills, 30, 600, multiplier);
+ if (!quantity) return;
+
+ const adjectives = [
+ "great",
+ "big",
+ "huge",
+ "prime",
+ "golden",
+ "proud",
+ "lucky",
+ "fat",
+ "giant",
+ "hungry",
+ "magical",
+ "superior",
+ "terrifying",
+ "horrifying",
+ "feared"
+ ];
+ const subjects = ["Locals", "Elders", "Inscriptions", "Tipplers", "Legends", "Whispers", "Rumors", "Journeying folk", "Tales"];
+ const species = [
+ "Ogre",
+ "Troll",
+ "Cyclops",
+ "Giant",
+ "Monster",
+ "Beast",
+ "Dragon",
+ "Undead",
+ "Ghoul",
+ "Vampire",
+ "Hag",
+ "Banshee",
+ "Bearded Devil",
+ "Roc",
+ "Hydra",
+ "Warg"
+ ];
+ const modusOperandi = [
+ "steals cattle at night",
+ "prefers eating children",
+ "doesn't mind of human flesh",
+ "keeps the region at bay",
+ "eats kids whole",
+ "abducts young women",
+ "terrorizes the region",
+ "harasses travelers in the area",
+ "snatches people from homes",
+ "attacks anyone who dares to approach its lair",
+ "attacks unsuspecting victims"
+ ];
+
+ while (quantity) {
+ const [cell] = extractAnyElement(hills);
+ const id = addMarker({cell, icon, type, dy: 54, px: 13});
+ const monster = ra(species);
+ const toponym = Names.getCulture(cells.culture[cell]);
+ const name = `${toponym} ${monster}`;
+ const legend = `${ra(subjects)} speak of a ${ra(adjectives)} ${monster} who inhabits ${toponym} hills and ${ra(modusOperandi)}`;
+ notes.push({id, name, legend});
+ quantity--;
+ }
+ }
+
+ function addSacredMountains(type, icon, multiplier) {
+ const {cells, cultures} = pack;
+
+ let lonelyMountains = Array.from(
+ cells.i.filter(i => !occupied[i] && cells.h[i] >= 70 && cells.c[i].some(c => cells.culture[c]) && cells.c[i].every(c => cells.h[c] < 60))
+ );
+ let quantity = getQuantity(lonelyMountains, 1, 5, multiplier);
+ if (!quantity) return;
+
+ while (quantity) {
+ const [cell] = extractAnyElement(lonelyMountains);
+ const id = addMarker({cell, icon, type, dy: 48});
+ const culture = cells.c[cell].map(c => cells.culture[c]).find(c => c);
+ const name = `${Names.getCulture(culture)} Mountain`;
+ const height = getFriendlyHeight(cells.p[cell]);
+ const legend = `A sacred mountain of ${cultures[culture].name} culture. Height: ${height}`;
+ notes.push({id, name, legend});
+ quantity--;
+ }
+ }
+
+ function addSacredForests(type, icon, multiplier) {
+ const {cells, cultures} = pack;
+
+ let temperateForests = Array.from(cells.i.filter(i => !occupied[i] && cells.culture[i] && [6, 8].includes(cells.biome[i])));
+ let quantity = getQuantity(temperateForests, 30, 1000, multiplier);
+ if (!quantity) return;
+
+ while (quantity) {
+ const [cell] = extractAnyElement(temperateForests);
+ const id = addMarker({cell, icon, type});
+ const culture = cells.culture[cell];
+ const name = `${Names.getCulture(culture)} Forest`;
+ const legend = `A sacred forest of ${cultures[culture].name} culture`;
+ notes.push({id, name, legend});
+ quantity--;
+ }
+ }
+
+ function addSacredPineries(type, icon, multiplier) {
+ const {cells, cultures} = pack;
+
+ let borealForests = Array.from(cells.i.filter(i => !occupied[i] && cells.culture[i] && cells.biome[i] === 9));
+ let quantity = getQuantity(borealForests, 30, 800, multiplier);
+ if (!quantity) return;
+
+ while (quantity) {
+ const [cell] = extractAnyElement(borealForests);
+ const id = addMarker({cell, icon, type, px: 13});
+ const culture = cells.culture[cell];
+ const name = `${Names.getCulture(culture)} Pinery`;
+ const legend = `A sacred pinery of ${cultures[culture].name} culture`;
+ notes.push({id, name, legend});
+ quantity--;
+ }
+ }
+
+ function addSacredPalmGroves(type, icon, multiplier) {
+ const {cells, cultures} = pack;
+
+ let oasises = Array.from(cells.i.filter(i => !occupied[i] && cells.culture[i] && cells.biome[i] === 1 && cells.pop[i] > 1 && cells.road[i]));
+ let quantity = getQuantity(oasises, 1, 100, multiplier);
+ if (!quantity) return;
+
+ while (quantity) {
+ const [cell] = extractAnyElement(oasises);
+ const id = addMarker({cell, icon, type, px: 13});
+ const culture = cells.culture[cell];
+ const name = `${Names.getCulture(culture)} Palm Grove`;
+ const legend = `A sacred palm grove of ${cultures[culture].name} culture`;
+ notes.push({id, name, legend});
+ quantity--;
+ }
+ }
+
+ function addBrigands(type, icon, multiplier) {
+ const {cells} = pack;
+
+ let roads = Array.from(cells.i.filter(i => !occupied[i] && cells.culture[i] && cells.road[i] > 4));
+ let quantity = getQuantity(roads, 50, 100, multiplier);
+ if (!quantity) return;
+
+ const animals = [
+ "Apes",
+ "Badgers",
+ "Bears",
+ "Beavers",
+ "Bisons",
+ "Boars",
+ "Cats",
+ "Crows",
+ "Dogs",
+ "Foxes",
+ "Hares",
+ "Hawks",
+ "Hyenas",
+ "Jackals",
+ "Jaguars",
+ "Leopards",
+ "Lions",
+ "Owls",
+ "Panthers",
+ "Rats",
+ "Ravens",
+ "Rooks",
+ "Scorpions",
+ "Sharks",
+ "Snakes",
+ "Spiders",
+ "Tigers",
+ "Wolfs",
+ "Wolverines",
+ "Falcons"
+ ];
+ const types = {brigands: 4, bandits: 3, robbers: 1, highwaymen: 1};
+
+ while (quantity) {
+ const [cell] = extractAnyElement(roads);
+ const id = addMarker({cell, icon, type, px: 13});
+ const culture = cells.culture[cell];
+ const biome = cells.biome[cell];
+ const height = cells.p[cell];
+ const locality =
+ height >= 70
+ ? "highlander"
+ : [1, 2].includes(biome)
+ ? "desert"
+ : [3, 4].includes(biome)
+ ? "mounted"
+ : [5, 6, 7, 8, 9].includes(biome)
+ ? "forest"
+ : biome === 12
+ ? "swamp"
+ : "angry";
+ const name = `${Names.getCulture(culture)} ${ra(animals)}`;
+ const legend = `A gang of ${locality} ${rw(types)}`;
+ notes.push({id, name, legend});
+ quantity--;
+ }
+ }
+
+ function addPirates(type, icon, multiplier) {
+ const {cells} = pack;
+
+ let searoutes = Array.from(cells.i.filter(i => !occupied[i] && cells.h[i] < 20 && cells.road[i]));
+ let quantity = getQuantity(searoutes, 40, 300, multiplier);
+ if (!quantity) return;
+
+ while (quantity) {
+ const [cell] = extractAnyElement(searoutes);
+ const id = addMarker({cell, icon, type, dx: 51});
+ const name = `Pirates`;
+ const legend = `Pirate ships have been spotted in these waters`;
+ notes.push({id, name, legend});
+ quantity--;
+ }
+ }
+
+ function addStatues(type, icon, multiplier) {
+ const {cells} = pack;
+ let statues = Array.from(cells.i.filter(i => !occupied[i] && cells.h[i] >= 20 && cells.h[i] < 40));
+ let quantity = getQuantity(statues, 80, 1200, multiplier);
+ if (!quantity) return;
+
+ const variants = ["Statue", "Obelisk", "Monument", "Column", "Monolith", "Pillar", "Megalith", "Stele", "Runestone", "Sculpture", "Effigy", "Idol"];
+ const scripts = {
+ cypriot: "𐠁𐠂𐠃𐠄𐠅𐠈𐠊𐠋𐠌𐠍𐠎𐠏𐠐𐠑𐠒𐠓𐠔𐠕𐠖𐠗𐠘𐠙𐠚𐠛𐠜𐠝𐠞𐠟𐠠𐠡𐠢𐠣𐠤𐠥𐠦𐠧𐠨𐠩𐠪𐠫𐠬𐠭𐠮𐠯𐠰𐠱𐠲𐠳𐠴𐠵𐠷𐠸𐠼𐠿 ",
+ geez: "ሀለሐመሠረሰቀበተኀነአከወዐዘየደገጠጰጸፀፈፐ ",
+ coptic: "ⲲⲴⲶⲸⲺⲼⲾⳀⳁⳂⳃⳄⳆⳈⳊⳌⳎⳐⳒⳔⳖⳘⳚⳜⳞⳠⳢⳤ⳥⳧⳩⳪ⳫⳬⳭⳲ⳹⳾ ",
+ tibetan: "ༀ༁༂༃༄༅༆༇༈༉༊་༌༐༑༒༓༔༕༖༗༘༙༚༛༜༠༡༢༣༤༥༦༧༨༩༪༫༬༭༮༯༰༱༲༳༴༵༶༷༸༹༺༻༼༽༾༿",
+ mongolian: "᠀᠐᠑᠒ᠠᠡᠦᠧᠨᠩᠪᠭᠮᠯᠰᠱᠲᠳᠵᠻᠼᠽᠾᠿᡀᡁᡆᡍᡎᡏᡐᡑᡒᡓᡔᡕᡖᡗᡙᡜᡝᡞᡟᡠᡡᡭᡮᡯᡰᡱᡲᡳᡴᢀᢁᢂᢋᢏᢐᢑᢒᢓᢛᢜᢞᢟᢠᢡᢢᢤᢥᢦ"
+ };
+
+ while (quantity) {
+ const [cell] = extractAnyElement(statues);
+ const id = addMarker({cell, icon, type});
+ const culture = cells.culture[cell];
+
+ const variant = ra(variants);
+ const name = `${Names.getCulture(culture)} ${variant}`;
+ const script = scripts[ra(Object.keys(scripts))];
+ const inscription = Array(rand(40, 100))
+ .fill(null)
+ .map(() => ra(script))
+ .join("");
+ const legend = `An ancient ${variant.toLowerCase()}. It has an inscription, but no one can translate it:
+ ${inscription}
`;
+ notes.push({id, name, legend});
+ quantity--;
+ }
+ }
+
+ function addRuines(type, icon, multiplier) {
+ const {cells} = pack;
+ let ruins = Array.from(cells.i.filter(i => !occupied[i] && cells.culture[i] && cells.h[i] >= 20 && cells.h[i] < 60));
+ let quantity = getQuantity(ruins, 80, 1200, multiplier);
+ if (!quantity) return;
+
+ const types = [
+ "City",
+ "Town",
+ "Settlement",
+ "Pyramid",
+ "Fort",
+ "Stronghold",
+ "Temple",
+ "Sacred site",
+ "Mausoleum",
+ "Outpost",
+ "Fortification",
+ "Fortress",
+ "Castle"
+ ];
+
+ while (quantity) {
+ const [cell] = extractAnyElement(ruins);
+ const id = addMarker({cell, icon, type});
+
+ const ruinType = ra(types);
+ const name = `Ruined ${ruinType}`;
+ const legend = `Ruins of an ancient ${ruinType.toLowerCase()}. Untold riches may lie within.`;
+ notes.push({id, name, legend});
+ quantity--;
+ }
+ }
+
+ function addPortals(type, icon, multiplier) {
+ const {burgs} = pack;
+ let portals = burgs
+ .slice(1, Math.ceil(burgs.length / 10) + 1)
+ .filter(({cell}) => !occupied[cell])
+ .map(burg => [burg.name, burg.cell]);
+ let quantity = getQuantity(portals, 16, 8, multiplier);
+ if (!quantity) return;
+
+ while (quantity) {
+ const [portal] = extractAnyElement(portals);
+ const [burgName, cell] = portal;
+ const id = addMarker({cell, icon, type, px: 14});
+ const name = `${burgName} Portal`;
+ const legend = `An element of the magic portal system connecting major cities. Portals installed centuries ago, but still work fine`;
+ notes.push({id, name, legend});
+ quantity--;
+ }
+ }
+
+ return {generate, regenerate, getConfig, setConfig};
+})();
diff --git a/modules/military-generator.js b/modules/military-generator.js
index 93ef24ce..c69dc1e3 100644
--- a/modules/military-generator.js
+++ b/modules/military-generator.js
@@ -3,9 +3,8 @@
window.Military = (function () {
const generate = function () {
TIME && console.time("generateMilitaryForces");
- const cells = pack.cells,
- p = cells.p,
- states = pack.states;
+ const {cells, states} = pack;
+ const {p} = cells;
const valid = states.filter(s => s.i && !s.removed); // valid states
if (!options.military) options.military = getDefaultOptions();
@@ -19,7 +18,6 @@ window.Military = (function () {
mounted: {Nomadic: 2.3, Highland: 0.6, Lake: 0.7, Naval: 0.3, Hunting: 0.7, River: 0.8},
machinery: {Nomadic: 0.8, Highland: 1.4, Lake: 1.1, Naval: 1.4, Hunting: 0.4, River: 1.1},
naval: {Nomadic: 0.5, Highland: 0.5, Lake: 1.2, Naval: 1.8, Hunting: 0.7, River: 1.2},
- // non-default generic:
armored: {Nomadic: 1, Highland: 0.5, Lake: 1, Naval: 1, Hunting: 0.7, River: 1.1},
aviation: {Nomadic: 0.5, Highland: 0.5, Lake: 1.2, Naval: 1.2, Hunting: 0.6, River: 1.2},
magical: {Nomadic: 1, Highland: 2, Lake: 1, Naval: 1, Hunting: 1, River: 1}
@@ -38,27 +36,24 @@ window.Military = (function () {
};
valid.forEach(s => {
- const temp = (s.temp = {}),
- d = s.diplomacy;
- const expansionRate = Math.min(Math.max(s.expansionism / expn / (s.area / area), 0.25), 4); // how much state expansionism is realized
+ s.temp = {};
+ const d = s.diplomacy;
+
+ const expansionRate = minmax(s.expansionism / expn / (s.area / area), 0.25, 4); // how much state expansionism is realized
const diplomacyRate = d.some(d => d === "Enemy") ? 1 : d.some(d => d === "Rival") ? 0.8 : d.some(d => d === "Suspicion") ? 0.5 : 0.1; // peacefulness
- const neighborsRate = Math.min(
- Math.max(
- s.neighbors.map(n => (n ? pack.states[n].diplomacy[s.i] : "Suspicion")).reduce((s, r) => (s += rate[r]), 0.5),
- 0.3
- ),
- 3
- ); // neighbors rate
- s.alert = Math.min(Math.max(rn(expansionRate * diplomacyRate * neighborsRate, 2), 0.1), 5); // war alert rate (army modifier)
- temp.platoons = [];
+ const neighborsRateRaw = s.neighbors.map(n => (n ? pack.states[n].diplomacy[s.i] : "Suspicion")).reduce((s, r) => (s += rate[r]), 0.5);
+ const neighborsRate = minmax(neighborsRateRaw, 0.3, 3); // neighbors rate
+ s.alert = minmax(rn(expansionRate * diplomacyRate * neighborsRate, 2), 0.1, 5); // alert rate (area modifier)
+ s.temp.platoons = [];
// apply overall state modifiers for unit types based on state features
for (const unit of options.military) {
if (!stateModifier[unit.type]) continue;
+
let modifier = stateModifier[unit.type][s.type] || 1;
if (unit.type === "mounted" && s.formName.includes("Horde")) modifier *= 2;
else if (unit.type === "naval" && s.form === "Republic") modifier *= 1.2;
- temp[unit.name] = modifier * s.alert;
+ s.temp[unit.name] = modifier * s.alert;
}
});
@@ -69,66 +64,96 @@ window.Military = (function () {
return "generic";
};
+ function passUnitLimits(unit, biome, state, culture, religion) {
+ if (unit.biomes && !unit.biomes.includes(biome)) return false;
+ if (unit.states && !unit.states.includes(state)) return false;
+ if (unit.cultures && !unit.cultures.includes(culture)) return false;
+ if (unit.religions && !unit.religions.includes(religion)) return false;
+ return true;
+ }
+
+ // rural cells
for (const i of cells.i) {
if (!cells.pop[i]) continue;
- const s = states[cells.state[i]]; // cell state
- if (!s.i || s.removed) continue;
- let m = cells.pop[i] / 100; // basic rural army in percentages
- if (cells.culture[i] !== s.culture) m = s.form === "Union" ? m / 1.2 : m / 2; // non-dominant culture
- if (cells.religion[i] !== cells.religion[s.center]) m = s.form === "Theocracy" ? m / 2.2 : m / 1.4; // non-dominant religion
- if (cells.f[i] !== cells.f[s.center]) m = s.type === "Naval" ? m / 1.2 : m / 1.8; // different landmass
+ const biome = cells.biome[i];
+ const state = cells.state[i];
+ const culture = cells.culture[i];
+ const religion = cells.religion[i];
+
+ const stateObj = states[state];
+ if (!state || stateObj.removed) continue;
+
+ let modifier = cells.pop[i] / 100; // basic rural army in percentages
+ if (culture !== stateObj.culture) modifier = stateObj.form === "Union" ? modifier / 1.2 : modifier / 2; // non-dominant culture
+ if (religion !== cells.religion[stateObj.center]) modifier = stateObj.form === "Theocracy" ? modifier / 2.2 : modifier / 1.4; // non-dominant religion
+ if (cells.f[i] !== cells.f[stateObj.center]) modifier = stateObj.type === "Naval" ? modifier / 1.2 : modifier / 1.8; // different landmass
const type = getType(i);
- for (const u of options.military) {
- const perc = +u.rural;
- if (isNaN(perc) || perc <= 0 || !s.temp[u.name]) continue;
+ for (const unit of options.military) {
+ const perc = +unit.rural;
+ if (isNaN(perc) || perc <= 0 || !stateObj.temp[unit.name]) continue;
+ if (!passUnitLimits(unit, biome, state, culture, religion)) continue;
+ if (unit.type === "naval" && !cells.haven[i]) continue; // only near-ocean cells create naval units
- const mod = type === "generic" ? 1 : cellTypeModifier[type][u.type]; // cell specific modifier
- const army = m * perc * mod; // rural cell army
- const t = rn(army * s.temp[u.name] * populationRate); // total troops
- if (!t) continue;
- let x = p[i][0],
- y = p[i][1],
- n = 0;
- if (u.type === "naval") {
- let haven = cells.haven[i];
- (x = p[haven][0]), (y = p[haven][1]);
+ const cellTypeMod = type === "generic" ? 1 : cellTypeModifier[type][unit.type]; // cell specific modifier
+ const army = modifier * perc * cellTypeMod; // rural cell army
+ const total = rn(army * stateObj.temp[unit.name] * populationRate); // total troops
+ if (!total) continue;
+
+ let [x, y] = p[i];
+ let n = 0;
+
+ // place naval units to sea
+ if (unit.type === "naval") {
+ const haven = cells.haven[i];
+ [x, y] = p[haven];
n = 1;
- } // place naval to sea
- s.temp.platoons.push({cell: i, a: t, t, x, y, u: u.name, n, s: u.separate, type: u.type});
+ }
+
+ stateObj.temp.platoons.push({cell: i, a: total, t: total, x, y, u: unit.name, n, s: unit.separate, type: unit.type});
}
}
+ // burgs
for (const b of pack.burgs) {
if (!b.i || b.removed || !b.state || !b.population) continue;
- const s = states[b.state]; // burg state
+ const biome = cells.biome[b.cell];
+ const state = b.state;
+ const culture = b.culture;
+ const religion = cells.religion[b.cell];
+
+ const stateObj = states[state];
let m = (b.population * urbanization) / 100; // basic urban army in percentages
if (b.capital) m *= 1.2; // capital has household troops
- if (b.culture !== s.culture) m = s.form === "Union" ? m / 1.2 : m / 2; // non-dominant culture
- if (cells.religion[b.cell] !== cells.religion[s.center]) m = s.form === "Theocracy" ? m / 2.2 : m / 1.4; // non-dominant religion
- if (cells.f[b.cell] !== cells.f[s.center]) m = s.type === "Naval" ? m / 1.2 : m / 1.8; // different landmass
+ if (culture !== stateObj.culture) m = stateObj.form === "Union" ? m / 1.2 : m / 2; // non-dominant culture
+ if (religion !== cells.religion[stateObj.center]) m = stateObj.form === "Theocracy" ? m / 2.2 : m / 1.4; // non-dominant religion
+ if (cells.f[b.cell] !== cells.f[stateObj.center]) m = stateObj.type === "Naval" ? m / 1.2 : m / 1.8; // different landmass
const type = getType(b.cell);
- for (const u of options.military) {
- if (u.type === "naval" && !b.port) continue; // only ports produce naval units
- const perc = +u.urban;
- if (isNaN(perc) || perc <= 0 || !s.temp || !s.temp[u.name]) continue;
+ for (const unit of options.military) {
+ const perc = +unit.urban;
+ if (isNaN(perc) || perc <= 0 || !stateObj.temp[unit.name]) continue;
+ if (!passUnitLimits(unit, biome, state, culture, religion)) continue;
+ if (unit.type === "naval" && (!b.port || !cells.haven[b.cell])) continue; // only ports create naval units
- const mod = type === "generic" ? 1 : burgTypeModifier[type][u.type]; // cell specific modifier
+ const mod = type === "generic" ? 1 : burgTypeModifier[type][unit.type]; // cell specific modifier
const army = m * perc * mod; // urban cell army
- const t = rn(army * s.temp[u.name] * populationRate); // total troops
- if (!t) continue;
- let x = p[b.cell][0],
- y = p[b.cell][1],
- n = 0;
- if (u.type === "naval") {
- let haven = cells.haven[b.cell];
- (x = p[haven][0]), (y = p[haven][1]);
+ const total = rn(army * stateObj.temp[unit.name] * populationRate); // total troops
+ if (!total) continue;
+
+ let [x, y] = p[b.cell];
+ let n = 0;
+
+ // place naval to sea
+ if (unit.type === "naval") {
+ const haven = cells.haven[b.cell];
+ [x, y] = p[haven];
n = 1;
- } // place naval in sea cell
- s.temp.platoons.push({cell: b.cell, a: t, t, x, y, u: u.name, n, s: u.separate, type: u.type});
+ }
+
+ stateObj.temp.platoons.push({cell: b.cell, a: total, t: total, x, y, u: unit.name, n, s: unit.separate, type: unit.type});
}
}
@@ -141,7 +166,7 @@ window.Military = (function () {
})();
const expected = 3 * populationRate; // expected regiment size
- const mergeable = (n0, n1) => (!n0.s && !n1.s) || n0.type === n1.type; // check if regiments can be merged
+ const mergeable = (n0, n1) => (!n0.s && !n1.s) || n0.u === n1.u; // check if regiments can be merged
// get regiments for each state
valid.forEach(s => {
@@ -152,25 +177,27 @@ window.Military = (function () {
function createRegiments(nodes, s) {
if (!nodes.length) return [];
+
nodes.sort((a, b) => a.a - b.a); // form regiments in cells with most troops
const tree = d3.quadtree(
nodes,
d => d.x,
d => d.y
);
- nodes.forEach(n => {
- tree.remove(n);
- const overlap = tree.find(n.x, n.y, 20);
- if (overlap && overlap.t && mergeable(n, overlap)) {
- merge(n, overlap);
+
+ nodes.forEach(node => {
+ tree.remove(node);
+ const overlap = tree.find(node.x, node.y, 20);
+ if (overlap && overlap.t && mergeable(node, overlap)) {
+ merge(node, overlap);
return;
}
- if (n.t > expected) return;
- const r = (expected - n.t) / (n.s ? 40 : 20); // search radius
- const candidates = tree.findAll(n.x, n.y, r);
+ if (node.t > expected) return;
+ const r = (expected - node.t) / (node.s ? 40 : 20); // search radius
+ const candidates = tree.findAll(node.x, node.y, r);
for (const c of candidates) {
- if (c.t < expected && mergeable(n, c)) {
- merge(n, c);
+ if (c.t < expected && mergeable(node, c)) {
+ merge(node, c);
break;
}
}
@@ -334,7 +361,13 @@ window.Military = (function () {
const getName = function (r, regiments) {
const cells = pack.cells;
- const proper = r.n ? null : cells.province[r.cell] && pack.provinces[cells.province[r.cell]] ? pack.provinces[cells.province[r.cell]].name : cells.burg[r.cell] && pack.burgs[cells.burg[r.cell]] ? pack.burgs[cells.burg[r.cell]].name : null;
+ const proper = r.n
+ ? null
+ : cells.province[r.cell] && pack.provinces[cells.province[r.cell]]
+ ? pack.provinces[cells.province[r.cell]].name
+ : cells.burg[r.cell] && pack.burgs[cells.burg[r.cell]]
+ ? pack.burgs[cells.burg[r.cell]].name
+ : null;
const number = nth(regiments.filter(reg => reg.n === r.n && reg.i < r.i).length + 1);
const form = r.n ? "Fleet" : "Regiment";
return `${number}${proper ? ` (${proper}) ` : ` `}${form}`;
@@ -351,7 +384,12 @@ window.Military = (function () {
const generateNote = function (r, s) {
const cells = pack.cells;
- const base = cells.burg[r.cell] && pack.burgs[cells.burg[r.cell]] ? pack.burgs[cells.burg[r.cell]].name : cells.province[r.cell] && pack.provinces[cells.province[r.cell]] ? pack.provinces[cells.province[r.cell]].fullName : null;
+ const base =
+ cells.burg[r.cell] && pack.burgs[cells.burg[r.cell]]
+ ? pack.burgs[cells.burg[r.cell]].name
+ : cells.province[r.cell] && pack.provinces[cells.province[r.cell]]
+ ? pack.provinces[cells.province[r.cell]].fullName
+ : null;
const station = base ? `${r.name} is ${r.n ? "based" : "stationed"} in ${base}. ` : "";
const composition = r.a
diff --git a/modules/names-generator.js b/modules/names-generator.js
index 3bd647a3..d7078abb 100644
--- a/modules/names-generator.js
+++ b/modules/names-generator.js
@@ -128,20 +128,14 @@ window.Names = (function () {
// generate name for culture
const getCulture = function (culture, min, max, dupl) {
- if (culture === undefined) {
- ERROR && console.error("Please define a culture");
- return;
- }
+ if (culture === undefined) return ERROR && console.error("Please define a culture");
const base = pack.cultures[culture].base;
return getBase(base, min, max, dupl);
};
// generate short name for culture
const getCultureShort = function (culture) {
- if (culture === undefined) {
- ERROR && console.error("Please define a culture");
- return;
- }
+ if (culture === undefined) return ERROR && console.error("Please define a culture");
return getBaseShort(pack.cultures[culture].base);
};
@@ -157,55 +151,75 @@ window.Names = (function () {
};
// generate state name based on capital or random name and culture-specific suffix
- // prettier-ignore
- const getState = function(name, culture, base) {
- if (name === undefined) {ERROR && console.error("Please define a base name"); return;}
- if (culture === undefined && base === undefined) {ERROR && console.error("Please define a culture"); return;}
+ const getState = function (name, culture, base) {
+ if (name === undefined) return ERROR && console.error("Please define a base name");
+ if (culture === undefined && base === undefined) return ERROR && console.error("Please define a culture");
if (base === undefined) base = pack.cultures[culture].base;
// exclude endings inappropriate for states name
if (name.includes(" ")) name = capitalize(name.replace(/ /g, "").toLowerCase()); // don't allow multiword state names
- if (name.length > 6 && name.slice(-4) === "berg") name = name.slice(0,-4); // remove -berg for any
- if (name.length > 5 && name.slice(-3) === "ton") name = name.slice(0,-3); // remove -ton for any
+ if (name.length > 6 && name.slice(-4) === "berg") name = name.slice(0, -4); // remove -berg for any
+ if (name.length > 5 && name.slice(-3) === "ton") name = name.slice(0, -3); // remove -ton for any
- if (base === 5 && ["sk", "ev", "ov"].includes(name.slice(-2))) name = name.slice(0,-2); // remove -sk/-ev/-ov for Ruthenian
- else if (base === 12) return vowel(name.slice(-1)) ? name : name + "u"; // Japanese ends on any vowel or -u
- else if (base === 18 && P(.4)) name = vowel(name.slice(0,1).toLowerCase()) ? "Al" + name.toLowerCase() : "Al " + name; // Arabic starts with -Al
+ if (base === 5 && ["sk", "ev", "ov"].includes(name.slice(-2))) name = name.slice(0, -2);
+ // remove -sk/-ev/-ov for Ruthenian
+ else if (base === 12) return vowel(name.slice(-1)) ? name : name + "u";
+ // Japanese ends on any vowel or -u
+ else if (base === 18 && P(0.4)) name = vowel(name.slice(0, 1).toLowerCase()) ? "Al" + name.toLowerCase() : "Al " + name; // Arabic starts with -Al
// no suffix for fantasy bases
if (base > 32 && base < 42) return name;
// define if suffix should be used
if (name.length > 3 && vowel(name.slice(-1))) {
- if (vowel(name.slice(-2,-1)) && P(.85)) name = name.slice(0,-2); // 85% for vv
- else if (P(.7)) name = name.slice(0,-1); // ~60% for cv
+ if (vowel(name.slice(-2, -1)) && P(0.85)) name = name.slice(0, -2);
+ // 85% for vv
+ else if (P(0.7)) name = name.slice(0, -1);
+ // ~60% for cv
else return name;
- } else if (P(.4)) return name; // 60% for cc and vc
+ } else if (P(0.4)) return name; // 60% for cc and vc
// define suffix
let suffix = "ia"; // standard suffix
- const rnd = Math.random(), l = name.length;
- if (base === 3 && rnd < .03 && l < 7) suffix = "terra"; // Italian
- else if (base === 4 && rnd < .03 && l < 7) suffix = "terra"; // Spanish
- else if (base === 13 && rnd < .03 && l < 7) suffix = "terra"; // Portuguese
- else if (base === 2 && rnd < .03 && l < 7) suffix = "terre"; // French
- else if (base === 0 && rnd < .5 && l < 7) suffix = "land"; // German
- else if (base === 1 && rnd < .4 && l < 7 ) suffix = "land"; // English
- else if (base === 6 && rnd < .3 && l < 7) suffix = "land"; // Nordic
- else if (base === 32 && rnd < .1 && l < 7) suffix = "land"; // generic Human
- else if (base === 7 && rnd < .1) suffix = "eia"; // Greek
- else if (base === 9 && rnd < .35) suffix = "maa"; // Finnic
- else if (base === 15 && rnd < .4 && l < 6) suffix = "orszag"; // Hungarian
- else if (base === 16) suffix = rnd < .6 ? "stan" : "ya"; // Turkish
- else if (base === 10) suffix = "guk"; // Korean
- else if (base === 11) suffix = " Guo"; // Chinese
- else if (base === 14) suffix = rnd < .5 && l < 6 ? "tlan" : "co"; // Nahuatl
- else if (base === 17 && rnd < .8) suffix = "a"; // Berber
- else if (base === 18 && rnd < .8) suffix = "a"; // Arabic
+ const rnd = Math.random(),
+ l = name.length;
+ if (base === 3 && rnd < 0.03 && l < 7) suffix = "terra";
+ // Italian
+ else if (base === 4 && rnd < 0.03 && l < 7) suffix = "terra";
+ // Spanish
+ else if (base === 13 && rnd < 0.03 && l < 7) suffix = "terra";
+ // Portuguese
+ else if (base === 2 && rnd < 0.03 && l < 7) suffix = "terre";
+ // French
+ else if (base === 0 && rnd < 0.5 && l < 7) suffix = "land";
+ // German
+ else if (base === 1 && rnd < 0.4 && l < 7) suffix = "land";
+ // English
+ else if (base === 6 && rnd < 0.3 && l < 7) suffix = "land";
+ // Nordic
+ else if (base === 32 && rnd < 0.1 && l < 7) suffix = "land";
+ // generic Human
+ else if (base === 7 && rnd < 0.1) suffix = "eia";
+ // Greek
+ else if (base === 9 && rnd < 0.35) suffix = "maa";
+ // Finnic
+ else if (base === 15 && rnd < 0.4 && l < 6) suffix = "orszag";
+ // Hungarian
+ else if (base === 16) suffix = rnd < 0.6 ? "stan" : "ya";
+ // Turkish
+ else if (base === 10) suffix = "guk";
+ // Korean
+ else if (base === 11) suffix = " Guo";
+ // Chinese
+ else if (base === 14) suffix = rnd < 0.5 && l < 6 ? "tlan" : "co";
+ // Nahuatl
+ else if (base === 17 && rnd < 0.8) suffix = "a";
+ // Berber
+ else if (base === 18 && rnd < 0.8) suffix = "a"; // Arabic
return validateSuffix(name, suffix);
- }
+ };
function validateSuffix(name, suffix) {
if (name.slice(-1 * suffix.length) === suffix) return name; // no suffix if name already ends with it
@@ -248,7 +262,7 @@ window.Names = (function () {
{name: "English", i: 1, min: 6, max: 11, d: "", m: .1, b: "Abingdon,Albrighton,Alcester,Almondbury,Altrincham,Amersham,Andover,Appleby,Ashboume,Atherstone,Aveton,Axbridge,Aylesbury,Baldock,Bamburgh,Barton,Basingstoke,Berden,Bere,Berkeley,Berwick,Betley,Bideford,Bingley,Birmingham,Blandford,Blechingley,Bodmin,Bolton,Bootham,Boroughbridge,Boscastle,Bossinney,Bramber,Brampton,Brasted,Bretford,Bridgetown,Bridlington,Bromyard,Bruton,Buckingham,Bungay,Burton,Calne,Cambridge,Canterbury,Carlisle,Castleton,Caus,Charmouth,Chawleigh,Chichester,Chillington,Chinnor,Chipping,Chisbury,Cleobury,Clifford,Clifton,Clitheroe,Cockermouth,Coleshill,Combe,Congleton,Crafthole,Crediton,Cuddenbeck,Dalton,Darlington,Dodbrooke,Drax,Dudley,Dunstable,Dunster,Dunwich,Durham,Dymock,Exeter,Exning,Faringdon,Felton,Fenny,Finedon,Flookburgh,Fowey,Frampton,Gateshead,Gatton,Godmanchester,Grampound,Grantham,Guildford,Halesowen,Halton,Harbottle,Harlow,Hatfield,Hatherleigh,Haydon,Helston,Henley,Hertford,Heytesbury,Hinckley,Hitchin,Holme,Hornby,Horsham,Kendal,Kenilworth,Kilkhampton,Kineton,Kington,Kinver,Kirby,Knaresborough,Knutsford,Launceston,Leighton,Lewes,Linton,Louth,Luton,Lyme,Lympstone,Macclesfield,Madeley,Malborough,Maldon,Manchester,Manningtree,Marazion,Marlborough,Marshfield,Mere,Merryfield,Middlewich,Midhurst,Milborne,Mitford,Modbury,Montacute,Mousehole,Newbiggin,Newborough,Newbury,Newenden,Newent,Norham,Northleach,Noss,Oakham,Olney,Orford,Ormskirk,Oswestry,Padstow,Paignton,Penkneth,Penrith,Penzance,Pershore,Petersfield,Pevensey,Pickering,Pilton,Pontefract,Portsmouth,Preston,Quatford,Reading,Redcliff,Retford,Rockingham,Romney,Rothbury,Rothwell,Salisbury,Saltash,Seaford,Seasalter,Sherston,Shifnal,Shoreham,Sidmouth,Skipsea,Skipton,Solihull,Somerton,Southam,Southwark,Standon,Stansted,Stapleton,Stottesdon,Sudbury,Swavesey,Tamerton,Tarporley,Tetbury,Thatcham,Thaxted,Thetford,Thornbury,Tintagel,Tiverton,Torksey,Totnes,Towcester,Tregoney,Trematon,Tutbury,Uxbridge,Wallingford,Wareham,Warenmouth,Wargrave,Warton,Watchet,Watford,Wendover,Westbury,Westcheap,Weymouth,Whitford,Wickwar,Wigan,Wigmore,Winchelsea,Winkleigh,Wiscombe,Witham,Witheridge,Wiveliscombe,Woodbury,Yeovil"},
{name: "French", i: 2, min: 5, max: 13, d: "nlrs", m: .1, b: "Adon,Aillant,Amilly,Andonville,Ardon,Artenay,Ascheres,Ascoux,Attray,Aubin,Audeville,Aulnay,Autruy,Auvilliers,Auxy,Aveyron,Baccon,Bardon,Barville,Batilly,Baule,Bazoches,Beauchamps,Beaugency,Beaulieu,Beaune,Bellegarde,Boesses,Boigny,Boiscommun,Boismorand,Boisseaux,Bondaroy,Bonnee,Bonny,Bordes,Bou,Bougy,Bouilly,Boulay,Bouzonville,Bouzy,Boynes,Bray,Breteau,Briare,Briarres,Bricy,Bromeilles,Bucy,Cepoy,Cercottes,Cerdon,Cernoy,Cesarville,Chailly,Chaingy,Chalette,Chambon,Champoulet,Chanteau,Chantecoq,Chapell,Charme,Charmont,Charsonville,Chateau,Chateauneuf,Chatel,Chatenoy,Chatillon,Chaussy,Checy,Chevannes,Chevillon,Chevilly,Chevry,Chilleurs,Choux,Chuelles,Clery,Coinces,Coligny,Combleux,Combreux,Conflans,Corbeilles,Corquilleroy,Cortrat,Coudroy,Coullons,Coulmiers,Courcelles,Courcy,Courtemaux,Courtempierre,Courtenay,Cravant,Crottes,Dadonville,Dammarie,Dampierre,Darvoy,Desmonts,Dimancheville,Donnery,Dordives,Dossainville,Douchy,Dry,Echilleuses,Egry,Engenville,Epieds,Erceville,Ervauville,Escrennes,Escrignelles,Estouy,Faverelles,Fay,Feins,Ferolles,Ferrieres,Fleury,Fontenay,Foret,Foucherolles,Freville,Gatinais,Gaubertin,Gemigny,Germigny,Gidy,Gien,Girolles,Givraines,Gondreville,Grangermont,Greneville,Griselles,Guigneville,Guilly,Gyleslonains,Huetre,Huisseau,Ingrannes,Ingre,Intville,Isdes,Jargeau,Jouy,Juranville,Bussiere,Laas,Ladon,Lailly,Langesse,Leouville,Ligny,Lombreuil,Lorcy,Lorris,Loury,Louzouer,Malesherbois,Marcilly,Mardie,Mareau,Marigny,Marsainvilliers,Melleroy,Menestreau,Merinville,Messas,Meung,Mezieres,Migneres,Mignerette,Mirabeau,Montargis,Montbarrois,Montbouy,Montcresson,Montereau,Montigny,Montliard,Mormant,Morville,Moulinet,Moulon,Nancray,Nargis,Nesploy,Neuville,Neuvy,Nevoy,Nibelle,Nogent,Noyers,Ocre,Oison,Olivet,Ondreville,Onzerain,Orleans,Ormes,Orville,Oussoy,Outarville,Ouzouer,Pannecieres,Pannes,Patay,Paucourt,Pers,Pierrefitte,Pithiverais,Pithiviers,Poilly,Potier,Prefontaines,Presnoy,Pressigny,Puiseaux,Quiers,Ramoulu,Rebrechien,Rouvray,Rozieres,Rozoy,Ruan,Sandillon,Santeau,Saran,Sceaux,Seichebrieres,Semoy,Sennely,Sermaises,Sigloy,Solterre,Sougy,Sully,Sury,Tavers,Thignonville,Thimory,Thorailles,Thou,Tigy,Tivernon,Tournoisis,Trainou,Treilles,Trigueres,Trinay,Vannes,Varennes,Vennecy,Vieilles,Vienne,Viglain,Vignes,Villamblain,Villemandeur,Villemoutiers,Villemurlin,Villeneuve,Villereau,Villevoques,Villorceau,Vimory,Vitry,Vrigny,Ivre"},
{name: "Italian", i: 3, min: 5, max: 12, d: "cltr", m: .1, b: "Accumoli,Acquafondata,Acquapendente,Acuto,Affile,Agosta,Alatri,Albano,Allumiere,Alvito,Amaseno,Amatrice,Anagni,Anguillara,Anticoli,Antrodoco,Anzio,Aprilia,Aquino,Arce,Arcinazzo,Ardea,Ariccia,Arlena,Arnara,Arpino,Arsoli,Artena,Ascrea,Atina,Ausonia,Bagnoregio,Barbarano,Bassano,Bassiano,Bellegra,Belmonte,Blera,Bolsena,Bomarzo,Borbona,Borgo,Borgorose,Boville,Bracciano,Broccostella,Calcata,Camerata,Campagnano,Campodimele,Campoli,Canale,Canepina,Canino,Cantalice,Cantalupo,Canterano,Capena,Capodimonte,Capranica,Caprarola,Carbognano,Casalattico,Casalvieri,Casape,Casaprota,Casperia,Cassino,Castelforte,Castelliri,Castello,Castelnuovo,Castiglione,Castro,Castrocielo,Cave,Ceccano,Celleno,Cellere,Ceprano,Cerreto,Cervara,Cervaro,Cerveteri,Ciampino,Ciciliano,Cineto,Cisterna,Cittaducale,Cittareale,Civita,Civitavecchia,Civitella,Colfelice,Collalto,Colle,Colleferro,Collegiove,Collepardo,Collevecchio,Colli,Colonna,Concerviano,Configni,Contigliano,Corchiano,Coreno,Cori,Cottanello,Esperia,Fabrica,Faleria,Fara,Farnese,Ferentino,Fiamignano,Fiano,Filacciano,Filettino,Fiuggi,Fiumicino,Fondi,Fontana,Fonte,Fontechiari,Forano,Formello,Formia,Frascati,Frasso,Frosinone,Fumone,Gaeta,Gallese,Gallicano,Gallinaro,Gavignano,Genazzano,Genzano,Gerano,Giuliano,Gorga,Gradoli,Graffignano,Greccio,Grottaferrata,Grotte,Guarcino,Guidonia,Ischia,Isola,Itri,Jenne,Labico,Labro,Ladispoli,Lanuvio,Lariano,Latera,Lenola,Leonessa,Licenza,Longone,Lubriano,Maenza,Magliano,Mandela,Manziana,Marano,Marcellina,Marcetelli,Marino,Marta,Mazzano,Mentana,Micigliano,Minturno,Mompeo,Montalto,Montasola,Monte,Montebuono,Montefiascone,Monteflavio,Montelanico,Monteleone,Montelibretti,Montenero,Monterosi,Monterotondo,Montopoli,Montorio,Moricone,Morlupo,Morolo,Morro,Nazzano,Nemi,Nepi,Nerola,Nespolo,Nettuno,Norma,Olevano,Onano,Oriolo,Orte,Orvinio,Paganico,Palestrina,Paliano,Palombara,Pastena,Patrica,Percile,Pescorocchiano,Pescosolido,Petrella,Piansano,Picinisco,Pico,Piedimonte,Piglio,Pignataro,Pisoniano,Pofi,Poggio,Poli,Pomezia,Pontecorvo,Pontinia,Ponza,Ponzano,Posta,Pozzaglia,Priverno,Proceno,Prossedi,Riano,Rieti,Rignano,Riofreddo,Ripi,Rivodutri,Rocca,Roccagiovine,Roccagorga,Roccantica,Roccasecca,Roiate,Ronciglione,Roviano,Sabaudia,Sacrofano,Salisano,Sambuci,Santa,Santi,Santopadre,Saracinesco,Scandriglia,Segni,Selci,Sermoneta,Serrone,Settefrati,Sezze,Sgurgola,Sonnino,Sora,Soriano,Sperlonga,Spigno,Stimigliano,Strangolagalli,Subiaco,Supino,Sutri,Tarano,Tarquinia,Terelle,Terracina,Tessennano,Tivoli,Toffia,Tolfa,Torre,Torri,Torrice,Torricella,Torrita,Trevi,Trevignano,Trivigliano,Turania,Tuscania,Vacone,Valentano,Vallecorsa,Vallemaio,Vallepietra,Vallerano,Vallerotonda,Vallinfreda,Valmontone,Varco,Vasanello,Vejano,Velletri,Ventotene,Veroli,Vetralla,Vicalvi,Vico,Vicovaro,Vignanello,Viterbo,Viticuso,Vitorchiano,Vivaro,Zagarolo"},
- {name: "Castillian", i: 4, min: 5, max: 11, d: "lr", m: 0, b: "Abanades,Ablanque,Adobes,Ajofrin,Alameda,Alaminos,Alarilla,Albalate,Albares,Albarreal,Albendiego,Alcabon,Alcanizo,Alcaudete,Alcocer,Alcolea,Alcoroches,Aldea,Aldeanueva,Algar,Algora,Alhondiga,Alique,Almadrones,Almendral,Almoguera,Almonacid,Almorox,Alocen,Alovera,Alustante,Angon,Anguita,Anover,Anquela,Arbancon,Arbeteta,Arcicollar,Argecilla,Arges,Armallones,Armuna,Arroyo,Atanzon,Atienza,Aunon,Azuqueca,Azutan,Baides,Banos,Banuelos,Barcience,Bargas,Barriopedro,Belvis,Berninches,Borox,Brihuega,Budia,Buenaventura,Bujalaro,Burguillos,Burujon,Bustares,Cabanas,Cabanillas,Calera,Caleruela,Calzada,Camarena,Campillo,Camunas,Canizar,Canredondo,Cantalojas,Cardiel,Carmena,Carranque,Carriches,Casa,Casarrubios,Casas,Casasbuenas,Caspuenas,Castejon,Castellar,Castilforte,Castillo,Castilnuevo,Cazalegas,Cebolla,Cedillo,Cendejas,Centenera,Cervera,Checa,Chequilla,Chillaron,Chiloeches,Chozas,Chueca,Cifuentes,Cincovillas,Ciruelas,Ciruelos,Cobeja,Cobeta,Cobisa,Cogollor,Cogolludo,Condemios,Congostrina,Consuegra,Copernal,Corduente,Corral,Cuerva,Domingo,Dosbarrios,Driebes,Duron,El,Embid,Erustes,Escalona,Escalonilla,Escamilla,Escariche,Escopete,Espinosa,Espinoso,Esplegares,Esquivias,Estables,Estriegana,Fontanar,Fuembellida,Fuensalida,Fuentelsaz,Gajanejos,Galve,Galvez,Garciotum,Gascuena,Gerindote,Guadamur,Henche,Heras,Herreria,Herreruela,Hijes,Hinojosa,Hita,Hombrados,Hontanar,Hontoba,Horche,Hormigos,Huecas,Huermeces,Huerta,Hueva,Humanes,Illan,Illana,Illescas,Iniestola,Irueste,Jadraque,Jirueque,Lagartera,Las,Layos,Ledanca,Lillo,Lominchar,Loranca,Los,Lucillos,Lupiana,Luzaga,Luzon,Madridejos,Magan,Majaelrayo,Malaga,Malaguilla,Malpica,Mandayona,Mantiel,Manzaneque,Maqueda,Maranchon,Marchamalo,Marjaliza,Marrupe,Mascaraque,Masegoso,Matarrubia,Matillas,Mazarete,Mazuecos,Medranda,Megina,Mejorada,Mentrida,Mesegar,Miedes,Miguel,Millana,Milmarcos,Mirabueno,Miralrio,Mocejon,Mochales,Mohedas,Molina,Monasterio,Mondejar,Montarron,Mora,Moratilla,Morenilla,Muduex,Nambroca,Navalcan,Negredo,Noblejas,Noez,Nombela,Noves,Numancia,Nuno,Ocana,Ocentejo,Olias,Olmeda,Ontigola,Orea,Orgaz,Oropesa,Otero,Palmaces,Palomeque,Pantoja,Pardos,Paredes,Pareja,Parrillas,Pastrana,Pelahustan,Penalen,Penalver,Pepino,Peralejos,Peralveche,Pinilla,Pioz,Piqueras,Polan,Portillo,Poveda,Pozo,Pradena,Prados,Puebla,Puerto,Pulgar,Quer,Quero,Quintanar,Quismondo,Rebollosa,Recas,Renera,Retamoso,Retiendas,Riba,Rielves,Rillo,Riofrio,Robledillo,Robledo,Romanillos,Romanones,Rueda,Sacecorbo,Sacedon,Saelices,Salmeron,San,Santa,Santiuste,Santo,Sartajada,Sauca,Sayaton,Segurilla,Selas,Semillas,Sesena,Setiles,Sevilleja,Sienes,Siguenza,Solanillos,Somolinos,Sonseca,Sotillo,Sotodosos,Talavera,Tamajon,Taragudo,Taravilla,Tartanedo,Tembleque,Tendilla,Terzaga,Tierzo,Tordellego,Tordelrabano,Tordesilos,Torija,Torralba,Torre,Torrecilla,Torrecuadrada,Torrejon,Torremocha,Torrico,Torrijos,Torrubia,Tortola,Tortuera,Tortuero,Totanes,Traid,Trijueque,Trillo,Turleque,Uceda,Ugena,Ujados,Urda,Utande,Valdarachas,Valdesotos,Valhermoso,Valtablado,Valverde,Velada,Viana,Vinuelas,Yebes,Yebra,Yelamos,Yeles,Yepes,Yuncler,Yunclillos,Yuncos,Yunquera,Zaorejas,Zarzuela,Zorita"},
+ {name: "Castillian", i: 4, min: 5, max: 11, d: "lr", m: 0, b: "Abanades,Ablanque,Adobes,Ajofrin,Alameda,Alaminos,Alarilla,Albalate,Albares,Albarreal,Albendiego,Alcabon,Alcanizo,Alcaudete,Alcocer,Alcolea,Alcoroches,Aldea,Aldeanueva,Algar,Algora,Alhondiga,Alique,Almadrones,Almendral,Almoguera,Almonacid,Almorox,Alocen,Alovera,Alustante,Angon,Anguita,Anover,Anquela,Arbancon,Arbeteta,Arcicollar,Argecilla,Arges,Armallones,Armuna,Arroyo,Atanzon,Atienza,Aunon,Azuqueca,Azutan,Baides,Banos,Banuelos,Barcience,Bargas,Barriopedro,Belvis,Berninches,Borox,Brihuega,Budia,Buenaventura,Bujalaro,Burguillos,Burujon,Bustares,Cabanas,Cabanillas,Calera,Caleruela,Calzada,Camarena,Campillo,Camunas,Canizar,Canredondo,Cantalojas,Cardiel,Carmena,Carranque,Carriches,Casa,Casarrubios,Casas,Casasbuenas,Caspuenas,Castejon,Castellar,Castilforte,Castillo,Castilnuevo,Cazalegas,Cebolla,Cedillo,Cendejas,Centenera,Cervera,Checa,Chequilla,Chillaron,Chiloeches,Chozas,Chueca,Cifuentes,Cincovillas,Ciruelas,Ciruelos,Cobeja,Cobeta,Cobisa,Cogollor,Cogolludo,Condemios,Congostrina,Consuegra,Copernal,Corduente,Corral,Cuerva,Domingo,Dosbarrios,Driebes,Duron,El,Embid,Erustes,Escalona,Escalonilla,Escamilla,Escariche,Escopete,Espinosa,Espinoso,Esplegares,Esquivias,Estables,Estriegana,Fontanar,Fuembellida,Fuensalida,Fuentelsaz,Gajanejos,Galve,Galvez,Garciotum,Gascuena,Gerindote,Guadamur,Henche,Heras,Herreria,Herreruela,Hijes,Hinojosa,Hita,Hombrados,Hontanar,Hontoba,Horche,Hormigos,Huecas,Huermeces,Huerta,Hueva,Humanes,Illan,Illana,Illescas,Iniestola,Irueste,Jadraque,Jirueque,Lagartera,Las,Layos,Ledanca,Lillo,Lominchar,Loranca,Los,Lucillos,Lupiana,Luzaga,Luzon,Madridejos,Magan,Majaelrayo,Malaga,Malaguilla,Malpica,Mandayona,Mantiel,Manzaneque,Maqueda,Maranchon,Marchamalo,Marjaliza,Marrupe,Mascaraque,Masegoso,Matarrubia,Matillas,Mazarete,Mazuecos,Medranda,Megina,Mejorada,Mentrida,Mesegar,Miedes,Miguel,Millana,Milmarcos,Mirabueno,Miralrio,Mocejon,Mochales,Mohedas,Molina,Monasterio,Mondejar,Montarron,Mora,Moratilla,Morenilla,Muduex,Nambroca,Navalcan,Negredo,Noblejas,Noez,Nombela,Noves,Numancia,Nuno,Ocana,Ocentejo,Olias,Olmeda,Ontigola,Orea,Orgaz,Oropesa,Otero,Palmaces,Palomeque,Pantoja,Pardos,Paredes,Pareja,Parrillas,Pastrana,Pelahustan,Penalen,Penalver,Pepino,Peralejos,Peralveche,Pinilla,Pioz,Piqueras,Polan,Portillo,Poveda,Pozo,Pradena,Prados,Puebla,Puerto,Pulgar,Quer,Quero,Quintanar,Quismondo,Rebollosa,Recas,Renera,Retamoso,Retiendas,Riba,Rielves,Rillo,Riofrio,Robledillo,Robledo,Romanillos,Romanones,Rueda,Sacecorbo,Sacedon,Saelices,Salmeron,San,Santa,Santiuste,Santo,Sartajada,Sauca,Sayaton,Segurilla,Selas,Semillas,Sesena,Setiles,Sevilleja,Sienes,Siguenza,Solanillos,Somolinos,Sonseca,Sotillo,Sotodasos,Talavera,Tamajon,Taragudo,Taravilla,Tartanedo,Tembleque,Tendilla,Terzaga,Tierzo,Tordellego,Tordelrabano,Tordesilos,Torija,Torralba,Torre,Torrecilla,Torrecuadrada,Torrejon,Torremocha,Torrico,Torrijos,Torrubia,Tortola,Tortuera,Tortuero,Totanes,Traid,Trijueque,Trillo,Turleque,Uceda,Ugena,Ujados,Urda,Utande,Valdarachas,Valdesotos,Valhermoso,Valtablado,Valverde,Velada,Viana,Vinuelas,Yebes,Yebra,Yelamos,Yeles,Yepes,Yuncler,Yunclillos,Yuncos,Yunquera,Zaorejas,Zarzuela,Zorita"},
{name: "Ruthenian", i: 5, min: 5, max: 10, d: "", m: 0, b: "Belgorod,Beloberezhye,Belyi,Belz,Berestiy,Berezhets,Berezovets,Berezutsk,Bobruisk,Bolonets,Borisov,Borovsk,Bozhesk,Bratslav,Bryansk,Brynsk,Buryn,Byhov,Chechersk,Chemesov,Cheremosh,Cherlen,Chern,Chernigov,Chernitsa,Chernobyl,Chernogorod,Chertoryesk,Chetvertnia,Demyansk,Derevesk,Devyagoresk,Dichin,Dmitrov,Dorogobuch,Dorogobuzh,Drestvin,Drokov,Drutsk,Dubechin,Dubichi,Dubki,Dubkov,Dveren,Galich,Glebovo,Glinsk,Goloty,Gomiy,Gorodets,Gorodische,Gorodno,Gorohovets,Goroshin,Gorval,Goryshon,Holm,Horobor,Hoten,Hotin,Hotmyzhsk,Ilovech,Ivan,Izborsk,Izheslavl,Kamenets,Kanev,Karachev,Karna,Kavarna,Klechesk,Klyapech,Kolomyya,Kolyvan,Kopyl,Korec,Kornik,Korochunov,Korshev,Korsun,Koshkin,Kotelno,Kovyla,Kozelsk,Kozelsk,Kremenets,Krichev,Krylatsk,Ksniatin,Kulatsk,Kursk,Kursk,Lebedev,Lida,Logosko,Lomihvost,Loshesk,Loshichi,Lubech,Lubno,Lubutsk,Lutsk,Luchin,Luki,Lukoml,Luzha,Lvov,Mtsensk,Mdin,Medniki,Melecha,Merech,Meretsk,Mescherskoe,Meshkovsk,Metlitsk,Mezetsk,Mglin,Mihailov,Mikitin,Mikulino,Miloslavichi,Mogilev,Mologa,Moreva,Mosalsk,Moschiny,Mozyr,Mstislav,Mstislavets,Muravin,Nemech,Nemiza,Nerinsk,Nichan,Novgorod,Novogorodok,Obolichi,Obolensk,Obolensk,Oleshsk,Olgov,Omelnik,Opoka,Opoki,Oreshek,Orlets,Osechen,Oster,Ostrog,Ostrov,Perelai,Peremil,Peremyshl,Pererov,Peresechen,Perevitsk,Pereyaslav,Pinsk,Ples,Polotsk,Pronsk,Proposhesk,Punia,Putivl,Rechitsa,Rodno,Rogachev,Romanov,Romny,Roslavl,Rostislavl,Rostovets,Rsha,Ruza,Rybchesk,Rylsk,Rzhavesk,Rzhev,Rzhischev,Sambor,Serensk,Serensk,Serpeysk,Shilov,Shuya,Sinech,Sizhka,Skala,Slovensk,Slutsk,Smedin,Sneporod,Snitin,Snovsk,Sochevo,Sokolec,Starica,Starodub,Stepan,Sterzh,Streshin,Sutesk,Svinetsk,Svisloch,Terebovl,Ternov,Teshilov,Teterin,Tiversk,Torchevsk,Toropets,Torzhok,Tripolye,Trubchevsk,Tur,Turov,Usvyaty,Uteshkov,Vasilkov,Velil,Velye,Venev,Venicha,Verderev,Vereya,Veveresk,Viazma,Vidbesk,Vidychev,Voino,Volodimer,Volok,Volyn,Vorobesk,Voronich,Voronok,Vorotynsk,Vrev,Vruchiy,Vselug,Vyatichsk,Vyatka,Vyshegorod,Vyshgorod,Vysokoe,Yagniatin,Yaropolch,Yasenets,Yuryev,Yuryevets,Zaraysk,Zhitomel,Zholvazh,Zizhech,Zubkov,Zudechev,Zvenigorod"},
{name: "Nordic", i: 6, min: 6, max: 10, d: "kln", m: .1, b: "Akureyri,Aldra,Alftanes,Andenes,Austbo,Auvog,Bakkafjordur,Ballangen,Bardal,Beisfjord,Bifrost,Bildudalur,Bjerka,Bjerkvik,Bjorkosen,Bliksvaer,Blokken,Blonduos,Bolga,Bolungarvik,Borg,Borgarnes,Bosmoen,Bostad,Bostrand,Botsvika,Brautarholt,Breiddalsvik,Bringsli,Brunahlid,Budardalur,Byggdakjarni,Dalvik,Djupivogur,Donnes,Drageid,Drangsnes,Egilsstadir,Eiteroga,Elvenes,Engavogen,Ertenvog,Eskifjordur,Evenes,Eyrarbakki,Fagernes,Fallmoen,Fellabaer,Fenes,Finnoya,Fjaer,Fjelldal,Flakstad,Flateyri,Flostrand,Fludir,Gardaber,Gardur,Gimstad,Givaer,Gjeroy,Gladstad,Godoya,Godoynes,Granmoen,Gravdal,Grenivik,Grimsey,Grindavik,Grytting,Hafnir,Halsa,Hauganes,Haugland,Hauknes,Hella,Helland,Hellissandur,Hestad,Higrav,Hnifsdalur,Hofn,Hofsos,Holand,Holar,Holen,Holkestad,Holmavik,Hopen,Hovden,Hrafnagil,Hrisey,Husavik,Husvik,Hvammstangi,Hvanneyri,Hveragerdi,Hvolsvollur,Igeroy,Indre,Inndyr,Innhavet,Innes,Isafjordur,Jarklaustur,Jarnsreykir,Junkerdal,Kaldvog,Kanstad,Karlsoy,Kavosen,Keflavik,Kjelde,Kjerstad,Klakk,Kopasker,Kopavogur,Korgen,Kristnes,Krutoga,Krystad,Kvina,Lande,Laugar,Laugaras,Laugarbakki,Laugarvatn,Laupstad,Leines,Leira,Leiren,Leland,Lenvika,Loding,Lodingen,Lonsbakki,Lopsmarka,Lovund,Luroy,Maela,Melahverfi,Meloy,Mevik,Misvaer,Mornes,Mosfellsber,Moskenes,Myken,Naurstad,Nesberg,Nesjahverfi,Nesset,Nevernes,Obygda,Ofoten,Ogskardet,Okervika,Oknes,Olafsfjordur,Oldervika,Olstad,Onstad,Oppeid,Oresvika,Orsnes,Orsvog,Osmyra,Overdal,Prestoya,Raudalaekur,Raufarhofn,Reipo,Reykholar,Reykholt,Reykjahlid,Rif,Rinoya,Rodoy,Rognan,Rosvika,Rovika,Salhus,Sanden,Sandgerdi,Sandoker,Sandset,Sandvika,Saudarkrokur,Selfoss,Selsoya,Sennesvik,Setso,Siglufjordur,Silvalen,Skagastrond,Skjerstad,Skonland,Skorvogen,Skrova,Sleneset,Snubba,Softing,Solheim,Solheimar,Sorarnoy,Sorfugloy,Sorland,Sormela,Sorvaer,Sovika,Stamsund,Stamsvika,Stave,Stokka,Stokkseyri,Storjord,Storo,Storvika,Strand,Straumen,Strendene,Sudavik,Sudureyri,Sundoya,Sydalen,Thingeyri,Thorlakshofn,Thorshofn,Tjarnabyggd,Tjotta,Tosbotn,Traelnes,Trofors,Trones,Tverro,Ulvsvog,Unnstad,Utskor,Valla,Vandved,Varmahlid,Vassos,Vevelstad,Vidrek,Vik,Vikholmen,Vogar,Vogehamn,Vopnafjordur"},
{name: "Greek", i: 7, min: 5, max: 11, d: "s", m: .1, b: "Abdera,Abila,Abydos,Acanthus,Acharnae,Actium,Adramyttium,Aegae,Aegina,Aegium,Aenus,Agrinion,Aigosthena,Akragas,Akrai,Akrillai,Akroinon,Akrotiri,Alalia,Alexandreia,Alexandretta,Alexandria,Alinda,Amarynthos,Amaseia,Ambracia,Amida,Amisos,Amnisos,Amphicaea,Amphigeneia,Amphipolis,Amphissa,Ankon,Antigona,Antipatrea,Antioch,Antioch,Antiochia,Andros,Apamea,Aphidnae,Apollonia,Argos,Arsuf,Artanes,Artemita,Argyroupoli,Asine,Asklepios,Aspendos,Assus,Astacus,Athenai,Athmonia,Aytos,Ancient,Baris,Bhrytos,Borysthenes,Berge,Boura,Bouthroton,Brauron,Byblos,Byllis,Byzantium,Bythinion,Callipolis,Cebrene,Chalcedon,Calydon,Carystus,Chamaizi,Chalcis,Chersonesos,Chios,Chytri,Clazomenae,Cleonae,Cnidus,Colosse,Corcyra,Croton,Cyme,Cyrene,Cythera,Decelea,Delos,Delphi,Demetrias,Dicaearchia,Dimale,Didyma,Dion,Dioscurias,Dodona,Dorylaion,Dyme,Edessa,Elateia,Eleusis,Eleutherna,Emporion,Ephesus,Ephyra,Epidamnos,Epidauros,Eresos,Eretria,Erythrae,Eubea,Gangra,Gaza,Gela,Golgi,Gonnos,Gorgippia,Gournia,Gortyn,Gythium,Hagios,Hagia,Halicarnassus,Halieis,Helike,Heliopolis,Hellespontos,Helorus,Hemeroskopeion,Heraclea,Hermione,Hermonassa,Hierapetra,Hierapolis,Himera,Histria,Hubla,Hyele,Ialysos,Iasus,Idalium,Imbros,Iolcus,Itanos,Ithaca,Juktas,Kallipolis,Kamares,Kameiros,Kannia,Kamarina,Kasmenai,Katane,Kerkinitida,Kepoi,Kimmerikon,Kios,Klazomenai,Knidos,Knossos,Korinthos,Kos,Kourion,Kume,Kydonia,Kynos,Kyrenia,Lamia,Lampsacus,Laodicea,Lapithos,Larissa,Lato,Laus,Lebena,Lefkada,Lekhaion,Leibethra,Leontinoi,Lepreum,Lessa,Lilaea,Lindus,Lissus,Epizephyrian,Madytos,Magnesia,Mallia,Mantineia,Marathon,Marmara,Maroneia,Masis,Massalia,Megalopolis,Megara,Mesembria,Messene,Metapontum,Methana,Methone,Methumna,Miletos,Misenum,Mochlos,Monastiraki,Morgantina,Mulai,Mukenai,Mylasa,Myndus,Myonia,Myra,Myrmekion,Mutilene,Myos,Nauplios,Naucratis,Naupactus,Naxos,Neapoli,Neapolis,Nemea,Nicaea,Nicopolis,Nirou,Nymphaion,Nysa,Oenoe,Oenus,Odessos,Olbia,Olous,Olympia,Olynthus,Opus,Orchomenus,Oricos,Orestias,Oreus,Oropus,Onchesmos,Pactye,Pagasae,Palaikastro,Pandosia,Panticapaeum,Paphos,Parium,Paros,Parthenope,Patrae,Pavlopetri,Pegai,Pelion,Peiraies,Pella,Percote,Pergamum,Petsofa,Phaistos,Phaleron,Phanagoria,Pharae,Pharnacia,Pharos,Phaselis,Philippi,Pithekussa,Philippopolis,Platanos,Phlius,Pherae,Phocaea,Pinara,Pisa,Pitane,Pitiunt,Pixous,Plataea,Poseidonia,Potidaea,Priapus,Priene,Prousa,Pseira,Psychro,Pteleum,Pydna,Pylos,Pyrgos,Rhamnus,Rhegion,Rhithymna,Rhodes,Rhypes,Rizinia,Salamis,Same,Samos,Scyllaeum,Selinus,Seleucia,Semasus,Sestos,Scidrus,Sicyon,Side,Sidon,Siteia,Sinope,Siris,Sklavokampos,Smyrna,Soli,Sozopolis,Sparta,Stagirus,Stratos,Stymphalos,Sybaris,Surakousai,Taras,Tanagra,Tanais,Tauromenion,Tegea,Temnos,Tenedos,Tenea,Teos,Thapsos,Thassos,Thebai,Theodosia,Therma,Thespiae,Thronion,Thoricus,Thurii,Thyreum,Thyria,Tiruns,Tithoraea,Tomis,Tragurion,Trapeze,Trapezus,Tripolis,Troizen,Troliton,Troy,Tylissos,Tyras,Tyros,Tyritake,Vasiliki,Vathypetros,Zakynthos,Zakros,Zankle"},
@@ -264,7 +278,7 @@ window.Names = (function () {
{name: "Berber", i: 17, min: 4, max: 10, d: "s", m: .2, b: "Abkhouch,Adrar,Agadir,Agelmam,Aghmat,Agrakal,Agulmam,Ahaggar,Almou,Anfa,Annaba,Aousja,Arbat,Argoub,Arif,Asfi,Assamer,Assif,Azaghar,Azmour,Azrou,Beccar,Beja,Bennour,Benslimane,Berkane,Berrechid,Bizerte,Bouskoura,Boutferda,Dar Bouazza,Darallouch,Darchaabane,Dcheira,Denden,Djebel,Djedeida,Drargua,Essaouira,Ezzahra,Fas,Fnideq,Ghezeze,Goubellat,Grisaffen,Guelmim,Guercif,Hammamet,Harrouda,Hoceima,Idurar,Ifendassen,Ifoghas,Imilchil,Inezgane,Izoughar,Jendouba,Kacem,Kelibia,Kenitra,Kerrando,Khalidia,Khemisset,Khenifra,Khouribga,Kidal,Korba,Korbous,Lahraouyine,Larache,Leyun,Lqliaa,Manouba,Martil,Mazagan,Mcherga,Mdiq,Megrine,Mellal,Melloul,Midelt,Mohammedia,Mornag,Mrrakc,Nabeul,Nadhour,Nador,Nawaksut,Nefza,Ouarzazate,Ouazzane,Oued Zem,Oujda,Ouladteima,Qsentina,Rades,Rafraf,Safi,Sefrou,Sejnane,Settat,Sijilmassa,Skhirat,Slimane,Somaa,Sraghna,Susa,Tabarka,Taferka,Tafza,Tagbalut,Tagerdayt,Takelsa,Tanja,Tantan,Taourirt,Taroudant,Tasfelalayt,Tattiwin,Taza,Tazerka,Tazizawt,Tebourba,Teboursouk,Temara,Testour,Tetouan,Tibeskert,Tifelt,Tinariwen,Tinduf,Tinja,Tiznit,Toubkal,Trables,Tubqal,Tunes,Urup,Watlas,Wehran,Wejda,Youssoufia,Zaghouan,Zahret,Zemmour,Zriba"},
{name: "Arabic", i: 18, min: 4, max: 9, d: "ae", m: .2, b: "Abadilah,Abayt,Abha,Abud,Aden,Ahwar,Ajman,Alabadilah,Alabar,Alahjer,Alain,Alaraq,Alarish,Alarjam,Alashraf,Alaswaaq,Alawali,Albarar,Albawadi,Albirk,Aldhabiyah,Alduwaid,Alfareeq,Algayed,Alhada,Alhafirah,Alhamar,Alharam,Alharidhah,Alhawtah,Alhazim,Alhrateem,Alhudaydah,Alhujun,Alhuwaya,Aljahra,Aljohar,Aljubail,Alkawd,Alkhalas,Alkhawaneej,Alkhen,Alkhhafah,Alkhobar,Alkhuznah,Alkiranah,Allisafah,Allith,Almadeed,Almardamah,Almarwah,Almasnaah,Almejammah,Almojermah,Almshaykh,Almurjan,Almuwayh,Almuzaylif,Alnaheem,Alnashifah,Alqadeimah,Alqah,Alqahma,Alqalh,Alqouz,Alquaba,Alqunfudhah,Alqurayyat,Alradha,Alraqmiah,Alsadyah,Alsafa,Alshagab,Alshoqiq,Alshuqaiq,Alsilaa,Althafeer,Alwakrah,Alwasqah,Amaq,Amran,Annaseem,Aqbiyah,Arafat,Arar,Ardah,Arrawdah,Asfan,Ashayrah,Ashshahaniyah,Askar,Assaffaniyah,Ayaar,Aziziyah,Baesh,Bahrah,Baish,Balhaf,Banizayd,Baqaa,Baqal,Bidiyah,Bisha,Biyatah,Buqhayq,Burayda,Dafiyat,Damad,Dammam,Dariyah,Daynah,Dhafar,Dhahran,Dhalkut,Dhamar,Dhubab,Dhurma,Dibab,Dirab,Doha,Dukhan,Duwaibah,Enaker,Fadhla,Fahaheel,Fanateer,Farasan,Fardah,Fujairah,Ghalilah,Ghar,Ghizlan,Ghomgyah,Ghran,Hababah,Habil,Hadiyah,Haffah,Hajanbah,Hajrah,Halban,Haqqaq,Haradh,Hasar,Hathah,Hawarwar,Hawaya,Hawiyah,Hebaa,Hefar,Hijal,Husnah,Huwailat,Huwaitah,Irqah,Isharah,Ithrah,Jamalah,Jarab,Jareef,Jarwal,Jash,Jazan,Jeddah,Jiblah,Jihanah,Jilah,Jizan,Joha,Joraibah,Juban,Jubbah,Juddah,Jumeirah,Kamaran,Keyad,Khab,Khabtsaeed,Khaiybar,Khasab,Khathirah,Khawarah,Khulais,Khulays,Klayah,Kumzar,Limah,Linah,Mabar,Madrak,Mahab,Mahalah,Makhtar,Makshosh,Manfuhah,Manifah,Manshabah,Mareah,Masdar,Mashwar,Masirah,Maskar,Masliyah,Mastabah,Maysaan,Mazhar,Mdina,Meeqat,Mirbah,Mirbat,Mokhtara,Muharraq,Muladdah,Musandam,Musaykah,Muscat,Mushayrif,Musrah,Mussafah,Mutrah,Nafhan,Nahdah,Nahwa,Najran,Nakhab,Nizwa,Oman,Qadah,Qalhat,Qamrah,Qasam,Qatabah,Qawah,Qosmah,Qurain,Quraydah,Quriyat,Qurwa,Rabigh,Radaa,Rafha,Rahlah,Rakamah,Rasheedah,Rasmadrakah,Risabah,Rustaq,Ryadh,Saabah,Saabar,Sabtaljarah,Sabya,Sadad,Sadah,Safinah,Saham,Sahlat,Saihat,Salalah,Salmalzwaher,Salmiya,Sanaa,Sanaban,Sayaa,Sayyan,Shabayah,Shabwah,Shafa,Shalim,Shaqra,Sharjah,Sharkat,Sharurah,Shatifiyah,Shibam,Shidah,Shifiyah,Shihar,Shoqra,Shoqsan,Shuwaq,Sibah,Sihmah,Sinaw,Sirwah,Sohar,Suhailah,Sulaibiya,Sunbah,Tabuk,Taif,Taqah,Tarif,Tharban,Thumrait,Thuqbah,Thuwal,Tubarjal,Turaif,Turbah,Tuwaiq,Ubar,Umaljerem,Urayarah,Urwah,Wabrah,Warbah,Yabreen,Yadamah,Yafur,Yarim,Yemen,Yiyallah,Zabid,Zahwah,Zallaq,Zinjibar,Zulumah"},
{name: "Inuit", i: 19, min: 5, max: 15, d: "alutsn", m: 0, b: "Aaluik,Aappilattoq,Aasiaat,Agdleruussakasit,Aggas,Akia,Akilia,Akuliaruseq,Akuliarutsip,Akunnaaq,Agissat,Agssaussat,Alluitsup,Alluttoq,Aluit,Aluk,Ammassalik,Amarortalik,Amitsorsuaq,Anarusuk,Angisorsuaq,Anguniartarfik,Annertussoq,Annikitsoq,Anoraliuirsoq,Appat,Apparsuit,Apusiaajik,Arsivik,Arsuk,Ataa,Atammik,Ateqanngitsorsuaq,Atilissuaq,Attu,Aukarnersuaq,Augpalugtoq, Aumat,Auvilikavsak,Auvilkikavsaup,Avadtlek,Avallersuaq,Bjornesk,Blabaerdalen,Blomsterdalen,Brattalhid,Bredebrae,Brededal,Claushavn,Edderfulegoer,Egger,Eqalugalinnguit,Eqalugarssuit,Eqaluit,Eqqua,Etah,Graah,Hakluyt,Haredalen,Hareoen,Hundeo,Igdlorssuit,Igaliku,Igdlugdlip,Igdluluarssuk,Iginniafik,Ikamiuk,Ikamiut,Ikarissat,Ikateq,Ikeq,Ikerasak,Ikerasaarsuk,Ikermiut,Ikermoissuaq,Ikertivaq,Ikorfarssuit,Ikorfat,Ilimanaq,Illorsuit,Iluileq,Iluiteq,Ilulissat,Illunnguit,Imaarsivik,Imartunarssuk,Immikkoortukajik,Innaarsuit,Ingjald,Inneruulalik,Inussullissuaq,Iqek,Ikerasakassak,Iperaq,Ippik,Isortok,Isungartussoq,Itileq,Itivdleq,Itissaalik,Ittit,Ittoqqortoormiit,Ivingmiut,Ivittuut,Kanajoorartuut,Kangaamiut,Kangaarsuk,Kangaatsiaq,Kangeq,Kangerluk,Kangerlussuaq,Kanglinnguit,Kapisillit,Karrat,Kekertamiut,Kiatak,Kiatassuaq,Kiataussaq,Kigatak,Kigdlussat,Kinaussak,Kingittorsuaq,Kitak,Kitsissuarsuit,Kitsissut,Klenczner,Kook,Kraulshavn,Kujalleq,Kullorsuaq,Kulusuk,Kuurmiit,Kuusuaq,Laksedalen,Maniitsoq,Marrakajik,Mattaangassut,Mernoq,Mittivakkat,Moriusaq,Myggbukta,Naajaat,Nako,Nangissat,Nanortalik,Nanuuseq,Nappassoq,Narsarmijt,Narssaq,Narsarsuaq,Narssarssuk,Nasaussaq,Nasiffik,Natsiarsiorfik,Naujanguit,Niaqornaarsuk,Niaqornat,Nordfjordspasset,Nugatsiaq,Nuluuk,Nunaa,Nunarssit,Nunarsuaq,Nunataaq,Nunatakavsaup,Nutaarmiut,Nuugaatsiaq,Nuuk,Nuukullak,Nuuluk,Nuussuaq,Olonkinbyen,Oqaatsut,Oqaitsúnguit,Oqonermiut,Oodaaq,Paagussat,Palungataq,Pamialluk,Paamiut,Paatuut,Patuersoq,Perserajoq,Paornivik,Pituffik,Puugutaa,Puulkuip,Qaanaq,Qaarsorsuaq,Qaarsorsuatsiaq,Qaasuitsup,Qaersut,Qajartalik,Qallunaat,Qaneq,Qaqaarissorsuaq,Qaqit,Qaqortok,Qasigiannguit,Qasse,Qassimiut,Qeertartivaq,Qeertartivatsiaq,Qeqertaq,Qeqertarssdaq,Qeqertarsuaq,Qeqertasussuk,Qeqertarsuatsiaat,Qeqertat,Qeqqata,Qernertoq,Qernertunnguit,Qianarreq,Qilalugkiarfik,Qingagssat,Qingaq,Qoornuup,Qorlortorsuaq,Qullikorsuit,Qunnerit,Qutdleq,Ravnedalen,Ritenbenk,Rypedalen,Sarfannguit,Saarlia,Saarloq,Saatoq,Saatorsuaq,Saatup,Saattut,Sadeloe,Salleq,Salliaruseq,Sammeqqat,Sammisoq,Sanningassoq,Saqqaq,Saqqarlersuaq,Saqqarliit,Sarqaq,Sattiaatteq,Savissivik,Serfanguaq,Sermersooq,Sermersut,Sermilik,Sermiligaaq,Sermitsiaq,Simitakaja,Simiutaq,Singamaq,Siorapaluk,Sisimiut,Sisuarsuit,Skal,Skarvefjeld,Skjoldungen,Storoen,Sullorsuaq,Suunikajik,Sverdrup,Taartoq,Takiseeq,Talerua,Tarqo,Tasirliaq,Tasiusak,Tiilerilaaq,Timilersua,Timmiarmiut,Tingmjarmiut,Traill,Tukingassoq,Tuttorqortooq,Tuujuk,Tuttulissuup,Tussaaq,Uigordlit,Uigorlersuaq,Uilortussoq,Uiivaq,Ujuaakajiip,Ukkusissat,Umanat,Upernavik,Upernattivik,Upepnagssivik,Upernivik,Uttorsiutit,Uumannaq,Uummannaarsuk,Uunartoq,Uvkusigssat,Ymer"},
- {name: "Basque", i: 20, min: 4, max: 11, d: "r", m: .1, b: "Abadio,Abaltzisketa,Abanto Zierbena,Aduna,Agurain,Aia,Aiara,Aizarnazabal,Ajangiz,Albiztur,Alegia,Alkiza,Alonsotegi,Altzaga,Altzo,Amezketa,Amorebieta,Amoroto,Amurrio,Andoain,Anoeta,Antzuola,Arakaldo,Arama,Aramaio,Arantzazu,Arbatzegi ,Areatza,Aretxabaleta,Arraia,Arrankudiaga,Arrasate,Arratzu,Arratzua,Arrieta,Arrigorriaga,Artea,Artzentales,Artziniega,Asparrena,Asteasu,Astigarraga,Ataun,Atxondo,Aulesti,Azkoitia,Azpeitia,Bakio,Baliarrain,Balmaseda,Barakaldo,Barrika,Barrundia,Basauri,Bastida,Beasain,Bedia,Beizama,Belauntza,Berango,Berantevilla,Berastegi,Bergara,Bermeo,Bernedo,Berriatua,Berriz,Berrobi,Bidania,Bilar,Bilbao,Burgelu,Busturia,Deba,Derio,Dima,Donemiliaga,Donostia,Dulantzi,Durango,Ea,Eibar,Elantxobe,Elduain,Elgeta,Elgoibar,Elorrio,Erandio,Ere-o,Ermua,Errenteria,Errezil,Erribera Beitia,Erriberagoitia,Errigoiti,Eskoriatza,Eskuernaga,Etxebarri,Etxebarria,Ezkio,Fika,Forua,Fruiz,Gabiria,Gaintza,Galdakao,Galdames,Gamiz,Garai,Gasteiz,Gatika,Gatzaga,Gaubea,Gauna,Gautegiz Arteaga,Gaztelu,Gernika,Gerrikaitz,Getaria,Getxo,Gizaburuaga,Goiatz,Gordexola,Gorliz,Harana,Hernani,Hernialde,Hondarribia,Ibarra,Ibarrangelu,Idiazabal,Iekora,Igorre,Ikaztegieta,Iru-a Oka,Irun,Irura,Iruraiz,Ispaster,Itsaso,Itsasondo,Iurreta,Izurtza,Jatabe,Kanpezu,Karrantza Harana,Kortezubi,Kripan,Kuartango,Lanestosa,Lantziego,Larrabetzu,Larraul,Lasarte,Laudio,Laukiz,Lazkao,Leaburu,Legazpi,Legorreta,Legutio,Leintz,Leioa,Lekeitio,Lemoa,Lemoiz,Leza,Lezama,Lezo,Lizartza,Loiu,Lumo,Ma-aria,Maeztu,Mallabia,Markina,Maruri,Ma-ueta,Me-aka,Mendaro,Mendata,Mendexa,Moreda Araba,Morga,Mundaka,Mungia,Munitibar,Murueta,Muskiz,Mutiloa,Mutriku,Muxika,Nabarniz,O-ati,Oiartzun,Oion,Okondo,Olaberria,Ondarroa,Ordizia,Orendain,Orexa,Oria,Orio,Ormaiztegi,Orozko,Ortuella,Otxandio,Pasaia,Plentzia,Portugalete,Samaniego,Santurtzi,Segura,Sestao,Sondika,Sopela,Sopuerta,Soraluze,Sukarrieta,Tolosa,Trapagaran,Turtzioz,Ubarrundia,Ubide,Ugao,Urdua,Urduliz,Urizaharra,Urkabustaiz,Urnieta,Urretxu,Usurbil,Xemein,Zaia,Zaldibar,Zaldibia,Zalduondo,Zambrana,Zamudio,Zaratamo,Zarautz,Zeanuri,Zeberio,Zegama,Zerain,Zestoa,Zierbena,Zigoitia,Ziortza,Zizurkil,Zuia,Zumaia,Zumarraga"},
+ {name: "Basque", i: 20, min: 4, max: 11, d: "r", m: .1, b: "Abadio,Abaltzisketa,Abanto Zierbena,Aduna,Agurain,Aia,Aiara,Aizarnazabal,Ajangiz,Albiztur,Alegia,Alkiza,Alonsotegi,Altzaga,Altzo,Amezketa,Amorebieta,Amoroto,Amurrio,Andoain,Anoeta,Antzuola,Arakaldo,Arama,Aramaio,Arantzazu,Arbatzegi ,Areatza,Aretxabaleta,Arraia,Arrankudiaga,Arrasate,Arratzu,Arratzua,Arrieta,Arrigorriaga,Artea,Artzentales,Artziniega,Asparrena,Asteasu,Astigarraga,Ataun,Atxondo,Aulesti,Azkoitia,Azpeitia,Bakio,Baliarrain,Balmaseda,Barakaldo,Barrika,Barrundia,Basauri,Bastida,Beasain,Bedia,Beizama,Belauntza,Berango,Berantevilla,Berastegi,Bergara,Bermeo,Bernedo,Berriatua,Berriz,Berrobi,Bidania,Bilar,Bilbao,Burgelu,Busturia,Deba,Derio,Dima,Donemiliaga,Donostia,Dulantzi,Durango,Ea,Eibar,Elantxobe,Elduain,Elgeta,Elgoibar,Elorrio,Erandio,Ereno,Ermua,Errenteria,Errezil,Erribera Beitia,Erriberagoitia,Errigoiti,Eskoriatza,Eskuernaga,Etxebarri,Etxebarria,Ezkio,Fika,Forua,Fruiz,Gabiria,Gaintza,Galdakao,Galdames,Gamiz,Garai,Gasteiz,Gatika,Gatzaga,Gaubea,Gauna,Gautegiz Arteaga,Gaztelu,Gernika,Gerrikaitz,Getaria,Getxo,Gizaburuaga,Goiatz,Gordexola,Gorliz,Harana,Hernani,Hernialde,Hondarribia,Ibarra,Ibarrangelu,Idiazabal,Iekora,Igorre,Ikaztegieta,Iruna Oka,Irun,Irura,Iruraiz,Ispaster,Itsaso,Itsasondo,Iurreta,Izurtza,Jatabe,Kanpezu,Karrantza Harana,Kortezubi,Kripan,Kuartango,Lanestosa,Lantziego,Larrabetzu,Larraul,Lasarte,Laudio,Laukiz,Lazkao,Leaburu,Legazpi,Legorreta,Legutio,Leintz,Leioa,Lekeitio,Lemoa,Lemoiz,Leza,Lezama,Lezo,Lizartza,Loiu,Lumo,Manaria,Maeztu,Mallabia,Markina,Maruri,Manueta,Menaka,Mendaro,Mendata,Mendexa,Moreda Araba,Morga,Mundaka,Mungia,Munitibar,Murueta,Muskiz,Mutiloa,Mutriku,Muxika,Nabarniz,Onati,Oiartzun,Oion,Okondo,Olaberria,Ondarroa,Ordizia,Orendain,Orexa,Oria,Orio,Ormaiztegi,Orozko,Ortuella,Otxandio,Pasaia,Plentzia,Portugalete,Samaniego,Santurtzi,Segura,Sestao,Sondika,Sopela,Sopuerta,Soraluze,Sukarrieta,Tolosa,Trapagaran,Turtzioz,Ubarrundia,Ubide,Ugao,Urdua,Urduliz,Urizaharra,Urkabustaiz,Urnieta,Urretxu,Usurbil,Xemein,Zaia,Zaldibar,Zaldibia,Zalduondo,Zambrana,Zamudio,Zaratamo,Zarautz,Zeanuri,Zeberio,Zegama,Zerain,Zestoa,Zierbena,Zigoitia,Ziortza,Zizurkil,Zuia,Zumaia,Zumarraga"},
{name: "Nigerian", i: 21, min: 4, max: 10, d: "", m: .3, b: "Abadogo,Abafon,Abdu,Acharu,Adaba,Adealesu,Adeto,Adyongo,Afaga,Afamju,Afuje,Agbelagba,Agigbigi,Agogoke,Ahute,Aiyelaboro,Ajebe,Ajola,Akarekwu,Akessan,Akunuba,Alawode,Alkaijji,Amangam,Amaoji,Amgbaye,Amtasa,Amunigun,Anase,Aniho,Animahun,Antul,Anyoko,Apekaa,Arapagi,Asamagidi,Asande,Ataibang,Awgbagba,Awhum,Awodu,Babanana,Babateduwa,Bagu,Bakura,Bandakwai,Bangdi,Barbo,Barkeje,Basa,Basabra,Basansagawa,Bieleshin,Bilikani,Birnindodo,Braidu,Bulakawa,Buriburi,Burisidna,Busum,Bwoi,Cainnan,Chakum,Charati,Chondugh,Dabibikiri,Dagwarga,Dallok,Danalili,Dandala,Darpi,Dhayaki,Dokatofa,Doma,Dozere,Duci,Dugan,Ebelibri,Efem,Efoi,Egudu,Egundugbo,Ekoku,Ekpe,Ekwere,Erhua,Eteu,Etikagbene,Ewhoeviri,Ewhotie,Ezemaowa,Fatima,Gadege,Galakura,Galea,Gamai,Gamen,Ganjin,Gantetudu,Garangamawa,Garema,Gargar,Gari,Garinbode,Garkuwa,Garu Kime,Gazabu,Gbure,Gerti,Gidan,Giringwe,Gitabaremu,Giyagiri,Giyawa,Gmawa,Golakochi,Golumba,Guchi,Gudugu,Gunji,Gusa,Gwambula,Gwamgwam,Gwodoti,Hayinlere,Hayinmaialewa,Hirishi,Hombo,Ibefum,Iberekodo,Ibodeipa,Icharge,Ideoro,Idofin,Idofinoka,Idya,Iganmeji,Igbetar,Igbogo,Ijoko,Ijuwa,Ikawga,Ikekogbe,Ikhin,Ikoro,Ikotefe,Ikotokpora,Ikpakidout,Ikpeoniong,Ilofa,Imuogo,Inyeneke,Iorsugh,Ipawo,Ipinlerere,Isicha,Itakpa,Itoki,Iyedeame,Jameri,Jangi,Jara,Jare,Jataudakum,Jaurogomki,Jepel,Jibam,Jirgu,Jirkange,Kafinmalama,Kamkem,Katab,Katanga,Katinda,Katirije,Kaurakimba,Keffinshanu,Kellumiri,Kiagbodor,Kibiare,Kingking,Kirbutu,Kita,Kogbo,Kogogo,Kopje,Koriga,Koroko,Korokorosei,Kotoku,Kuata,Kujum,Kukau,Kunboon,Kuonubogbene,Kurawe,Kushinahu,Kwaramakeri,Ladimeji,Lafiaro,Lahaga,Laindebajanle,Laindegoro,Lajere,Lakati,Ligeri,Litenswa,Lokobimagaji,Lusabe,Maba,Madarzai,Magoi,Maialewa,Maianita,Maijuja,Mairakuni,Maleh,Malikansaa,Mallamkola,Mallammaduri,Marmara,Masagu,Masoma,Mata,Matankali,Mbalare,Megoyo,Meku,Miama,Mige,Mkporagwu,Modi,Molafa,Mshi,Msugh,Muduvu,Murnachehu,Namnai,Nanumawa,Nasudu,Ndagawo,Ndamanma,Ndiebeleagu,Ndiwulunbe,Ndonutim,Ngaruwa,Ngbande,Nguengu,Nto Ekpe,Nubudi,Nyajo,Nyido,Nyior,Obafor,Obazuwa,Odajie,Odiama,Ofunatam,Ogali,Ogan,Ogbaga,Ogbahu,Ogultu,Ogunbunmi,Ogunmakin,Ojaota,Ojirami,Ojopode,Okehin,Olugunna,Omotunde,Onipede,Onisopi,Onma,Orhere,Orya,Oshotan,Otukwang,Otunade,Pepegbene,Poros,Rafin,Rampa,Rimi,Rinjim,Robertkiri,Rugan,Rumbukawa,Sabiu,Sabon,Sabongari,Sai,Salmatappare,Sangabama,Sarabe,Seboregetore,Seibiri,Sendowa,Shafar,Shagwa,Shata,Shefunda,Shengu,Sokoron,Sunnayu,Taberlma,Tafoki,Takula,Talontan,Taraku,Tarhemba,Tayu,Ter,Timtim,Timyam,Tindirke,Tirkalou,Tokunbo,Tonga,Torlwam,Tseakaadza,Tseanongo,Tseavungu,Tsebeeve,Tsekov,Tsepaegh,Tuba,Tumbo,Tungalombo,Tungamasu,Tunganrati,Tunganyakwe,Tungenzuri,Ubimimi,Uhkirhi,Umoru,Umuabai,Umuaja,Umuajuju,Umuimo,Umuojala,Unchida,Ungua,Unguwar,Unongo,Usha,Ute,Utongbo,Vembera,Vorokotok,Wachin,Walebaga,Wurawura,Wuro,Yanbashi,Yanmedi,Yenaka,Yoku,Zamangera,Zarunkwari,Zilumo,Zulika"},
{name: "Celtic", i: 22, min: 4, max: 12, d: "nld", m: 0, b: "Aberaman,Aberangell,Aberarth,Aberavon,Aberbanc,Aberbargoed,Aberbeeg,Abercanaid,Abercarn,Abercastle,Abercegir,Abercraf,Abercregan,Abercych,Abercynon,Aberdare,Aberdaron,Aberdaugleddau,Aberdeen,Aberdulais,Aberdyfi,Aberedw,Abereiddy,Abererch,Abereron,Aberfan,Aberffraw,Aberffrwd,Abergavenny,Abergele,Aberglasslyn,Abergorlech,Abergwaun,Abergwesyn,Abergwili,Abergwynfi,Abergwyngregyn,Abergynolwyn,Aberhafesp,Aberhonddu,Aberkenfig,Aberllefenni,Abermain,Abermaw,Abermorddu,Abermule,Abernant,Aberpennar,Aberporth,Aberriw,Abersoch,Abersychan,Abertawe,Aberteifi,Aberthin,Abertillery,Abertridwr,Aberystwyth,Achininver,Afonhafren,Alisaha,Antinbhearmor,Ardenna,Attacon,Beira,Bhrura,Boioduro,Bona,Boudobriga,Bravon,Brigant,Briganta,Briva,Cambodunum,Cambra,Caracta,Catumagos,Centobriga,Ceredigion,Chalain,Dinn,Diwa,Dubingen,Duro,Ebora,Ebruac,Eburodunum,Eccles,Eighe,Eireann,Ferkunos,Genua,Ghrainnse,Inbhear,Inbhir,Inbhirair,Innerleithen,Innerleven,Innerwick,Inver,Inveraldie,Inverallan,Inveralmond,Inveramsay,Inveran,Inveraray,Inverarnan,Inverbervie,Inverclyde,Inverell,Inveresk,Inverfarigaig,Invergarry,Invergordon,Invergowrie,Inverhaddon,Inverkeilor,Inverkeithing,Inverkeithney,Inverkip,Inverleigh,Inverleith,Inverloch,Inverlochlarig,Inverlochy,Invermay,Invermoriston,Inverness,Inveroran,Invershin,Inversnaid,Invertrossachs,Inverugie,Inveruglas,Inverurie,Kilninver,Kirkcaldy,Kirkintilloch,Krake,Latense,Leming,Lindomagos,Llanaber,Lochinver,Lugduno,Magoduro,Monmouthshire,Narann,Novioduno,Nowijonago,Octoduron,Penning,Pheofharain,Ricomago,Rossinver,Salodurum,Seguia,Sentica,Theorsa,Uige,Vitodurum,Windobona"},
{name: "Mesopotamian", i: 23, min: 4, max: 9, d: "srpl", m: .1, b: "Adab,Akkad,Akshak,Amnanum,Arbid,Arpachiyah,Arrapha,Assur,Babilim,Badtibira,Balawat,Barsip,Borsippa,Carchemish,Chagar Bazar,Chuera,Ctesiphon ,Der,Dilbat,Diniktum,Doura,Durkurigalzu,Ekallatum,Emar,Erbil,Eridu,Eshnunn,Fakhariya ,Gawra,Girsu,Hadatu,Hamoukar,Haradum,Harran,Hatra,Idu,Irisagrig,Isin,Jemdet,Kahat,Kartukulti,Khaiber,Kish ,Kisurra,Kuara,Kutha,Lagash,Larsa ,Leilan,Marad,Mardaman,Mari,Mashkan,Mumbaqat ,Nabada,Nagar,Nerebtum,Nimrud,Nineveh,Nippur,Nuzi,Qalatjarmo,Qatara,Rawda,Seleucia,Shaduppum,Shanidar,Sharrukin,Shemshara,Shibaniba,Shuruppak,Sippar,Tarbisu,Tellagrab,Tellessawwan,Tellessweyhat,Tellhassuna,Telltaya,Telul,Terqa,Thalathat,Tutub,Ubaid ,Umma,Ur,Urfa,Urkesh,Uruk,Urum,Zabalam,Zenobia"},
diff --git a/modules/relief-icons.js b/modules/relief-icons.js
index 3cb4fd84..498886df 100644
--- a/modules/relief-icons.js
+++ b/modules/relief-icons.js
@@ -52,7 +52,7 @@ window.ReliefIcons = (function () {
function getReliefIcon(i, h) {
const temp = grid.cells.temp[pack.cells.g[i]];
const type = h > 70 && temp < 0 ? "mountSnow" : h > 70 ? "mount" : "hill";
- const size = h > 70 ? (h - 45) * mod : Math.min(Math.max((h - 40) * mod, 3), 6);
+ const size = h > 70 ? (h - 45) * mod : minmax((h - 40) * mod, 3, 6);
return [getIcon(type), size];
}
}
diff --git a/modules/religions-generator.js b/modules/religions-generator.js
index f5d63573..b0b0dae2 100644
--- a/modules/religions-generator.js
+++ b/modules/religions-generator.js
@@ -2,7 +2,22 @@
window.Religions = (function () {
// name generation approach and relative chance to be selected
- const approach = {Number: 1, Being: 3, Adjective: 5, "Color + Animal": 5, "Adjective + Animal": 5, "Adjective + Being": 5, "Adjective + Genitive": 1, "Color + Being": 3, "Color + Genitive": 3, "Being + of + Genitive": 2, "Being + of the + Genitive": 1, "Animal + of + Genitive": 1, "Adjective + Being + of + Genitive": 2, "Adjective + Animal + of + Genitive": 2};
+ const approach = {
+ Number: 1,
+ Being: 3,
+ Adjective: 5,
+ "Color + Animal": 5,
+ "Adjective + Animal": 5,
+ "Adjective + Being": 5,
+ "Adjective + Genitive": 1,
+ "Color + Being": 3,
+ "Color + Genitive": 3,
+ "Being + of + Genitive": 2,
+ "Being + of the + Genitive": 1,
+ "Animal + of + Genitive": 1,
+ "Adjective + Being + of + Genitive": 2,
+ "Adjective + Animal + of + Genitive": 2
+ };
// turn weighted array into simple array
const approaches = [];
@@ -14,11 +29,254 @@ window.Religions = (function () {
const base = {
number: ["One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve"],
- being: ["God", "Goddess", "Lord", "Lady", "Deity", "Creator", "Maker", "Overlord", "Ruler", "Chief", "Master", "Spirit", "Ancestor", "Father", "Forebear", "Forefather", "Mother", "Brother", "Sister", "Elder", "Numen", "Ancient", "Virgin", "Giver", "Council", "Guardian", "Reaper"],
- animal: ["Dragon", "Wyvern", "Phoenix", "Unicorn", "Sphinx", "Centaur", "Pegasus", "Kraken", "Basilisk", "Chimera", "Cyclope", "Antelope", "Ape", "Badger", "Bear", "Beaver", "Bison", "Boar", "Buffalo", "Cat", "Cobra", "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", "Viper", "Vulture", "Walrus", "Wolf", "Wolverine", "Worm", "Camel", "Falcon", "Hound", "Ox", "Serpent"],
- adjective: ["New", "Good", "High", "Old", "Great", "Big", "Young", "Major", "Strong", "Happy", "Last", "Main", "Huge", "Far", "Beautiful", "Wild", "Fair", "Prime", "Crazy", "Ancient", "Proud", "Secret", "Lucky", "Sad", "Silent", "Latter", "Severe", "Fat", "Holy", "Pure", "Aggressive", "Honest", "Giant", "Mad", "Pregnant", "Distant", "Lost", "Broken", "Blind", "Friendly", "Unknown", "Sleeping", "Slumbering", "Loud", "Hungry", "Wise", "Worried", "Sacred", "Magical", "Superior", "Patient", "Dead", "Deadly", "Peaceful", "Grateful", "Frozen", "Evil", "Scary", "Burning", "Divine", "Bloody", "Dying", "Waking", "Brutal", "Unhappy", "Calm", "Cruel", "Favorable", "Blond", "Explicit", "Disturbing", "Devastating", "Brave", "Sunny", "Troubled", "Flying", "Sustainable", "Marine", "Fatal", "Inherent", "Selected", "Naval", "Cheerful", "Almighty", "Benevolent", "Eternal", "Immutable", "Infallible"],
- genitive: ["Day", "Life", "Death", "Night", "Home", "Fog", "Snow", "Winter", "Summer", "Cold", "Springs", "Gates", "Nature", "Thunder", "Lightning", "War", "Ice", "Frost", "Fire", "Doom", "Fate", "Pain", "Heaven", "Justice", "Light", "Love", "Time", "Victory"],
- theGenitive: ["World", "Word", "South", "West", "North", "East", "Sun", "Moon", "Peak", "Fall", "Dawn", "Eclipse", "Abyss", "Blood", "Tree", "Earth", "Harvest", "Rainbow", "Sea", "Sky", "Stars", "Storm", "Underworld", "Wild"],
+ being: [
+ "God",
+ "Goddess",
+ "Lord",
+ "Lady",
+ "Deity",
+ "Creator",
+ "Maker",
+ "Overlord",
+ "Ruler",
+ "Chief",
+ "Master",
+ "Spirit",
+ "Ancestor",
+ "Father",
+ "Forebear",
+ "Forefather",
+ "Mother",
+ "Brother",
+ "Sister",
+ "Elder",
+ "Numen",
+ "Ancient",
+ "Virgin",
+ "Giver",
+ "Council",
+ "Guardian",
+ "Reaper"
+ ],
+ animal: [
+ "Dragon",
+ "Wyvern",
+ "Phoenix",
+ "Unicorn",
+ "Sphinx",
+ "Centaur",
+ "Pegasus",
+ "Kraken",
+ "Basilisk",
+ "Chimera",
+ "Cyclope",
+ "Antelope",
+ "Ape",
+ "Badger",
+ "Bear",
+ "Beaver",
+ "Bison",
+ "Boar",
+ "Buffalo",
+ "Cat",
+ "Cobra",
+ "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",
+ "Viper",
+ "Vulture",
+ "Walrus",
+ "Wolf",
+ "Wolverine",
+ "Worm",
+ "Camel",
+ "Falcon",
+ "Hound",
+ "Ox",
+ "Serpent"
+ ],
+ adjective: [
+ "New",
+ "Good",
+ "High",
+ "Old",
+ "Great",
+ "Big",
+ "Young",
+ "Major",
+ "Strong",
+ "Happy",
+ "Last",
+ "Main",
+ "Huge",
+ "Far",
+ "Beautiful",
+ "Wild",
+ "Fair",
+ "Prime",
+ "Crazy",
+ "Ancient",
+ "Proud",
+ "Secret",
+ "Lucky",
+ "Sad",
+ "Silent",
+ "Latter",
+ "Severe",
+ "Fat",
+ "Holy",
+ "Pure",
+ "Aggressive",
+ "Honest",
+ "Giant",
+ "Mad",
+ "Pregnant",
+ "Distant",
+ "Lost",
+ "Broken",
+ "Blind",
+ "Friendly",
+ "Unknown",
+ "Sleeping",
+ "Slumbering",
+ "Loud",
+ "Hungry",
+ "Wise",
+ "Worried",
+ "Sacred",
+ "Magical",
+ "Superior",
+ "Patient",
+ "Dead",
+ "Deadly",
+ "Peaceful",
+ "Grateful",
+ "Frozen",
+ "Evil",
+ "Scary",
+ "Burning",
+ "Divine",
+ "Bloody",
+ "Dying",
+ "Waking",
+ "Brutal",
+ "Unhappy",
+ "Calm",
+ "Cruel",
+ "Favorable",
+ "Blond",
+ "Explicit",
+ "Disturbing",
+ "Devastating",
+ "Brave",
+ "Sunny",
+ "Troubled",
+ "Flying",
+ "Sustainable",
+ "Marine",
+ "Fatal",
+ "Inherent",
+ "Selected",
+ "Naval",
+ "Cheerful",
+ "Almighty",
+ "Benevolent",
+ "Eternal",
+ "Immutable",
+ "Infallible"
+ ],
+ genitive: [
+ "Day",
+ "Life",
+ "Death",
+ "Night",
+ "Home",
+ "Fog",
+ "Snow",
+ "Winter",
+ "Summer",
+ "Cold",
+ "Springs",
+ "Gates",
+ "Nature",
+ "Thunder",
+ "Lightning",
+ "War",
+ "Ice",
+ "Frost",
+ "Fire",
+ "Doom",
+ "Fate",
+ "Pain",
+ "Heaven",
+ "Justice",
+ "Light",
+ "Love",
+ "Time",
+ "Victory"
+ ],
+ theGenitive: [
+ "World",
+ "Word",
+ "South",
+ "West",
+ "North",
+ "East",
+ "Sun",
+ "Moon",
+ "Peak",
+ "Fall",
+ "Dawn",
+ "Eclipse",
+ "Abyss",
+ "Blood",
+ "Tree",
+ "Earth",
+ "Harvest",
+ "Rainbow",
+ "Sea",
+ "Sky",
+ "Stars",
+ "Storm",
+ "Underworld",
+ "Wild"
+ ],
color: ["Dark", "Light", "Bright", "Golden", "White", "Black", "Red", "Pink", "Purple", "Blue", "Green", "Yellow", "Amber", "Orange", "Brown", "Grey"]
};
@@ -29,7 +287,16 @@ window.Religions = (function () {
Heresy: {Heresy: 1}
};
- const methods = {"Random + type": 3, "Random + ism": 1, "Supreme + ism": 5, "Faith of + Supreme": 5, "Place + ism": 1, "Culture + ism": 2, "Place + ian + type": 6, "Culture + type": 4};
+ const methods = {
+ "Random + type": 3,
+ "Random + ism": 1,
+ "Supreme + ism": 5,
+ "Faith of + Supreme": 5,
+ "Place + ism": 1,
+ "Culture + ism": 2,
+ "Place + ian + type": 6,
+ "Culture + type": 4
+ };
const types = {
Shamanism: {Beliefs: 3, Shamanism: 2, Spirits: 1},
@@ -78,7 +345,10 @@ window.Religions = (function () {
}
const burgs = pack.burgs.filter(b => b.i && !b.removed);
- const sorted = burgs.length > +religionsInput.value ? burgs.sort((a, b) => b.population - a.population).map(b => b.cell) : cells.i.filter(i => cells.s[i] > 2).sort((a, b) => cells.s[b] - cells.s[a]);
+ const sorted =
+ burgs.length > +religionsInput.value
+ ? burgs.sort((a, b) => b.population - a.population).map(b => b.cell)
+ : cells.i.filter(i => cells.s[i] > 2).sort((a, b) => cells.s[b] - cells.s[a]);
const religionsTree = d3.quadtree();
const spacing = (graphWidth + graphHeight) / 6 / religionsInput.value; // base min distance between towns
const cultsCount = Math.floor((rand(10, 40) / 100) * religionsInput.value);
@@ -160,9 +430,20 @@ window.Religions = (function () {
const name = getCultName("Heresy", center);
const expansionism = gauss(1.2, 0.5, 0, 5);
const color = getMixedColor(r.color, 0.4, 0.2); // "url(#hatch6)";
- religions.push({i: religions.length, name, color, culture, type: "Heresy", form: r.form, deity: r.deity, expansion: "global", expansionism, center, origin: r.i});
+ religions.push({
+ i: religions.length,
+ name,
+ color,
+ culture,
+ type: "Heresy",
+ form: r.form,
+ deity: r.deity,
+ expansion: "global",
+ expansionism,
+ center,
+ origin: r.i
+ });
religionsTree.add([x, y]);
- //debug.append("circle").attr("cx", x).attr("cy", y).attr("r", 2).attr("fill", "green");
}
});
@@ -195,7 +476,24 @@ window.Religions = (function () {
name,
religions.map(r => r.code)
);
- religions.push({i, name, color, culture, type, form: formName, deity, expansion, expansionism: 0, center, cells: 0, area: 0, rural: 0, urban: 0, origin: r, code});
+ religions.push({
+ i,
+ name,
+ color,
+ culture,
+ type,
+ form: formName,
+ deity,
+ expansion,
+ expansionism: 0,
+ center,
+ cells: 0,
+ area: 0,
+ rural: 0,
+ urban: 0,
+ origin: r,
+ code
+ });
cells.religion[center] = i;
};
@@ -292,21 +590,19 @@ window.Religions = (function () {
};
function checkCenters() {
- const cells = pack.cells,
- religions = pack.religions;
+ const {cells, religions} = pack;
const codes = religions.map(r => r.code);
- religions
- .filter(r => r.i)
- .forEach(r => {
- r.code = abbreviate(r.name, codes);
+ religions.forEach(r => {
+ if (!r.i) return;
+ r.code = abbreviate(r.name, codes);
- // move religion center if it's not within religion area after expansion
- if (cells.religion[r.center] === r.i) return; // in area
- const religCells = cells.i.filter(i => cells.religion[i] === r.i);
- if (!religCells.length) return; // extinct religion
- r.center = religCells.sort((a, b) => b.pop - a.pop)[0];
- });
+ // move religion center if it's not within religion area after expansion
+ if (cells.religion[r.center] === r.i) return; // in area
+ const religCells = cells.i.filter(i => cells.religion[i] === r.i);
+ if (!religCells.length) return; // extinct religion
+ r.center = religCells.sort((a, b) => cells.pop[b] - cells.pop[a])[0];
+ });
}
function updateCultures() {
diff --git a/modules/save.js b/modules/save.js
index 8618a007..04f3e665 100644
--- a/modules/save.js
+++ b/modules/save.js
@@ -1,377 +1,5 @@
-// Functions to save and load the map
"use strict";
-
-// download map as SVG
-async function saveSVG() {
- TIME && console.time("saveSVG");
- const url = await getMapURL("svg");
- const link = document.createElement("a");
- link.download = getFileName() + ".svg";
- link.href = url;
- link.click();
-
- tip(`${link.download} is saved. Open "Downloads" screen (crtl + J) to check. You can set image scale in options`, true, "success", 5000);
- TIME && console.timeEnd("saveSVG");
-}
-
-// download map as PNG
-async function savePNG() {
- TIME && console.time("savePNG");
- const url = await getMapURL("png");
-
- const link = document.createElement("a");
- const canvas = document.createElement("canvas");
- const ctx = canvas.getContext("2d");
- canvas.width = svgWidth * pngResolutionInput.value;
- canvas.height = svgHeight * pngResolutionInput.value;
- const img = new Image();
- img.src = url;
-
- 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);
- });
- };
-
- TIME && console.timeEnd("savePNG");
-}
-
-// download map as JPEG
-async function saveJPEG() {
- TIME && console.time("saveJPEG");
- const url = await getMapURL("png");
-
- const canvas = document.createElement("canvas");
- canvas.width = svgWidth * pngResolutionInput.value;
- canvas.height = svgHeight * pngResolutionInput.value;
- const img = new Image();
- img.src = url;
-
- 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), 0.92);
- const URL = await canvas.toDataURL("image/jpeg", quality);
- const link = document.createElement("a");
- link.download = getFileName() + ".jpeg";
- link.href = URL;
- 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");
-}
-
-// download map as png tiles
-async function saveTiles() {
- return new Promise(async (resolve, reject) => {
- // download schema
- const urlSchema = await getMapURL("tiles", {debug: true});
- const zip = new JSZip();
-
- const canvas = document.createElement("canvas");
- const ctx = canvas.getContext("2d");
- canvas.width = graphWidth;
- canvas.height = graphHeight;
-
- const imgSchema = new Image();
- imgSchema.src = urlSchema;
- imgSchema.onload = function () {
- ctx.drawImage(imgSchema, 0, 0, canvas.width, canvas.height);
- canvas.toBlob(blob => zip.file(`fmg_tile_schema.png`, blob));
- };
-
- // download tiles
- const url = await getMapURL("tiles");
- const tilesX = +document.getElementById("tileColsInput").value;
- const tilesY = +document.getElementById("tileRowsInput").value;
- const scale = +document.getElementById("tileScaleInput").value;
-
- const tileW = (graphWidth / tilesX) | 0;
- const tileH = (graphHeight / tilesY) | 0;
- const tolesTotal = tilesX * tilesY;
-
- const width = graphWidth * scale;
- const height = width * (tileH / tileW);
- canvas.width = width;
- canvas.height = height;
-
- let loaded = 0;
- const img = new Image();
- img.src = url;
- img.onload = function () {
- for (let y = 0, i = 0; y + tileH <= graphHeight; y += tileH) {
- for (let x = 0; x + tileW <= graphWidth; x += tileW, i++) {
- ctx.drawImage(img, x, y, tileW, tileH, 0, 0, width, height);
- const name = `fmg_tile_${i}.png`;
- canvas.toBlob(blob => {
- zip.file(name, blob);
- loaded += 1;
- if (loaded === tolesTotal) return downloadZip();
- });
- }
- }
- };
-
- function downloadZip() {
- const name = `${getFileName()}.zip`;
- zip.generateAsync({type: "blob"}).then(blob => {
- const link = document.createElement("a");
- link.href = URL.createObjectURL(blob);
- link.download = name;
- link.click();
- link.remove();
-
- setTimeout(() => URL.revokeObjectURL(link.href), 5000);
- resolve(true);
- });
- }
- });
-}
-
-// parse map svg to object url
-async function getMapURL(type, options = {}) {
- const {debug = false, globe = false, noLabels = false, noWater = false} = options;
- const cloneEl = document.getElementById("map").cloneNode(true); // clone svg
- cloneEl.id = "fantasyMap";
- document.body.appendChild(cloneEl);
- const clone = d3.select(cloneEl);
- if (!debug) clone.select("#debug")?.remove();
-
- const cloneDefs = cloneEl.getElementsByTagName("defs")[0];
- const svgDefs = document.getElementById("defElements");
-
- const isFirefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
- if (isFirefox && type === "mesh") clone.select("#oceanPattern")?.remove();
- if (globe) clone.select("#scaleBar")?.remove();
- if (noLabels) {
- clone.select("#labels #states")?.remove();
- clone.select("#labels #burgLabels")?.remove();
- clone.select("#icons #burgIcons")?.remove();
- }
- if (noWater) {
- clone.select("#oceanBase").attr("opacity", 0);
- clone.select("#oceanPattern").attr("opacity", 0);
- }
- if (type !== "png") {
- // reset transform to show the whole map
- clone.attr("width", graphWidth).attr("height", graphHeight);
- clone.select("#viewbox").attr("transform", null);
- }
-
- if (type === "svg") removeUnusedElements(clone);
- if (customization && type === "mesh") updateMeshCells(clone);
- inlineStyle(clone);
-
- // remove unused filters
- const filters = cloneEl.querySelectorAll("filter");
- 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;
- filters[i].remove();
- }
-
- // remove unused patterns
- const patterns = cloneEl.querySelectorAll("pattern");
- for (let i = 0; i < patterns.length; i++) {
- const id = patterns[i].id;
- 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++) {
- const id = symbols[i].id;
- 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));
- });
- } else {
- cloneDefs.querySelector("#defs-emblems")?.remove();
- }
-
- // replace ocean pattern href to base64
- if (PRODUCTION && cloneEl.getElementById("oceanicPattern")) {
- const el = cloneEl.getElementById("oceanicPattern");
- const url = el.getAttribute("href");
- await new Promise(resolve => {
- getBase64(url, base64 => {
- el.setAttribute("href", base64);
- resolve();
- });
- });
- }
-
- // add relief icons
- if (cloneEl.getElementById("terrain")) {
- const uniqueElements = new Set();
- const terrainNodes = cloneEl.getElementById("terrain").childNodes;
- for (let i = 0; i < terrainNodes.length; i++) {
- const href = terrainNodes[i].getAttribute("href") || terrainNodes[i].getAttribute("xlink:href");
- uniqueElements.add(href);
- }
-
- const defsRelief = svgDefs.getElementById("defs-relief");
- for (const terrain of [...uniqueElements]) {
- const element = defsRelief.querySelector(terrain);
- if (element) cloneDefs.appendChild(element.cloneNode(true));
- }
- }
-
- // add wind rose
- if (cloneEl.getElementById("compass")) {
- const rose = svgDefs.getElementById("rose");
- if (rose) cloneDefs.appendChild(rose.cloneNode(true));
- }
-
- // add port icon
- if (cloneEl.getElementById("anchors")) {
- const anchor = svgDefs.getElementById("icon-anchor");
- if (anchor) cloneDefs.appendChild(anchor.cloneNode(true));
- }
-
- // add grid pattern
- if (cloneEl.getElementById("gridOverlay")?.hasChildNodes()) {
- const type = cloneEl.getElementById("gridOverlay").getAttribute("type");
- const pattern = svgDefs.getElementById("pattern_" + type);
- if (pattern) cloneDefs.appendChild(pattern.cloneNode(true));
- }
-
- if (!cloneEl.getElementById("hatching").children.length) cloneEl.getElementById("hatching")?.remove(); // remove unused hatching group
- if (!cloneEl.getElementById("fogging-cont")) cloneEl.getElementById("fog")?.remove(); // remove unused fog
- if (!cloneEl.getElementById("regions")) cloneEl.getElementById("statePaths")?.remove(); // removed unused statePaths
- if (!cloneEl.getElementById("labels")) cloneEl.getElementById("textPaths")?.remove(); // removed unused textPaths
-
- // add armies style
- if (cloneEl.getElementById("armies")) cloneEl.insertAdjacentHTML("afterbegin", "");
-
- // add xlink: for href to support svg1.1
- if (type === "svg") {
- cloneEl.querySelectorAll("[href]").forEach(el => {
- const href = el.getAttribute("href");
- el.removeAttribute("href");
- el.setAttribute("xlink:href", href);
- });
- }
-
- // TODO: add dataURL for all used fonts
- const usedFonts = getUsedFonts(cloneEl);
- const fontsToLoad = usedFonts.filter(font => font.src);
- if (fontsToLoad.length) {
- const dataURLfonts = await loadFontsAsDataURI(fontsToLoad);
-
- const fontFaces = dataURLfonts
- .map(({family, src, unicodeRange = "", variant = "normal"}) => {
- return `@font-face {font-family: "${family}"; src: ${src}; unicode-range: ${unicodeRange}; font-variant: ${variant};}`;
- })
- .join("\n");
-
- const style = document.createElement("style");
- style.setAttribute("type", "text/css");
- style.innerHTML = fontFaces;
- cloneEl.querySelector("defs").appendChild(style);
- }
-
- clone.remove();
-
- 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;
-}
-
-// remove hidden g elements and g elements without children to make downloaded svg smaller in size
-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; ) {
- empty = 0;
- 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");
- });
- }
-}
-
-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));
-}
-
-// for each g element get inline style
-function inlineStyle(clone) {
- const emptyG = clone.append("g").node();
- const defaultStyles = window.getComputedStyle(emptyG);
-
- clone.selectAll("g, #ruler *, #scaleBar > text").each(function () {
- const compStyle = window.getComputedStyle(this);
- let style = "";
-
- for (let i = 0; i < compStyle.length; i++) {
- const key = compStyle[i];
- const value = compStyle.getPropertyValue(key);
-
- // Firefox mask hack
- if (key === "mask-image" && value !== defaultStyles.getPropertyValue(key)) {
- style += "mask-image: url('#land');";
- continue;
- }
-
- 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 + ";";
- }
-
- for (const key in compStyle) {
- const value = compStyle.getPropertyValue(key);
-
- 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 + ";";
- }
-
- if (style != "") this.setAttribute("style", style);
- });
-
- emptyG.remove();
-}
+// functions to save project as .map file
// prepare map data for saving
function getMapData() {
@@ -381,7 +9,33 @@ 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, barSizeInput.value, barLabel.value, barBackOpacity.value, barBackColor.value, barPosX.value, barPosY.value, populationRate, urbanization, mapSizeOutput.value, latitudeOutput.value, temperatureEquatorOutput.value, temperaturePoleOutput.value, precOutput.value, JSON.stringify(options), mapName.value, +hideLabels.checked, stylePreset.value, +rescaleLabels.checked].join("|");
+ const settings = [
+ distanceUnitInput.value,
+ distanceScaleInput.value,
+ areaUnit.value,
+ heightUnit.value,
+ heightExponentInput.value,
+ temperatureScale.value,
+ barSizeInput.value,
+ barLabel.value,
+ barBackOpacity.value,
+ barBackColor.value,
+ barPosX.value,
+ barPosY.value,
+ populationRate,
+ urbanization,
+ mapSizeOutput.value,
+ latitudeOutput.value,
+ temperatureEquatorOutput.value,
+ temperaturePoleOutput.value,
+ precOutput.value,
+ JSON.stringify(options),
+ mapName.value,
+ +hideLabels.checked,
+ stylePreset.value,
+ +rescaleLabels.checked,
+ urbanDensity
+ ].join("|");
const coords = JSON.stringify(mapCoordinates);
const biomes = [biomesData.color, biomesData.habitability, biomesData.name].join("|");
const notesData = JSON.stringify(notes);
@@ -409,6 +63,7 @@ function getMapData() {
const religions = JSON.stringify(pack.religions);
const provinces = JSON.stringify(pack.provinces);
const rivers = JSON.stringify(pack.rivers);
+ const markers = JSON.stringify(pack.markers);
// store name array only if not the same as default
const defaultNB = Names.getNameBases();
@@ -423,7 +78,44 @@ function getMapData() {
const pop = Array.from(pack.cells.pop).map(p => rn(p, 4));
// data format as below
- const mapData = [params, settings, coords, biomes, notesData, serializedSVG, gridGeneral, grid.cells.h, grid.cells.prec, grid.cells.f, grid.cells.t, grid.cells.temp, packFeatures, 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, fonts].join("\r\n");
+ const mapData = [
+ params,
+ settings,
+ coords,
+ biomes,
+ notesData,
+ serializedSVG,
+ gridGeneral,
+ grid.cells.h,
+ grid.cells.prec,
+ grid.cells.f,
+ grid.cells.t,
+ grid.cells.temp,
+ packFeatures,
+ 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,
+ fonts,
+ markers
+ ].join("\r\n");
TIME && console.timeEnd("createMapData");
return mapData;
}
@@ -445,7 +137,6 @@ function dowloadMap() {
}
async function saveToDropbox() {
- const sharableLinkContainer = document.getElementById("sharableLinkContainer");
if (customization) return tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error");
closeDialogs("#alert");
const mapData = getMapData();
@@ -459,124 +150,6 @@ async function saveToDropbox() {
}
}
-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]]));
-
- cells.i.forEach(i => {
- const coordinates = getCellCoordinates(cells.v[i]);
- const height = getHeight(i);
- const biome = cells.biome[i];
- const type = pack.features[cells.f[i]].type;
- const population = getPopulation(i);
- const state = cells.state[i];
- const province = cells.province[i];
- const culture = cells.culture[i];
- const religion = cells.religion[i];
- const neighbors = cells.c[i];
-
- 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);
- });
-
- const name = getFileName("Cells") + ".geojson";
- downloadFile(JSON.stringify(json), name, "application/json");
-}
-
-function saveGeoJSON_Routes() {
- const json = {type: "FeatureCollection", features: []};
-
- routes.selectAll("g > path").each(function () {
- const coordinates = getRoutePoints(this);
- const id = this.id;
- const type = this.parentElement.id;
-
- const feature = {type: "Feature", geometry: {type: "LineString", coordinates}, properties: {id, type}};
- json.features.push(feature);
- });
-
- const name = getFileName("Routes") + ".geojson";
- downloadFile(JSON.stringify(json), name, "application/json");
-}
-
-function saveGeoJSON_Rivers() {
- const json = {type: "FeatureCollection", features: []};
-
- rivers.selectAll("path").each(function () {
- const coordinates = getRiverPoints(this);
- const id = this.id;
- const width = +this.dataset.increment;
- const increment = +this.dataset.increment;
- const river = pack.rivers.find(r => r.i === +id.slice(5));
- const name = river ? river.name : "";
- const type = river ? river.type : "";
- const i = river ? river.i : "";
- const basin = river ? river.basin : "";
-
- const feature = {type: "Feature", geometry: {type: "LineString", coordinates}, properties: {id, i, basin, name, type, width, increment}};
- json.features.push(feature);
- });
-
- const name = getFileName("Rivers") + ".geojson";
- downloadFile(JSON.stringify(json), name, "application/json");
-}
-
-function saveGeoJSON_Markers() {
- const json = {type: "FeatureCollection", features: []};
-
- 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 icon = document.getElementById(type).textContent;
- const note = notes.length ? notes.find(note => note.id === this.id) : null;
- const name = note ? note.name : "";
- const legend = note ? note.legend : "";
-
- const feature = {type: "Feature", geometry: {type: "Point", coordinates}, properties: {id, type, icon, name, legend}};
- json.features.push(feature);
- });
-
- const name = getFileName("Markers") + ".geojson";
- downloadFile(JSON.stringify(json), name, "application/json");
-}
-
-function getCellCoordinates(vertices) {
- const p = pack.vertices.p;
- const coordinates = vertices.map(n => getQGIScoordinates(p[n][0], p[n][1]));
- return [coordinates.concat([coordinates[0]])];
-}
-
-function getRoutePoints(node) {
- let points = [];
- const l = node.getTotalLength();
- const increment = l / Math.ceil(l / 2);
- for (let i = 0; i <= l; i += increment) {
- const p = node.getPointAtLength(i);
- points.push(getQGIScoordinates(p.x, p.y));
- }
- return points;
-}
-
-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) {
- 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]);
- }
- return points;
-}
-
function quickSave() {
if (customization) return tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error");
@@ -588,15 +161,24 @@ function quickSave() {
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"
+ ];
+ const interval = 15 * 60 * 1000; // remind every 15 minutes
saveReminder.reminder = setInterval(() => {
if (customization) return;
tip(ra(message), true, "warn", 2500);
- }, 1e6);
+ }, interval);
saveReminder.status = 1;
};
-
saveReminder();
function toggleSaveReminder() {
diff --git a/modules/ui/3d.js b/modules/ui/3d.js
index b3fb4ce5..3135c6e1 100644
--- a/modules/ui/3d.js
+++ b/modules/ui/3d.js
@@ -1,20 +1,43 @@
"use strict";
window.ThreeD = (function () {
- // set default options
- const options = {scale: 50, lightness: 0.7, shadow: 0.5, sun: {x: 100, y: 600, z: 1000}, rotateMesh: 0, rotateGlobe: 0.5, skyColor: "#9ecef5", waterColor: "#466eab", extendedWater: 0, labels3d: 0, resolution: 2};
+ const options = {
+ scale: 50,
+ lightness: 0.7,
+ shadow: 0.5,
+ sun: {x: 100, y: 600, z: 1000},
+ rotateMesh: 0,
+ rotateGlobe: 0.5,
+ skyColor: "#9ecef5",
+ waterColor: "#466eab",
+ extendedWater: 0,
+ labels3d: 0,
+ resolution: 2
+ };
// set variables
- let Renderer, scene, camera, controls, animationFrame, material, texture, geometry, mesh, ambientLight, spotLight, waterPlane, waterMaterial, waterMesh, raycaster;
-
- const drawCtx = document.createElement("canvas").getContext("2d");
- const drawSVG = document.createElementNS("http://www.w3.org/2000/svg", "svg");
- document.body.appendChild(drawSVG);
+ let Renderer,
+ scene,
+ camera,
+ controls,
+ animationFrame,
+ material,
+ texture,
+ geometry,
+ mesh,
+ ambientLight,
+ spotLight,
+ waterPlane,
+ waterMaterial,
+ waterMesh,
+ raycaster;
let labels = [];
let icons = [];
let lines = [];
+ const context2d = document.createElement("canvas").getContext("2d");
+
// initiate 3d scene
const create = async function (canvas, type = "viewMesh") {
options.isOn = true;
@@ -210,16 +233,16 @@ window.ThreeD = (function () {
}
async function createTextLabel({text, font, size, color, quality}) {
- drawCtx.font = `${size * quality}px ${font}`;
- drawCtx.canvas.width = drawCtx.measureText(text).width;
- drawCtx.canvas.height = size * quality * 1.25; // 25% margin as text can overflow the font size
- drawCtx.clearRect(0, 0, drawCtx.canvas.width, drawCtx.canvas.height);
+ context2d.font = `${size * quality}px ${font}`;
+ context2d.canvas.width = context2d.measureText(text).width;
+ context2d.canvas.height = size * quality * 1.25; // 25% margin as text can overflow the font size
+ context2d.clearRect(0, 0, context2d.canvas.width, context2d.canvas.height);
- drawCtx.font = `${size * quality}px ${font}`;
- drawCtx.fillStyle = color;
- drawCtx.fillText(text, 0, size * quality);
+ context2d.font = `${size * quality}px ${font}`;
+ context2d.fillStyle = color;
+ context2d.fillText(text, 0, size * quality);
- return textureToSprite(drawCtx.canvas.toDataURL(), drawCtx.canvas.width / quality, drawCtx.canvas.height / quality);
+ return textureToSprite(context2d.canvas.toDataURL(), context2d.canvas.width / quality, context2d.canvas.height / quality);
}
function get3dCoords(baseX, baseY) {
@@ -368,7 +391,8 @@ window.ThreeD = (function () {
async function createMesh(width, height, segmentsX, segmentsY) {
const mapOptions = {
noLabels: options.labels3d,
- noWater: options.extendedWater
+ noWater: options.extendedWater,
+ fullMap: true
};
const url = await getMapURL("mesh", mapOptions);
window.setTimeout(() => window.URL.revokeObjectURL(url), 5000);
@@ -422,7 +446,8 @@ window.ThreeD = (function () {
if (texture) texture.dispose();
const mapOptions = {
noLabels: options.labels3d,
- noWater: options.extendedWater
+ noWater: options.extendedWater,
+ fullMap: true
};
const url = await getMapURL("mesh", mapOptions);
window.setTimeout(() => window.URL.revokeObjectURL(url), 4000);
@@ -503,7 +528,7 @@ window.ThreeD = (function () {
material.map = texture;
if (addMesh) addGlobe3dMesh();
};
- img2.src = await getMapURL("mesh", {globe: true});
+ img2.src = await getMapURL("mesh", {globe: true, fullMap: true});
}
async function getOBJ() {
@@ -578,5 +603,21 @@ window.ThreeD = (function () {
});
}
- return {create, redraw, update, stop, options, setScale, setLightness, setSun, setRotation, toggleLabels, toggleSky, setResolution, setColors, saveScreenshot, saveOBJ};
+ return {
+ create,
+ redraw,
+ update,
+ stop,
+ options,
+ setScale,
+ setLightness,
+ setSun,
+ setRotation,
+ toggleLabels,
+ toggleSky,
+ setResolution,
+ setColors,
+ saveScreenshot,
+ saveOBJ
+ };
})();
diff --git a/modules/ui/battle-screen.js b/modules/ui/battle-screen.js
index 778866ad..37a1bd51 100644
--- a/modules/ui/battle-screen.js
+++ b/modules/ui/battle-screen.js
@@ -330,7 +330,7 @@ class Battle {
}
getInitialMorale() {
- const powerFee = diff => Math.min(Math.max(100 - diff ** 1.5 * 10 + 10, 50), 100);
+ const powerFee = diff => minmax(100 - diff ** 1.5 * 10 + 10, 50, 100);
const distanceFee = dist => Math.min(d3.mean(dist) / 50, 15);
const powerDiff = this.defenders.power / this.attackers.power;
this.attackers.morale = powerFee(powerDiff) - distanceFee(this.attackers.distances);
@@ -677,7 +677,22 @@ class Battle {
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 > 0.8 ? "is almost completely destroyed" : losses > 0.5 ? "suffered terrible losses" : losses > 0.3 ? "suffered severe losses" : losses > 0.2 ? "suffered heavy losses" : losses > 0.05 ? "suffered significant losses" : losses > 0 ? "suffered unsignificant losses" : "left the battle without loss";
+ const regStatus =
+ losses === 1
+ ? "is destroyed"
+ : losses > 0.8
+ ? "is almost completely destroyed"
+ : losses > 0.5
+ ? "suffered terrible losses"
+ : losses > 0.3
+ ? "suffered severe losses"
+ : losses > 0.2
+ ? "suffered heavy losses"
+ : losses > 0.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);
@@ -691,40 +706,32 @@ class Battle {
armies.select(`g#${id} > text`).text(Military.getTotal(r)); // update reg box
}
- // 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 i = last(pack.markers)?.i + 1 || 0;
+ {
+ // append battlefield marker
+ const marker = {i, x: this.x, y: this.y, cell: this.cell, icon: "⚔️", type: "battlefields", dy: 52};
+ pack.markers.push(marker);
+ const markerHTML = drawMarker(marker);
+ document.getElementById("markers").insertAdjacentHTML("beforeend", markerHTML);
+ }
- const getSide = (regs, n) => (regs.length > 1 ? `${n ? "regiments" : "forces"} of ${list([...new Set(regs.map(r => pack.states[r.state].name))])}` : getAdjective(pack.states[regs[0].state].name) + " " + regs[0].name);
+ const getSide = (regs, n) =>
+ regs.length > 1
+ ? `${n ? "regiments" : "forces"} of ${list([...new Set(regs.map(r => pack.states[r.state].name))])}`
+ : getAdjective(pack.states[regs[0].state].name) + " " + regs[0].name;
const getLosses = casualties => Math.min(rn(casualties * 100), 100);
const status = battleStatus[+P(0.7)];
const result = `The ${this.getTypeName(this.type)} ended in ${status}`;
- 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)}. ${result}.
+ 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
+ )}. ${result}.
\r\nAttackers losses: ${getLosses(this.attackers.casualties)}%, defenders losses: ${getLosses(this.defenders.casualties)}%`;
- const id = getNextId("markerElement");
- notes.push({id, name: this.name, legend});
+ notes.push({id: `marker${i}`, name: this.name, legend});
tip(`${this.name} is over. ${result}`, true, "success", 4000);
- 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/biomes-editor.js b/modules/ui/biomes-editor.js
index fb048044..ca5bb41d 100644
--- a/modules/ui/biomes-editor.js
+++ b/modules/ui/biomes-editor.js
@@ -94,10 +94,14 @@ function editBiomes() {
lines += `
-
+
%
-
+
${b.cells[i]}
@@ -189,41 +193,27 @@ function editBiomes() {
}
function openWiki(el) {
- const name = el.parentNode.dataset.name;
- if (name === "Custom" || !name) {
- tip("Please provide a biome name", false, "error");
- return;
- }
- const wiki = "https://en.wikipedia.org/wiki/";
+ const biomeName = el.parentNode.dataset.name;
+ if (biomeName === "Custom" || !biomeName) return tip("Please fill in the biome name", false, "error");
- switch (name) {
- case "Hot desert":
- openURL(wiki + "Desert_climate#Hot_desert_climates");
- case "Cold desert":
- openURL(wiki + "Desert_climate#Cold_desert_climates");
- case "Savanna":
- openURL(wiki + "Tropical_and_subtropical_grasslands,_savannas,_and_shrublands");
- case "Grassland":
- openURL(wiki + "Temperate_grasslands,_savannas,_and_shrublands");
- case "Tropical seasonal forest":
- openURL(wiki + "Seasonal_tropical_forest");
- case "Temperate deciduous forest":
- openURL(wiki + "Temperate_deciduous_forest");
- case "Tropical rainforest":
- openURL(wiki + "Tropical_rainforest");
- case "Temperate rainforest":
- openURL(wiki + "Temperate_rainforest");
- case "Taiga":
- openURL(wiki + "Taiga");
- case "Tundra":
- openURL(wiki + "Tundra");
- case "Glacier":
- openURL(wiki + "Glacier");
- case "Wetland":
- openURL(wiki + "Wetland");
- default:
- openURL(`https://en.wikipedia.org/w/index.php?search=${name}`);
- }
+ const wikiBase = "https://en.wikipedia.org/wiki/";
+ const pages = {
+ "Hot desert": "Desert_climate#Hot_desert_climates",
+ "Cold desert": "Desert_climate#Cold_desert_climates",
+ Savanna: "Tropical_and_subtropical_grasslands,_savannas,_and_shrublands",
+ Grassland: "Temperate_grasslands,_savannas,_and_shrublands",
+ "Tropical seasonal forest": "Seasonal_tropical_forest",
+ "Temperate deciduous forest": "Temperate_deciduous_forest",
+ "Tropical rainforest": "Tropical_rainforest",
+ "Temperate rainforest": "Temperate_rainforest",
+ Taiga: "Taiga",
+ Tundra: "Tundra",
+ Glacier: "Glacier",
+ Wetland: "Wetland"
+ };
+ const customBiomeLink = `https://en.wikipedia.org/w/index.php?search=${biomeName}`;
+ const link = pages[biomeName] ? wikiBase + pages[biomeName] : customBiomeLink;
+ openURL(link);
}
function toggleLegend() {
@@ -343,7 +333,11 @@ function editBiomes() {
$("#biomesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}});
tip("Click on biome to select, drag the circle to change biome", true);
- viewbox.style("cursor", "crosshair").on("click", selectBiomeOnMapClick).call(d3.drag().on("start", dragBiomeBrush)).on("touchmove mousemove", moveBiomeBrush);
+ viewbox
+ .style("cursor", "crosshair")
+ .on("click", selectBiomeOnMapClick)
+ .call(d3.drag().on("start", dragBiomeBrush))
+ .on("touchmove mousemove", moveBiomeBrush);
}
function selectBiomeOnLineClick(line) {
diff --git a/modules/ui/burg-editor.js b/modules/ui/burg-editor.js
index 433a9a9d..7af55c29 100644
--- a/modules/ui/burg-editor.js
+++ b/modules/ui/burg-editor.js
@@ -10,15 +10,11 @@ function editBurg(id) {
burgLabels.selectAll("text").call(d3.drag().on("start", dragBurgLabel)).classed("draggable", true);
updateBurgValues();
- const my = id || d3.event.target.tagName === "text" ? "center bottom-20" : "center top+20";
- const at = id ? "center" : d3.event.target.tagName === "text" ? "top" : "bottom";
- const of = id ? "svg" : d3.event.target;
-
$("#burgEditor").dialog({
title: "Edit Burg",
resizable: false,
close: closeBurgEditor,
- position: {my, at, of, collision: "fit"}
+ position: {my: "left top", at: "left+10 top+10", of: "svg", collision: "fit"}
});
if (modules.editBurg) return;
@@ -39,6 +35,8 @@ function editBurg(id) {
document.getElementById("burgNameReCulture").addEventListener("click", generateNameCulture);
document.getElementById("burgPopulation").addEventListener("change", changePopulation);
burgBody.querySelectorAll(".burgFeature").forEach(el => el.addEventListener("click", toggleFeature));
+ document.getElementById("mfcgBurgSeed").addEventListener("change", changeSeed);
+ document.getElementById("regenerateMFCGBurgSeed").addEventListener("click", randomizeSeed);
document.getElementById("burgStyleShow").addEventListener("click", showStyleSection);
document.getElementById("burgStyleHide").addEventListener("click", hideStyleSection);
@@ -46,12 +44,12 @@ function editBurg(id) {
document.getElementById("burgEditIconStyle").addEventListener("click", editGroupIconStyle);
document.getElementById("burgEditAnchorStyle").addEventListener("click", editGroupAnchorStyle);
- document.getElementById("burgSeeInMFCG").addEventListener("click", openInMFCG);
+ document.getElementById("burgEmblem").addEventListener("click", openEmblemEdit);
+ document.getElementById("burgToggleMFCGMap").addEventListener("click", toggleMFCGMap);
document.getElementById("burgEditEmblem").addEventListener("click", openEmblemEdit);
document.getElementById("burgRelocate").addEventListener("click", toggleRelocateBurg);
document.getElementById("burglLegend").addEventListener("click", editBurgLegend);
document.getElementById("burgLock").addEventListener("click", toggleBurgLockButton);
- document.getElementById("burgLock").addEventListener("mouseover", showBurgELockTip);
document.getElementById("burgRemove").addEventListener("click", removeSelectedBurg);
function updateBurgValues() {
@@ -110,6 +108,14 @@ function editBurg(id) {
const coaID = "burgCOA" + id;
COArenderer.trigger(coaID, b.coa);
document.getElementById("burgEmblem").setAttribute("href", "#" + coaID);
+
+ if (options.showMFCGMap) {
+ document.getElementById("mfcgPreviewSection").style.display = "block";
+ updateMFCGFrame(b);
+ document.getElementById("mfcgBurgSeed").value = getBurgSeed(b);
+ } else {
+ document.getElementById("mfcgPreviewSection").style.display = "none";
+ }
}
// in °C, array from -1 °C; source: https://en.wikipedia.org/wiki/List_of_cities_by_average_temperature
@@ -275,12 +281,12 @@ function editBurg(id) {
const capital = burgsToRemove.length < burgsInGroup.length;
alertMessage.innerHTML = `Are you sure you want to remove
- ${basic || capital ? "all unlocked elements in the group" : "the entire burg group"}?
+ ${basic || capital ? "all unlocked elements in the burg group" : "the entire burg group"}?
Please note that capital or locked burgs will not be deleted.
Burgs to be removed: ${burgsToRemove.length}`;
$("#alert").dialog({
resizable: false,
- title: "Remove route group",
+ title: "Remove burg group",
buttons: {
Remove: function () {
$(this).dialog("close");
@@ -372,11 +378,6 @@ function editBurg(id) {
}
}
- function showBurgELockTip() {
- const id = +elSelected.attr("data-id");
- showBurgLockTip(id);
- }
-
function showStyleSection() {
document.querySelectorAll("#burgBottom > button").forEach(el => (el.style.display = "none"));
document.getElementById("burgStyleSection").style.display = "inline-block";
@@ -402,59 +403,27 @@ function editBurg(id) {
editStyle("anchors", g);
}
- function openInMFCG(event) {
- const id = elSelected.attr("data-id");
+ function updateMFCGFrame(burg) {
+ const mfcgURL = getMFCGlink(burg);
+ document.getElementById("mfcgPreview").setAttribute("src", mfcgURL);
+ document.getElementById("mfcgLink").setAttribute("href", mfcgURL);
+ }
+
+ function changeSeed() {
+ const id = +elSelected.attr("data-id");
const burg = pack.burgs[id];
- const defSeed = +(seed + id.padStart(4, 0));
- if (isCtrlClick(event)) {
- prompt(
- `Please provide a Medieval Fantasy City Generator seed.
- Seed should be a number. Default seed is FMG map seed + burg id padded to 4 chars with zeros (${defSeed}).
- Please note that if seed is custom, "Overworld" button from MFCG will open a different map`,
- {default: burg.MFCG || defSeed, step: 1, min: 1, max: 1e13 - 1},
- v => {
- burg.MFCG = v;
- openMFCG(v);
- }
- );
- } else openMFCG();
+ const burgSeed = +this.value;
+ burg.MFCG = burgSeed;
+ updateMFCGFrame(burg);
+ }
- function openMFCG(seed) {
- if (!seed && burg.MFCGlink) {
- openURL(burg.MFCGlink);
- return;
- }
- const cells = pack.cells;
- const name = elSelected.text();
- const size = Math.max(Math.min(rn(burg.population), 100), 6); // to be removed once change on MFDC is done
- const population = rn(burg.population * populationRate * urbanization);
-
- const s = burg.MFCG || defSeed;
- const cell = burg.cell;
- const hub = +cells.road[cell] > 50;
- const river = cells.r[cell] ? 1 : 0;
-
- const coast = +burg.port;
- const citadel = +burg.citadel;
- const walls = +burg.walls;
- const plaza = +burg.plaza;
- const temple = +burg.temple;
- const shanty = +burg.shanty;
-
- const sea = coast && cells.haven[burg.cell] ? getSeaDirections(burg.cell) : "";
- function getSeaDirections(i) {
- const p1 = cells.p[i];
- const p2 = cells.p[cells.haven[i]];
- let deg = (Math.atan2(p2[1] - p1[1], p2[0] - p1[0]) * 180) / Math.PI - 90;
- if (deg < 0) deg += 360;
- const norm = rn(normalize(deg, 0, 360) * 2, 2); // 0 = south, 0.5 = west, 1 = north, 1.5 = east
- return "&sea=" + norm;
- }
-
- const site = "http://fantasycities.watabou.ru/?random=0&continuous=0";
- const url = `${site}&name=${name}&population=${population}&size=${size}&seed=${s}&hub=${hub}&river=${river}&coast=${coast}&citadel=${citadel}&plaza=${plaza}&temple=${temple}&walls=${walls}&shantytown=${shanty}${sea}`;
- openURL(url);
- }
+ function randomizeSeed() {
+ const id = +elSelected.attr("data-id");
+ const burg = pack.burgs[id];
+ const burgSeed = rand(1e9 - 1);
+ burg.MFCG = burgSeed;
+ updateMFCGFrame(burg);
+ document.getElementById("mfcgBurgSeed").value = burgSeed;
}
function openEmblemEdit() {
@@ -463,6 +432,12 @@ function editBurg(id) {
editEmblem("burg", "burgCOA" + id, burg);
}
+ function toggleMFCGMap() {
+ options.showMFCGMap = !options.showMFCGMap;
+ document.getElementById("mfcgPreviewSection").style.display = options.showMFCGMap ? "block" : "none";
+ document.getElementById("burgToggleMFCGMap").className = options.showMFCGMap ? "icon-map" : "icon-map-o";
+ }
+
function toggleRelocateBurg() {
const toggler = document.getElementById("toggleCells");
document.getElementById("burgRelocate").classList.toggle("pressed");
diff --git a/modules/ui/burgs-overview.js b/modules/ui/burgs-overview.js
index d8847131..97093035 100644
--- a/modules/ui/burgs-overview.js
+++ b/modules/ui/burgs-overview.js
@@ -34,6 +34,7 @@ function overviewBurgs() {
uploadFile(this, importBurgNames);
});
document.getElementById("burgsRemoveAll").addEventListener("click", triggerAllBurgsRemove);
+ document.getElementById("burgsInvertLock").addEventListener("click", invertLock);
function refreshBurgsEditor() {
updateFilter();
@@ -79,20 +80,26 @@ function overviewBurgs() {
const province = prov ? pack.provinces[prov].name : "";
const culture = pack.cultures[b.culture].name;
- lines += `
+ lines += `
`;
}
@@ -112,7 +119,6 @@ function overviewBurgs() {
body.querySelectorAll("div > span.icon-star-empty").forEach(el => el.addEventListener("click", toggleCapitalStatus));
body.querySelectorAll("div > span.icon-anchor").forEach(el => el.addEventListener("click", togglePortStatus));
body.querySelectorAll("div > span.locks").forEach(el => el.addEventListener("click", toggleBurgLockStatus));
- body.querySelectorAll("div > span.locks").forEach(el => el.addEventListener("mouseover", showBurgOLockTip));
body.querySelectorAll("div > span.icon-pencil").forEach(el => el.addEventListener("click", openBurgEditor));
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.addEventListener("click", triggerBurgRemove));
@@ -147,8 +153,8 @@ function overviewBurgs() {
function zoomIntoBurg() {
const burg = +this.parentNode.dataset.id;
const label = document.querySelector("#burgLabels [data-id='" + burg + "']");
- const x = +label.getAttribute("x"),
- y = +label.getAttribute("y");
+ const x = +label.getAttribute("x");
+ const y = +label.getAttribute("y");
zoomTo(x, y, 8, 2000);
}
@@ -202,11 +208,6 @@ function overviewBurgs() {
}
}
- function showBurgOLockTip() {
- const burg = +this.parentNode.dataset.id;
- showBurgLockTip(burg);
- }
-
function openBurgEditor() {
const burg = +this.parentNode.dataset.id;
editBurg(burg);
@@ -214,24 +215,15 @@ function overviewBurgs() {
function triggerBurgRemove() {
const burg = +this.parentNode.dataset.id;
- if (pack.burgs[burg].capital) {
- tip("You cannot remove the capital. Please change the capital first", false, "error");
- return;
- }
+ if (pack.burgs[burg].capital) return tip("You cannot remove the capital. Please change the capital first", false, "error");
- alertMessage.innerHTML = "Are you sure you want to remove the burg?";
- $("#alert").dialog({
- resizable: false,
+ confirmationDialog({
title: "Remove burg",
- buttons: {
- Remove: function () {
- $(this).dialog("close");
- removeBurg(burg);
- burgsOverviewAddLines();
- },
- Cancel: function () {
- $(this).dialog("close");
- }
+ message: "Are you sure you want to remove the burg? This actiove cannot be reverted",
+ confirm: "Remove",
+ onConfirm: () => {
+ removeBurg(burg);
+ burgsOverviewAddLines();
}
});
}
@@ -239,22 +231,19 @@ function overviewBurgs() {
function regenerateNames() {
body.querySelectorAll(":scope > div").forEach(function (el) {
const burg = +el.dataset.id;
- //if (pack.burgs[burg].lock) return;
+ if (pack.burgs[burg].lock) return;
+
const culture = pack.burgs[burg].culture;
const name = Names.getCulture(culture);
- if (!pack.burgs[burg].lock) {
- el.querySelector(".burgName").value = name;
- pack.burgs[burg].name = el.dataset.name = name;
- burgLabels.select("[data-id='" + burg + "']").text(name);
- }
+
+ el.querySelector(".burgName").value = name;
+ pack.burgs[burg].name = el.dataset.name = name;
+ burgLabels.select("[data-id='" + burg + "']").text(name);
});
}
function enterAddBurgMode() {
- if (this.classList.contains("pressed")) {
- exitAddBurgMode();
- return;
- }
+ if (this.classList.contains("pressed")) return exitAddBurgMode();
customization = 3;
this.classList.add("pressed");
tip("Click on the map to create a new burg. Hold Shift to add multiple", true, "warn");
@@ -264,14 +253,9 @@ function overviewBurgs() {
function addBurgOnClick() {
const point = d3.mouse(this);
const cell = findCell(point[0], point[1]);
- if (pack.cells.h[cell] < 20) {
- tip("You cannot place state into the water. Please click on a land cell", false, "error");
- return;
- }
- if (pack.cells.burg[cell]) {
- tip("There is already a burg in this cell. Please select a free cell", false, "error");
- return;
- }
+ if (pack.cells.h[cell] < 20) return tip("You cannot place state into the water. Please click on a land cell", false, "error");
+ if (pack.cells.burg[cell]) return tip("There is already a burg in this cell. Please select a free cell", false, "error");
+
addBurg(point); // add new burg
if (d3.event.shiftKey === false) {
@@ -295,6 +279,7 @@ function overviewBurgs() {
const name = s.fullName ? s.fullName : s.name;
return {id: s.i, state: s.i ? 0 : null, color, name};
});
+
const burgs = pack.burgs
.filter(b => b.i && !b.removed)
.map(b => {
@@ -306,6 +291,7 @@ function overviewBurgs() {
return {id, i: b.i, state: b.state, culture: b.culture, province, parent, name: b.name, population, capital, x: b.x, y: b.y};
});
const data = states.concat(burgs);
+ if (data.length < 2) return tip("No burgs to show", false, "error");
const root = d3
.stratify()
@@ -313,8 +299,8 @@ function overviewBurgs() {
.sum(d => d.population)
.sort((a, b) => b.value - a.value);
- const width = 150 + 200 * uiSizeOutput.value,
- height = 150 + 200 * uiSizeOutput.value;
+ const width = 150 + 200 * uiSizeOutput.value;
+ const height = 150 + 200 * uiSizeOutput.value;
const margin = {top: 0, right: -50, bottom: -10, left: -50};
const w = width - margin.left - margin.right;
const h = height - margin.top - margin.bottom;
@@ -413,7 +399,14 @@ function overviewBurgs() {
if (this.value === "provinces") return d.province;
};
- const base = this.value === "states" ? getStatesData() : this.value === "cultures" ? getCulturesData() : this.value === "parent" ? getParentData() : getProvincesData();
+ const mapping = {
+ states: getStatesData,
+ cultures: getCulturesData,
+ parent: getParentData,
+ provinces: getProvincesData
+ };
+
+ const base = mapping[this.value]();
burgs.forEach(b => (b.id = b.i + base.length - 1));
const data = base.concat(burgs);
@@ -440,14 +433,15 @@ function overviewBurgs() {
width: fitContent(),
position: {my: "left bottom", at: "left+10 bottom-10", of: "svg"},
buttons: {},
- close: () => {
- alertMessage.innerHTML = "";
- }
+ close: () => (alertMessage.innerHTML = "")
});
}
function downloadBurgsData() {
- let data = "Id,Burg,Province,Province Full Name,State,State Full Name,Culture,Religion,Population,Longitude,Latitude,Elevation (" + heightUnit.value + "),Capital,Port,Citadel,Walls,Plaza,Temple,Shanty Town\n"; // headers
+ let data = `Id,Burg,Province,Province Full Name,State,State Full Name,Culture,Religion,Population,Latitude,Longitude,Elevation (${heightUnit.value}),Capital,Port,Citadel,Walls,Plaza,Temple,Shanty Town`; // headers
+ if (options.showMFCGMap) data += `,City Generator Link`;
+ data += "\n";
+
const valid = pack.burgs.filter(b => b.i && !b.removed); // all valid burgs
valid.forEach(b => {
@@ -463,8 +457,8 @@ function overviewBurgs() {
data += rn(b.population * populationRate * urbanization) + ",";
// add geography data
- data += mapCoordinates.lonW + (b.x / graphWidth) * mapCoordinates.lonT + ",";
- data += mapCoordinates.latN - (b.y / graphHeight) * mapCoordinates.latT + ","; // this is inverted in QGIS otherwise
+ data += getLatitude(b.y, 2) + ",";
+ data += getLongitude(b.x, 2) + ",";
data += parseInt(getHeight(pack.cells.h[b.cell])) + ",";
// add status data
@@ -474,7 +468,9 @@ function overviewBurgs() {
data += b.walls ? "walls," : ",";
data += b.plaza ? "plaza," : ",";
data += b.temple ? "temple," : ",";
- data += b.shanty ? "shanty town\n" : "\n";
+ data += b.shanty ? "shanty town," : ",";
+ if (options.showMFCGMap) data += getMFCGlink(b);
+ data += "\n";
});
const name = getFileName("Burgs") + ".csv";
@@ -508,19 +504,14 @@ function overviewBurgs() {
}
function importBurgNames(dataLoaded) {
- if (!dataLoaded) {
- tip("Cannot load the file, please check the format", false, "error");
- return;
- }
+ if (!dataLoaded) return tip("Cannot load the file, please check the format", false, "error");
const data = dataLoaded.split("\r\n");
- if (!data.length) {
- tip("Cannot parse the list, please check the file format", false, "error");
- return;
- }
+ if (!data.length) return tip("Cannot parse the list, please check the file format", false, "error");
- let change = [],
- message = `Burgs will be renamed as below. Please confirm`;
+ let change = [];
+ let message = `Burgs to be renamed as below:`;
message += `
| Id | Current name | New Name |
`;
+
const burgs = pack.burgs.filter(b => b.i && !b.removed);
for (let i = 0; i < data.length && i <= burgs.length; i++) {
const v = data[i];
@@ -529,45 +520,36 @@ function overviewBurgs() {
message += `| ${burgs[i].i} | ${burgs[i].name} | ${v} |
`;
}
message += `
`;
+
if (!change.length) message = "No changes found in the file. Please change some names to get a result";
alertMessage.innerHTML = message;
- $("#alert").dialog({
- title: "Burgs bulk renaming",
- width: "22em",
- position: {my: "center", at: "center", of: "svg"},
- buttons: {
- Cancel: function () {
- $(this).dialog("close");
- },
- Confirm: function () {
- for (let i = 0; i < change.length; i++) {
- const id = change[i].id;
- pack.burgs[id].name = change[i].name;
- burgLabels.select("[data-id='" + id + "']").text(change[i].name);
- }
- $(this).dialog("close");
- burgsOverviewAddLines();
- }
+ const onConfirm = () => {
+ for (let i = 0; i < change.length; i++) {
+ const id = change[i].id;
+ pack.burgs[id].name = change[i].name;
+ burgLabels.select("[data-id='" + id + "']").text(change[i].name);
}
+ burgsOverviewAddLines();
+ };
+
+ confirmationDialog({
+ title: "Burgs bulk renaming",
+ message,
+ confirm: "Rename",
+ onConfirm
});
}
function triggerAllBurgsRemove() {
- alertMessage.innerHTML = `Are you sure you want to remove all unlocked burgs except for capitals?
-
To remove a capital you have to remove a state first`;
- $("#alert").dialog({
- resizable: false,
- title: "Remove all burgs",
- buttons: {
- Remove: function () {
- $(this).dialog("close");
- removeAllBurgs();
- },
- Cancel: function () {
- $(this).dialog("close");
- }
- }
+ const number = pack.burgs.filter(b => b.i && !b.removed && !b.capital && !b.lock).length;
+ confirmationDialog({
+ title: `Remove ${number} burgs`,
+ message: `
+ Are you sure you want to remove all
unlocked burgs except for capitals?
+
To remove a capital you have to remove a state first`,
+ confirm: "Remove",
+ onConfirm: removeAllBurgs
});
}
@@ -575,4 +557,9 @@ function overviewBurgs() {
pack.burgs.filter(b => b.i && !(b.capital || b.lock)).forEach(b => removeBurg(b.i));
burgsOverviewAddLines();
}
+
+ function invertLock() {
+ pack.burgs = pack.burgs.map(burg => ({...burg, lock: !burg.lock}));
+ burgsOverviewAddLines();
+ }
}
diff --git a/modules/ui/cultures-editor.js b/modules/ui/cultures-editor.js
index 75aa2223..74c73535 100644
--- a/modules/ui/cultures-editor.js
+++ b/modules/ui/cultures-editor.js
@@ -62,9 +62,9 @@ function editCultures() {
// add line for each culture
function culturesEditorAddLines() {
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
- let lines = "",
- totalArea = 0,
- totalPopulation = 0;
+ let lines = "";
+ let totalArea = 0;
+ let totalPopulation = 0;
const emblemShapeGroup = document.getElementById("emblemShape").selectedOptions[0].parentNode.label;
const selectShape = emblemShapeGroup === "Diversiform";
@@ -84,7 +84,8 @@ function editCultures() {
lines += `
-
+
+
${c.cells}
@@ -96,19 +97,28 @@ function editCultures() {
${si(population)}
- ${selectShape ? `
` : ""}
+ ${
+ selectShape
+ ? `
`
+ : ""
+ }
`;
continue;
}
lines += `
-
+
+
${c.cells}
-
+
${si(area) + unit}
@@ -116,7 +126,11 @@ function editCultures() {
${si(population)}
- ${selectShape ? `
` : ""}
+ ${
+ selectShape
+ ? `
`
+ : ""
+ }
`;
}
@@ -136,6 +150,7 @@ function editCultures() {
body.querySelectorAll("div.states").forEach(el => el.addEventListener("click", selectCultureOnLineClick));
body.querySelectorAll("rect.fillRect").forEach(el => el.addEventListener("click", cultureChangeColor));
body.querySelectorAll("div > input.cultureName").forEach(el => el.addEventListener("input", cultureChangeName));
+ body.querySelectorAll("div > span.icon-cw").forEach(el => el.addEventListener("click", cultureRegenerateName));
body.querySelectorAll("div > input.statePower").forEach(el => el.addEventListener("input", cultureChangeExpansionism));
body.querySelectorAll("div > select.cultureType").forEach(el => el.addEventListener("change", cultureChangeType));
body.querySelectorAll("div > select.cultureBase").forEach(el => el.addEventListener("change", cultureChangeBase));
@@ -258,6 +273,13 @@ function editCultures() {
);
}
+ function cultureRegenerateName() {
+ const culture = +this.parentNode.dataset.id;
+ const name = Names.getCultureShort(culture);
+ this.parentNode.querySelector("input.cultureName").value = name;
+ pack.cultures[culture].name = name;
+ }
+
function cultureChangeExpansionism() {
const culture = +this.parentNode.dataset.id;
this.parentNode.dataset.expansionism = this.value;
@@ -526,7 +548,13 @@ function editCultures() {
// prepare svg
alertMessage.innerHTML = "
";
- const svg = d3.select("#alertMessage").insert("svg", "#cultureInfo").attr("id", "hierarchy").attr("width", width).attr("height", height).style("text-anchor", "middle");
+ const svg = d3
+ .select("#alertMessage")
+ .insert("svg", "#cultureInfo")
+ .attr("id", "hierarchy")
+ .attr("width", width)
+ .attr("height", height)
+ .style("text-anchor", "middle");
const graph = svg.append("g").attr("transform", `translate(10, -45)`);
const links = graph.append("g").attr("fill", "none").attr("stroke", "#aaaaaa");
const nodes = graph.append("g");
@@ -540,7 +568,24 @@ function editCultures() {
.enter()
.append("path")
.attr("d", d => {
- return "M" + d.source.x + "," + d.source.y + "C" + d.source.x + "," + (d.source.y * 3 + d.target.y) / 4 + " " + d.target.x + "," + (d.source.y * 2 + d.target.y) / 3 + " " + d.target.x + "," + d.target.y;
+ return (
+ "M" +
+ d.source.x +
+ "," +
+ d.source.y +
+ "C" +
+ d.source.x +
+ "," +
+ (d.source.y * 3 + d.target.y) / 4 +
+ " " +
+ d.target.x +
+ "," +
+ (d.source.y * 2 + d.target.y) / 3 +
+ " " +
+ d.target.x +
+ "," +
+ d.target.y
+ );
});
const node = nodes
@@ -661,7 +706,11 @@ function editCultures() {
$("#culturesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}});
tip("Click on culture to select, drag the circle to change culture", true);
- viewbox.style("cursor", "crosshair").on("click", selectCultureOnMapClick).call(d3.drag().on("start", dragCultureBrush)).on("touchmove mousemove", moveCultureBrush);
+ viewbox
+ .style("cursor", "crosshair")
+ .on("click", selectCultureOnMapClick)
+ .call(d3.drag().on("start", dragCultureBrush))
+ .on("touchmove mousemove", moveCultureBrush);
body.querySelector("div").classList.add("selected");
}
@@ -712,7 +761,14 @@ function editCultures() {
// change of append new element
if (exists.size()) exists.attr("data-culture", cultureNew).attr("fill", color).attr("stroke", color);
- else temp.append("polygon").attr("data-cell", i).attr("data-culture", cultureNew).attr("points", getPackPolygon(i)).attr("fill", color).attr("stroke", color);
+ else
+ temp
+ .append("polygon")
+ .attr("data-cell", i)
+ .attr("data-culture", cultureNew)
+ .attr("points", getPackPolygon(i))
+ .attr("fill", color)
+ .attr("stroke", color);
});
}
diff --git a/modules/ui/editors.js b/modules/ui/editors.js
index 9acf751c..5f648fa5 100644
--- a/modules/ui/editors.js
+++ b/modules/ui/editors.js
@@ -1,6 +1,7 @@
// module stub to store common functions for ui editors
"use strict";
+modules.editors = true;
restoreDefaultEvents(); // apply default viewbox events on load
// restore default viewbox events
@@ -28,7 +29,7 @@ function clicked() {
else if (grand.id === "burgIcons") editBurg();
else if (parent.id === "ice") editIce();
else if (parent.id === "terrain") editReliefIcon();
- else if (parent.id === "markers") editMarker();
+ else if (grand.id === "markers" || great.id === "markers") editMarker();
else if (grand.id === "coastline") editCoastline();
else if (great.id === "armies") editRegiment();
else if (pack.cells.t[i] === 1) {
@@ -259,20 +260,48 @@ function togglePort(burg) {
.attr("height", size);
}
+function getBurgSeed(burg) {
+ return burg.MFCG || Number(`${seed}${String(burg.i).padStart(4, 0)}`);
+}
+
+function getMFCGlink(burg) {
+ const {cells} = pack;
+ const {name, population, cell} = burg;
+ const burgSeed = getBurgSeed(burg);
+ const sizeRaw = 2.13 * Math.pow((population * populationRate) / urbanDensity, 0.385);
+ const size = minmax(Math.ceil(sizeRaw), 6, 100);
+ const people = rn(population * populationRate * urbanization);
+
+ const hub = +cells.road[cell] > 50;
+ const river = cells.r[cell] ? 1 : 0;
+
+ const coast = +burg.port;
+ const citadel = +burg.citadel;
+ const walls = +burg.walls;
+ const plaza = +burg.plaza;
+ const temple = +burg.temple;
+ const shanty = +burg.shanty;
+
+ const sea = coast && cells.haven[cell] ? getSeaDirections(cell) : "";
+ function getSeaDirections(i) {
+ const p1 = cells.p[i];
+ const p2 = cells.p[cells.haven[i]];
+ let deg = (Math.atan2(p2[1] - p1[1], p2[0] - p1[0]) * 180) / Math.PI - 90;
+ if (deg < 0) deg += 360;
+ const norm = rn(normalize(deg, 0, 360) * 2, 2); // 0 = south, 0.5 = west, 1 = north, 1.5 = east
+ return "&sea=" + norm;
+ }
+
+ const baseURL = "https://watabou.github.io/city-generator/?random=0&continuous=0";
+ const url = `${baseURL}&name=${name}&population=${people}&size=${size}&seed=${burgSeed}&hub=${hub}&river=${river}&coast=${coast}&citadel=${citadel}&plaza=${plaza}&temple=${temple}&walls=${walls}&shantytown=${shanty}${sea}`;
+ return url;
+}
+
function toggleBurgLock(burg) {
const b = pack.burgs[burg];
b.lock = b.lock ? 0 : 1;
}
-function showBurgLockTip(burg) {
- const b = pack.burgs[burg];
- if (b.lock) {
- tip("Click to Unlock burg and allow it to be change by regeneration tools");
- } else {
- tip("Click to Lock burg and prevent changes by regeneration tools");
- }
-}
-
// draw legend box
function drawLegend(name, data) {
legend.selectAll("*").remove(); // fully redraw every time
@@ -331,7 +360,15 @@ function drawLegend(name, data) {
const width = bbox.width + colOffset * 2;
const height = bbox.height + colOffset / 2 + vOffset;
- legend.insert("rect", ":first-child").attr("id", "legendBox").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height).attr("fill", backClr).attr("fill-opacity", opacity);
+ legend
+ .insert("rect", ":first-child")
+ .attr("id", "legendBox")
+ .attr("x", 0)
+ .attr("y", 0)
+ .attr("width", width)
+ .attr("height", height)
+ .attr("fill", backClr)
+ .attr("fill-opacity", opacity);
fitLegendBox();
}
@@ -384,7 +421,15 @@ function createPicker() {
const closePicker = () => contaiter.style("display", "none");
const contaiter = d3.select("body").append("svg").attr("id", "pickerContainer").attr("width", "100%").attr("height", "100%");
- contaiter.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%").attr("opacity", 0.2).on("mousemove", cl).on("click", closePicker);
+ contaiter
+ .append("rect")
+ .attr("x", 0)
+ .attr("y", 0)
+ .attr("width", "100%")
+ .attr("height", "100%")
+ .attr("opacity", 0.2)
+ .on("mousemove", cl)
+ .on("click", closePicker);
const picker = contaiter
.append("g")
.attr("id", "picker")
@@ -483,9 +528,25 @@ function createPicker() {
const width = bbox.width + 8;
const height = bbox.height + 9;
- picker.insert("rect", ":first-child").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height).attr("fill", "#ffffff").attr("stroke", "#5d4651").on("mousemove", pos);
+ picker
+ .insert("rect", ":first-child")
+ .attr("x", 0)
+ .attr("y", 0)
+ .attr("width", width)
+ .attr("height", height)
+ .attr("fill", "#ffffff")
+ .attr("stroke", "#5d4651")
+ .on("mousemove", pos);
picker.insert("text", ":first-child").attr("x", 291).attr("y", -10).attr("id", "pickerCloseText").text("✕");
- picker.insert("rect", ":first-child").attr("x", 288).attr("y", -21).attr("id", "pickerCloseRect").attr("width", 14).attr("height", 14).on("mousemove", cl).on("click", closePicker);
+ picker
+ .insert("rect", ":first-child")
+ .attr("x", 288)
+ .attr("y", -21)
+ .attr("id", "pickerCloseRect")
+ .attr("width", 14)
+ .attr("height", 14)
+ .on("mousemove", cl)
+ .on("click", closePicker);
picker.insert("text", ":first-child").attr("x", 12).attr("y", -10).attr("id", "pickerLabel").text("Color Picker").on("mousemove", pos);
picker.insert("rect", ":first-child").attr("x", 0).attr("y", -30).attr("width", width).attr("height", 30).attr("id", "pickerHeader").on("mousemove", pos);
picker.attr("transform", `translate(${(svgWidth - width) / 2},${(svgHeight - height) / 2})`);
@@ -695,23 +756,33 @@ function uploadFile(el, callback) {
fileReader.onload = loaded => callback(loaded.target.result);
}
-function highlightElement(element) {
- if (debug.select(".highlighted").size()) return; // allow only 1 highlight element simultaniosly
- const box = element.getBBox();
+function getBBox(element) {
+ const x = +element.getAttribute("x");
+ const y = +element.getAttribute("y");
+ const width = +element.getAttribute("width");
+ const height = +element.getAttribute("height");
+ return {x, y, width, height};
+}
+
+function highlightElement(element, zoom) {
+ if (debug.select(".highlighted").size()) return; // allow only 1 highlight element simultaneously
+ const box = element.tagName === "svg" ? getBBox(element) : element.getBBox();
const transform = element.getAttribute("transform") || null;
const enter = d3.transition().duration(1000).ease(d3.easeBounceOut);
const exit = d3.transition().duration(500).ease(d3.easeLinear);
- const highlight = debug.append("rect").attr("x", box.x).attr("y", box.y).attr("width", box.width).attr("height", box.height).attr("transform", transform);
+ const highlight = debug.append("rect").attr("x", box.x).attr("y", box.y).attr("width", box.width).attr("height", box.height);
+ highlight.classed("highlighted", 1).attr("transform", transform);
+ highlight.transition(enter).style("outline-offset", "0px").transition(exit).style("outline-color", "transparent").delay(1000).remove();
- highlight.classed("highlighted", 1).transition(enter).style("outline-offset", "0px").transition(exit).style("outline-color", "transparent").delay(1000).remove();
-
- const tr = parseTransform(transform);
- let x = box.x + box.width / 2;
- if (tr[0]) x += tr[0];
- let y = box.y + box.height / 2;
- if (tr[1]) y += tr[1];
- zoomTo(x, y, scale > 2 ? scale : 3, 1600);
+ if (zoom) {
+ const tr = parseTransform(transform);
+ let x = box.x + box.width / 2;
+ if (tr[0]) x += tr[0];
+ let y = box.y + box.height / 2;
+ if (tr[1]) y += tr[1];
+ zoomTo(x, y, scale > 2 ? scale : zoom, 1600);
+ }
}
function selectIcon(initial, callback) {
@@ -921,6 +992,7 @@ function selectIcon(initial, callback) {
}
}
+ input.oninput = e => callback(input.value);
table.onclick = e => {
if (e.target.tagName === "TD") {
input.value = e.target.innerHTML;
@@ -947,6 +1019,37 @@ function selectIcon(initial, callback) {
});
}
+function confirmationDialog(options) {
+ const {
+ title = "Confirm action",
+ message = "Are you sure you want to continue?
The action cannot be reverted",
+ cancel = "Cancel",
+ confirm = "Continue",
+ onCancel,
+ onConfirm
+ } = options;
+
+ const buttons = {
+ [confirm]: function () {
+ if (onConfirm) onConfirm();
+ $(this).dialog("close");
+ },
+ [cancel]: function () {
+ if (onCancel) onCancel();
+ $(this).dialog("close");
+ }
+ };
+
+ document.getElementById("alertMessage").innerHTML = message;
+ $("#alert").dialog({resizable: false, title, buttons});
+}
+
+// add and register event listeners to clean up on editor closure
+function listen(element, event, handler) {
+ element.addEventListener(event, handler);
+ return () => element.removeEventListener(event, handler);
+}
+
// Calls the refresh functionality on all editors currently open.
function refreshAllEditors() {
TIME && console.time("refreshAllEditors");
diff --git a/modules/ui/general.js b/modules/ui/general.js
index b001b9fd..356b3668 100644
--- a/modules/ui/general.js
+++ b/modules/ui/general.js
@@ -1,15 +1,17 @@
-// Module to store general UI functions
"use strict";
+// Module to store general UI functions
// fit full-screen map if window is resized
-$(window).resize(function (e) {
+window.addEventListener("resize", function (e) {
if (localStorage.getItem("mapWidth") && localStorage.getItem("mapHeight")) return;
mapWidthInput.value = window.innerWidth;
mapHeightInput.value = window.innerHeight;
changeMapSize();
});
-window.onbeforeunload = () => "Are you sure you want to navigate away?";
+if (location.hostname && location.hostname !== "localhost" && location.hostname !== "127.0.0.1") {
+ window.onbeforeunload = () => "Are you sure you want to navigate away?";
+}
// Tooltips
const tooltip = document.getElementById("tooltip");
@@ -19,12 +21,6 @@ document.getElementById("dialogs").addEventListener("mousemove", showDataTip);
document.getElementById("optionsContainer").addEventListener("mousemove", showDataTip);
document.getElementById("exitCustomization").addEventListener("mousemove", showDataTip);
-/**
- * @param {string} tip Tooltip text
- * @param {boolean} main Show above other tooltips
- * @param {string} type Message type (color): error / warn / success
- * @param {number} time Timeout to auto hide, ms
- */
function tip(tip = "Tip is undefined", main, type, time) {
tooltip.innerHTML = tip;
tooltip.style.background = "linear-gradient(0.1turn, #ffffff00, #5e5c5c80, #ffffff00)";
@@ -60,6 +56,15 @@ function showDataTip(e) {
tip(dataTip);
}
+function showElementLockTip(event) {
+ const locked = event?.target?.classList?.contains("icon-lock");
+ if (locked) {
+ tip("Click to unlock the element and allow it to be changed by regeneration tools");
+ } else {
+ tip("Click to lock the element and prevent changes to it by regeneration tools");
+ }
+}
+
const moved = debounce(mouseMove, 100);
function mouseMove() {
const point = d3.mouse(this);
@@ -84,7 +89,7 @@ function showNotes(e, i) {
document.getElementById("notes").style.display = "block";
document.getElementById("notesHeader").innerHTML = note.name;
document.getElementById("notesBody").innerHTML = note.legend;
- } else if (!options.pinNotes) {
+ } else if (!options.pinNotes && !markerEditor.offsetParent) {
document.getElementById("notes").style.display = "none";
document.getElementById("notesHeader").innerHTML = "";
document.getElementById("notesBody").innerHTML = "";
@@ -105,7 +110,8 @@ function showMapTooltip(point, e, i, g) {
if (group === "emblems" && e.target.tagName === "use") {
const parent = e.target.parentNode;
- const [g, type] = parent.id === "burgEmblems" ? [pack.burgs, "burg"] : parent.id === "provinceEmblems" ? [pack.provinces, "province"] : [pack.states, "state"];
+ const [g, type] =
+ parent.id === "burgEmblems" ? [pack.burgs, "burg"] : parent.id === "provinceEmblems" ? [pack.provinces, "province"] : [pack.states, "state"];
const i = +e.target.dataset.i;
if (event.shiftKey) highlightEmblemElement(type, g[i]);
@@ -140,7 +146,7 @@ function showMapTooltip(point, e, i, g) {
}
if (group === "labels") return tip("Click to edit the Label");
- if (group === "markers") return tip("Click to edit the Marker");
+ if (group === "markers") return tip("Click to edit the Marker and pin the marker note");
if (group === "ruler") {
const tag = e.target.tagName;
@@ -222,8 +228,8 @@ function updateCellInfo(point, i, g) {
const x = (infoX.innerHTML = rn(point[0]));
const y = (infoY.innerHTML = rn(point[1]));
const f = cells.f[i];
- infoLat.innerHTML = toDMS(mapCoordinates.latN - (y / graphHeight) * mapCoordinates.latT, "lat");
- infoLon.innerHTML = toDMS(mapCoordinates.lonW + (x / graphWidth) * mapCoordinates.lonT, "lon");
+ infoLat.innerHTML = toDMS(getLatitude(y, 4), "lat");
+ infoLon.innerHTML = toDMS(getLongitude(x, 4), "lon");
infoCell.innerHTML = i;
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
@@ -332,7 +338,20 @@ function highlightEmblemElement(type, el) {
if (type === "burg") {
const {x, y} = el;
- debug.append("circle").attr("cx", x).attr("cy", y).attr("r", 0).attr("fill", "none").attr("stroke", "#d0240f").attr("stroke-width", 1).attr("opacity", 1).transition(animation).attr("r", 20).attr("opacity", 0.1).attr("stroke-width", 0).remove();
+ debug
+ .append("circle")
+ .attr("cx", x)
+ .attr("cy", y)
+ .attr("r", 0)
+ .attr("fill", "none")
+ .attr("stroke", "#d0240f")
+ .attr("stroke-width", 1)
+ .attr("opacity", 1)
+ .transition(animation)
+ .attr("r", 20)
+ .attr("opacity", 0.1)
+ .attr("stroke-width", 0)
+ .remove();
return;
}
@@ -481,226 +500,3 @@ function showInfo() {
position: {my: "center", at: "center", of: "svg"}
});
}
-
-// prevent default browser behavior for FMG-used hotkeys
-document.addEventListener("keydown", event => {
- if (event.altKey && event.keyCode !== 18) event.preventDefault(); // disallow alt key combinations
- if (event.ctrlKey && event.code === "KeyS") event.preventDefault(); // disallow CTRL + C
- if ([112, 113, 117, 120, 9].includes(event.keyCode)) event.preventDefault(); // F1, F2, F6, F9, Tab
-});
-
-// Hotkeys, see github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys
-document.addEventListener("keyup", event => {
- if (!window.closeDialogs) return; // not all modules are loaded
- const canvas3d = document.getElementById("canvas3d"); // check if 3d mode is active
- const active = document.activeElement.tagName;
- if (active === "INPUT" || active === "SELECT" || active === "TEXTAREA") return; // don't trigger if user inputs a text
- if (active === "DIV" && document.activeElement.contentEditable === "true") return; // don't trigger if user inputs a text
- event.stopPropagation();
-
- const key = event.keyCode;
- const ctrl = event.ctrlKey || event.metaKey || key === 17;
- const shift = event.shiftKey || key === 16;
- const alt = event.altKey || key === 18;
-
- if (key === 112) showInfo();
- // "F1" to show info
- else if (key === 113) regeneratePrompt();
- // "F2" for new map
- else if (key === 113) regeneratePrompt();
- // "F2" for a new map
- else if (key === 117) quickSave();
- // "F6" for quick save
- else if (key === 120) quickLoad();
- // "F9" for quick load
- else if (key === 9) toggleOptions(event);
- // Tab to toggle options
- else if (key === 27) {
- closeDialogs();
- hideOptions();
- } // Escape to close all dialogs
- else if (key === 46) removeElementOnKey();
- // "Delete" to remove the selected element
- else if (key === 79 && canvas3d) toggle3dOptions();
- // "O" to toggle 3d options
- else if (ctrl && key === 81) toggleSaveReminder();
- // Ctrl + "Q" to toggle save reminder
- else if (ctrl && key === 83) saveMap();
- // Ctrl + "S" to save .map file
- else if (undo.offsetParent && ctrl && key === 90) undo.click();
- // Ctrl + "Z" to undo
- else if (redo.offsetParent && ctrl && key === 89) redo.click();
- // Ctrl + "Y" to redo
- else if (shift && key === 72) editHeightmap();
- // Shift + "H" to edit Heightmap
- else if (shift && key === 66) editBiomes();
- // Shift + "B" to edit Biomes
- else if (shift && key === 83) editStates();
- // Shift + "S" to edit States
- else if (shift && key === 80) editProvinces();
- // Shift + "P" to edit Provinces
- else if (shift && key === 68) editDiplomacy();
- // Shift + "D" to edit Diplomacy
- else if (shift && key === 67) editCultures();
- // Shift + "C" to edit Cultures
- else if (shift && key === 78) editNamesbase();
- // Shift + "N" to edit Namesbase
- else if (shift && key === 90) editZones();
- // Shift + "Z" to edit Zones
- else if (shift && key === 82) editReligions();
- // Shift + "R" to edit Religions
- else if (shift && key === 89) openEmblemEditor();
- // Shift + "Y" to edit Emblems
- else if (shift && key === 81) editUnits();
- // Shift + "Q" to edit Units
- else if (shift && key === 79) editNotes();
- // Shift + "O" to edit Notes
- else if (shift && key === 84) overviewBurgs();
- // Shift + "T" to open Burgs overview
- else if (shift && key === 86) overviewRivers();
- // Shift + "V" to open Rivers overview
- else if (shift && key === 77) overviewMilitary();
- // Shift + "M" to open Military overview
- else if (shift && key === 69) viewCellDetails();
- // Shift + "E" to open Cell Details
- else if (shift && key === 49) toggleAddBurg();
- // Shift + "1" to click to add Burg
- else if (shift && key === 50) toggleAddLabel();
- // Shift + "2" to click to add Label
- else if (shift && key === 51) toggleAddRiver();
- // Shift + "3" to click to add River
- else if (shift && key === 52) toggleAddRoute();
- // Shift + "4" to click to add Route
- else if (shift && key === 53) toggleAddMarker();
- // Shift + "5" to click to add Marker
- else if (alt && key === 66) console.table(pack.burgs);
- // Alt + "B" to log burgs data
- else if (alt && key === 83) console.table(pack.states);
- // Alt + "S" to log states data
- else if (alt && key === 67) console.table(pack.cultures);
- // Alt + "C" to log cultures data
- else if (alt && key === 82) console.table(pack.religions);
- // Alt + "R" to log religions data
- else if (alt && key === 70) console.table(pack.features);
- // Alt + "F" to log features data
- else if (key === 88) toggleTexture();
- // "X" to toggle Texture layer
- else if (key === 72) toggleHeight();
- // "H" to toggle Heightmap layer
- else if (key === 66) toggleBiomes();
- // "B" to toggle Biomes layer
- else if (key === 69) toggleCells();
- // "E" to toggle Cells layer
- else if (key === 71) toggleGrid();
- // "G" to toggle Grid layer
- else if (key === 79) toggleCoordinates();
- // "O" to toggle Coordinates layer
- else if (key === 87) toggleCompass();
- // "W" to toggle Compass Rose layer
- else if (key === 86) toggleRivers();
- // "V" to toggle Rivers layer
- else if (key === 70) toggleRelief();
- // "F" to toggle Relief icons layer
- else if (key === 67) toggleCultures();
- // "C" to toggle Cultures layer
- else if (key === 83) toggleStates();
- // "S" to toggle States layer
- else if (key === 80) toggleProvinces();
- // "P" to toggle Provinces layer
- else if (key === 90) toggleZones();
- // "Z" to toggle Zones
- else if (key === 68) toggleBorders();
- // "D" to toggle Borders layer
- else if (key === 82) toggleReligions();
- // "R" to toggle Religions layer
- else if (key === 85) toggleRoutes();
- // "U" to toggle Routes layer
- else if (key === 84) toggleTemp();
- // "T" to toggle Temperature layer
- else if (key === 78) togglePopulation();
- // "N" to toggle Population layer
- else if (key === 74) toggleIce();
- // "J" to toggle Ice layer
- else if (key === 65) togglePrec();
- // "A" to toggle Precipitation layer
- else if (key === 89) toggleEmblems();
- // "Y" to toggle Emblems layer
- else if (key === 76) toggleLabels();
- // "L" to toggle Labels layer
- else if (key === 73) toggleIcons();
- // "I" to toggle Icons layer
- else if (key === 77) toggleMilitary();
- // "M" to toggle Military layer
- else if (key === 75) toggleMarkers();
- // "K" to toggle Markers layer
- else if (key === 187) toggleRulers();
- // Equal (=) to toggle Rulers
- else if (key === 189) toggleScaleBar();
- // Minus (-) to toggle Scale bar
- else if (key === 37) zoom.translateBy(svg, 10, 0);
- // Left to scroll map left
- else if (key === 39) zoom.translateBy(svg, -10, 0);
- // Right to scroll map right
- else if (key === 38) zoom.translateBy(svg, 0, 10);
- // Up to scroll map up
- else if (key === 40) zoom.translateBy(svg, 0, -10);
- // Up to scroll map up
- else if (key === 107 || key === 109) pressNumpadSign(key);
- // Numpad Plus/Minus to zoom map or change brush size
- else if (key === 48 || key === 96) resetZoom(1000);
- // 0 to reset zoom
- else if (key === 49 || key === 97) zoom.scaleTo(svg, 1);
- // 1 to zoom to 1
- else if (key === 50 || key === 98) zoom.scaleTo(svg, 2);
- // 2 to zoom to 2
- else if (key === 51 || key === 99) zoom.scaleTo(svg, 3);
- // 3 to zoom to 3
- else if (key === 52 || key === 100) zoom.scaleTo(svg, 4);
- // 4 to zoom to 4
- else if (key === 53 || key === 101) zoom.scaleTo(svg, 5);
- // 5 to zoom to 5
- else if (key === 54 || key === 102) zoom.scaleTo(svg, 6);
- // 6 to zoom to 6
- else if (key === 55 || key === 103) zoom.scaleTo(svg, 7);
- // 7 to zoom to 7
- else if (key === 56 || key === 104) zoom.scaleTo(svg, 8);
- // 8 to zoom to 8
- else if (key === 57 || key === 105) zoom.scaleTo(svg, 9);
- // 9 to zoom to 9
- else if (ctrl) pressControl(); // Control to toggle mode
-});
-
-function pressNumpadSign(key) {
- // if brush sliders are displayed, decrease brush size
- let brush = null;
- const d = key === 107 ? 1 : -1;
-
- if (brushRadius.offsetParent) brush = document.getElementById("brushRadius");
- else if (biomesManuallyBrush.offsetParent) brush = document.getElementById("biomesManuallyBrush");
- else if (statesManuallyBrush.offsetParent) brush = document.getElementById("statesManuallyBrush");
- else if (provincesManuallyBrush.offsetParent) brush = document.getElementById("provincesManuallyBrush");
- else if (culturesManuallyBrush.offsetParent) brush = document.getElementById("culturesManuallyBrush");
- else if (zonesBrush.offsetParent) brush = document.getElementById("zonesBrush");
- else if (religionsManuallyBrush.offsetParent) brush = document.getElementById("religionsManuallyBrush");
-
- if (brush) {
- const value = Math.max(Math.min(+brush.value + d, +brush.max), +brush.min);
- brush.value = document.getElementById(brush.id + "Number").value = value;
- return;
- }
-
- const scaleBy = key === 107 ? 1.2 : 0.8;
- zoom.scaleBy(svg, scaleBy); // if no, zoom map
-}
-
-function pressControl() {
- if (zonesRemove.offsetParent) {
- zonesRemove.classList.contains("pressed") ? zonesRemove.classList.remove("pressed") : zonesRemove.classList.add("pressed");
- }
-}
-
-// trigger trash button click on "Delete" keypress
-function removeElementOnKey() {
- $(".dialog:visible .fastDelete").click();
- $("button:visible:contains('Remove')").click();
-}
diff --git a/modules/ui/heightmap-editor.js b/modules/ui/heightmap-editor.js
index 739e9a24..b4ebde9f 100644
--- a/modules/ui/heightmap-editor.js
+++ b/modules/ui/heightmap-editor.js
@@ -15,15 +15,9 @@ function editHeightmap() {
title: "Edit Heightmap",
width: "28em",
buttons: {
- Erase: function () {
- enterHeightmapEditMode("erase");
- },
- Keep: function () {
- enterHeightmapEditMode("keep");
- },
- Risk: function () {
- enterHeightmapEditMode("risk");
- },
+ Erase: () => enterHeightmapEditMode("erase"),
+ Keep: () => enterHeightmapEditMode("keep"),
+ Risk: () => enterHeightmapEditMode("risk"),
Cancel: function () {
$(this).dialog("close");
}
@@ -87,7 +81,16 @@ 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();
@@ -130,7 +133,8 @@ function editHeightmap() {
// Exit customization mode
function finalizeHeightmap() {
- if (viewbox.select("#heights").selectAll("*").size() < 200) return tip("Insufficient land area! There should be at least 200 land cells to finalize the heightmap", null, "error");
+ if (viewbox.select("#heights").selectAll("*").size() < 200)
+ return tip("Insufficient land area! There should be at least 200 land cells to finalize the heightmap", null, "error");
if (document.getElementById("imageConverter").offsetParent) return tip("Please exit the Image Conversion mode first", null, "error");
delete window.edits; // remove global variable
@@ -216,7 +220,7 @@ function editHeightmap() {
Lakes.generateName();
Military.generate();
- addMarkers();
+ Markers.generate();
addZones();
TIME && console.timeEnd("regenerateErasedData");
INFO && console.groupEnd("Edit Heightmap");
@@ -608,7 +612,7 @@ function editHeightmap() {
const interpolate = d3.interpolateRound(power, 1);
const land = changeOnlyLand.checked;
function lim(v) {
- return Math.max(Math.min(v, 100), land ? 20 : 0);
+ return minmax(v, land ? 20 : 0, 100);
}
const h = grid.cells.h;
@@ -618,7 +622,10 @@ function editHeightmap() {
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 === "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);
@@ -767,15 +774,29 @@ function editHeightmap() {
const TempY = `
y:`;
const TempX = `
x:`;
- const Height = `
h:`;
+ 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) {
@@ -1170,10 +1191,14 @@ 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 => {
- convertColors.value = number;
- heightsFromImage(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);
+ }
+ );
}
function setOverlayOpacity(v) {
diff --git a/modules/ui/hotkeys.js b/modules/ui/hotkeys.js
new file mode 100644
index 00000000..9081d002
--- /dev/null
+++ b/modules/ui/hotkeys.js
@@ -0,0 +1,153 @@
+"use strict";
+// Hotkeys, see github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys
+document.addEventListener("keydown", handleKeydown);
+document.addEventListener("keyup", handleKeyup);
+
+function handleKeydown(event) {
+ const {code, ctrlKey, altKey} = event;
+ if (altKey && !ctrlKey) event.preventDefault(); // disallow alt key combinations
+ if (ctrlKey && ["KeyS", "KeyC"].includes(code)) event.preventDefault(); // disallow CTRL + S and CTRL + C
+ if (["F1", "F2", "F6", "F9", "Tab"].includes(code)) event.preventDefault(); // disallow default Fn and Tab
+}
+
+function handleKeyup(event) {
+ if (!modules.editors) return; // if editors are not loaded, do nothing
+
+ const {tagName, contentEditable} = document.activeElement;
+ if (["INPUT", "SELECT", "TEXTAREA"].includes(tagName)) return; // don't trigger if user inputs text
+ if (tagName === "DIV" && contentEditable === "true") return; // don't trigger if user inputs a text
+ if (document.getSelection().toString()) return; // don't trigger if user selects text
+ event.stopPropagation();
+
+ const {code, key, ctrlKey, metaKey, shiftKey, altKey} = event;
+ const ctrl = ctrlKey || metaKey || key === "Control";
+ const shift = shiftKey || key === "Shift";
+ const alt = altKey || key === "Alt";
+
+ if (code === "F1") showInfo();
+ else if (code === "F2") regeneratePrompt("hotkey");
+ else if (code === "F6") quickSave();
+ else if (code === "F9") quickLoad();
+ else if (code === "Tab") toggleOptions(event);
+ else if (code === "Escape") closeAllDialogs();
+ else if (code === "Delete") removeElementOnKey();
+ else if (code === "KeyO" && document.getElementById("canvas3d")) toggle3dOptions();
+ else if (ctrl && code === "KeyQ") toggleSaveReminder();
+ else if (ctrl && code === "KeyS") dowloadMap();
+ else if (ctrl && code === "KeyC") saveToDropbox();
+ else if (ctrl && code === "KeyZ" && undo.offsetParent) undo.click();
+ else if (ctrl && code === "KeyY" && redo.offsetParent) redo.click();
+ else if (shift && code === "KeyH") editHeightmap();
+ else if (shift && code === "KeyB") editBiomes();
+ else if (shift && code === "KeyS") editStates();
+ else if (shift && code === "KeyP") editProvinces();
+ else if (shift && code === "KeyD") editDiplomacy();
+ else if (shift && code === "KeyC") editCultures();
+ else if (shift && code === "KeyN") editNamesbase();
+ else if (shift && code === "KeyZ") editZones();
+ else if (shift && code === "KeyR") editReligions();
+ else if (shift && code === "KeyY") openEmblemEditor();
+ else if (shift && code === "KeyQ") editUnits();
+ else if (shift && code === "KeyO") editNotes();
+ else if (shift && code === "KeyT") overviewBurgs();
+ else if (shift && code === "KeyV") overviewRivers();
+ else if (shift && code === "KeyM") overviewMilitary();
+ else if (shift && code === "KeyK") overviewMarkers();
+ else if (shift && code === "KeyE") viewCellDetails();
+ else if (key === "!") toggleAddBurg();
+ else if (key === "@") toggleAddLabel();
+ else if (key === "#") toggleAddRiver();
+ else if (key === "$") toggleAddRoute();
+ else if (key === "%") toggleAddMarker();
+ else if (alt && code === "KeyB") console.table(pack.burgs);
+ else if (alt && code === "KeyS") console.table(pack.states);
+ else if (alt && code === "KeyC") console.table(pack.cultures);
+ else if (alt && code === "KeyR") console.table(pack.religions);
+ else if (alt && code === "KeyF") console.table(pack.features);
+ else if (code === "KeyX") toggleTexture();
+ else if (code === "KeyH") toggleHeight();
+ else if (code === "KeyB") toggleBiomes();
+ else if (code === "KeyE") toggleCells();
+ else if (code === "KeyG") toggleGrid();
+ else if (code === "KeyO") toggleCoordinates();
+ else if (code === "KeyW") toggleCompass();
+ else if (code === "KeyV") toggleRivers();
+ else if (code === "KeyF") toggleRelief();
+ else if (code === "KeyC") toggleCultures();
+ else if (code === "KeyS") toggleStates();
+ else if (code === "KeyP") toggleProvinces();
+ else if (code === "KeyZ") toggleZones();
+ else if (code === "KeyD") toggleBorders();
+ else if (code === "KeyR") toggleReligions();
+ else if (code === "KeyU") toggleRoutes();
+ else if (code === "KeyT") toggleTemp();
+ else if (code === "KeyN") togglePopulation();
+ else if (code === "KeyJ") toggleIce();
+ else if (code === "KeyA") togglePrec();
+ else if (code === "KeyY") toggleEmblems();
+ else if (code === "KeyL") toggleLabels();
+ else if (code === "KeyI") toggleIcons();
+ else if (code === "KeyM") toggleMilitary();
+ else if (code === "KeyK") toggleMarkers();
+ else if (code === "Equal") toggleRulers();
+ else if (code === "Slash") toggleScaleBar();
+ else if (code === "ArrowLeft") zoom.translateBy(svg, 10, 0);
+ else if (code === "ArrowRight") zoom.translateBy(svg, -10, 0);
+ else if (code === "ArrowUp") zoom.translateBy(svg, 0, 10);
+ else if (code === "ArrowDown") zoom.translateBy(svg, 0, -10);
+ else if (key === "+" || key === "-") pressNumpadSign(key);
+ else if (key === "0") resetZoom(1000);
+ else if (key === "1") zoom.scaleTo(svg, 1);
+ else if (key === "2") zoom.scaleTo(svg, 2);
+ else if (key === "3") zoom.scaleTo(svg, 3);
+ else if (key === "4") zoom.scaleTo(svg, 4);
+ else if (key === "5") zoom.scaleTo(svg, 5);
+ else if (key === "6") zoom.scaleTo(svg, 6);
+ else if (key === "7") zoom.scaleTo(svg, 7);
+ else if (key === "8") zoom.scaleTo(svg, 8);
+ else if (key === "9") zoom.scaleTo(svg, 9);
+ else if (ctrl) toggleMode();
+}
+
+function pressNumpadSign(key) {
+ const change = key === "+" ? 1 : -1;
+ let brush = null;
+
+ if (brushRadius.offsetParent) brush = document.getElementById("brushRadius");
+ else if (biomesManuallyBrush.offsetParent) brush = document.getElementById("biomesManuallyBrush");
+ else if (statesManuallyBrush.offsetParent) brush = document.getElementById("statesManuallyBrush");
+ else if (provincesManuallyBrush.offsetParent) brush = document.getElementById("provincesManuallyBrush");
+ else if (culturesManuallyBrush.offsetParent) brush = document.getElementById("culturesManuallyBrush");
+ else if (zonesBrush.offsetParent) brush = document.getElementById("zonesBrush");
+ else if (religionsManuallyBrush.offsetParent) brush = document.getElementById("religionsManuallyBrush");
+
+ if (brush) {
+ const value = minmax(+brush.value + change, +brush.min, +brush.max);
+ brush.value = document.getElementById(brush.id + "Number").value = value;
+ return;
+ }
+
+ const scaleBy = key === "+" ? 1.2 : 0.8;
+ zoom.scaleBy(svg, scaleBy); // if no brush elements displayed, zoom map
+}
+
+function toggleMode() {
+ if (zonesRemove.offsetParent) {
+ zonesRemove.classList.contains("pressed") ? zonesRemove.classList.remove("pressed") : zonesRemove.classList.add("pressed");
+ }
+}
+
+function removeElementOnKey() {
+ const fastDelete = Array.from(document.querySelectorAll("[role='dialog'] .fastDelete")).find(dialog => dialog.style.display !== "none");
+ if (fastDelete) fastDelete.click();
+
+ const visibleDialogs = Array.from(document.querySelectorAll("[role='dialog']")).filter(dialog => dialog.style.display !== "none");
+ if (!visibleDialogs.length) return;
+
+ visibleDialogs.forEach(dialog => dialog.querySelectorAll("button").forEach(button => button.textContent === "Remove" && button.click()));
+}
+
+function closeAllDialogs() {
+ closeDialogs();
+ hideOptions();
+}
diff --git a/modules/ui/labels-editor.js b/modules/ui/labels-editor.js
index 18b1f8f6..3d7a3b7f 100644
--- a/modules/ui/labels-editor.js
+++ b/modules/ui/labels-editor.js
@@ -11,7 +11,9 @@ function editLabel() {
viewbox.on("touchmove mousemove", showEditorTips);
$("#labelEditor").dialog({
- title: "Edit Label", resizable: false, width: fitContent(),
+ title: "Edit Label",
+ resizable: false,
+ width: fitContent(),
position: {my: "center top+10", at: "bottom", of: text, collision: "fit"},
close: closeLabelEditor
});
@@ -49,8 +51,8 @@ function editLabel() {
function showEditorTips() {
showMainTip();
- if (d3.event.target.parentNode.parentNode.id === elSelected.attr("id")) tip("Drag to shift the label"); else
- if (d3.event.target.parentNode.id === "controlPoints") {
+ if (d3.event.target.parentNode.parentNode.id === elSelected.attr("id")) tip("Drag to shift the label");
+ else if (d3.event.target.parentNode.id === "controlPoints") {
if (d3.event.target.tagName === "circle") tip("Drag to move, click to delete the control point");
if (d3.event.target.tagName === "path") tip("Click to add a control point");
}
@@ -58,10 +60,18 @@ function editLabel() {
function selectLabelGroup(text) {
const group = text.parentNode.id;
+
+ if (group === "states" || group === "burgLabels") {
+ document.getElementById("labelGroupShow").style.display = "none";
+ return;
+ }
+
+ hideGroupSection();
const select = document.getElementById("labelGroupSelect");
select.options.length = 0; // remove all options
- labels.selectAll(":scope > g").each(function() {
+ labels.selectAll(":scope > g").each(function () {
+ if (this.id === "states") return;
if (this.id === "burgLabels") return;
select.options.add(new Option(this.id, this.id, false, this.id === group));
});
@@ -81,12 +91,19 @@ function editLabel() {
const l = path.getTotalLength();
if (!l) return;
const increment = l / Math.max(Math.ceil(l / 200), 2);
- for (let i=0; i <= l; i += increment) {addControlPoint(path.getPointAtLength(i));}
+ for (let i = 0; i <= l; i += increment) {
+ addControlPoint(path.getPointAtLength(i));
+ }
}
function addControlPoint(point) {
- debug.select("#controlPoints").append("circle")
- .attr("cx", point.x).attr("cy", point.y).attr("r", 2.5).attr("stroke-width", .8)
+ debug
+ .select("#controlPoints")
+ .append("circle")
+ .attr("cx", point.x)
+ .attr("cy", point.y)
+ .attr("r", 2.5)
+ .attr("stroke-width", 0.8)
.call(d3.drag().on("drag", dragControlPoint))
.on("click", clickControlPoint);
}
@@ -101,9 +118,12 @@ function editLabel() {
const path = document.getElementById("textPath_" + elSelected.attr("id"));
lineGen.curve(d3.curveBundle.beta(1));
const points = [];
- debug.select("#controlPoints").selectAll("circle").each(function() {
- points.push([this.getAttribute("cx"), this.getAttribute("cy")]);
- });
+ debug
+ .select("#controlPoints")
+ .selectAll("circle")
+ .each(function () {
+ points.push([this.getAttribute("cx"), this.getAttribute("cy")]);
+ });
const d = round(lineGen(points));
path.setAttribute("d", d);
debug.select("#controlPoints > path").attr("d", d);
@@ -118,52 +138,63 @@ function editLabel() {
const point = d3.mouse(this);
const dists = [];
- debug.select("#controlPoints").selectAll("circle").each(function() {
- const x = +this.getAttribute("cx");
- const y = +this.getAttribute("cy");
- dists.push((point[0] - x) ** 2 + (point[1] - y) ** 2);
- });
+ debug
+ .select("#controlPoints")
+ .selectAll("circle")
+ .each(function () {
+ const x = +this.getAttribute("cx");
+ const y = +this.getAttribute("cy");
+ dists.push((point[0] - x) ** 2 + (point[1] - y) ** 2);
+ });
let index = dists.length;
if (dists.length > 1) {
- const sorted = dists.slice(0).sort((a, b) => a-b);
+ const sorted = dists.slice(0).sort((a, b) => a - b);
const closest = dists.indexOf(sorted[0]);
const next = dists.indexOf(sorted[1]);
- if (closest <= next) index = closest+1; else index = next+1;
+ if (closest <= next) index = closest + 1;
+ else index = next + 1;
}
const before = ":nth-child(" + (index + 2) + ")";
- debug.select("#controlPoints").insert("circle", before)
- .attr("cx", point[0]).attr("cy", point[1]).attr("r", 2.5).attr("stroke-width", .8)
+ debug
+ .select("#controlPoints")
+ .insert("circle", before)
+ .attr("cx", point[0])
+ .attr("cy", point[1])
+ .attr("r", 2.5)
+ .attr("stroke-width", 0.8)
.call(d3.drag().on("drag", dragControlPoint))
.on("click", clickControlPoint);
- redrawLabelPath();
+ redrawLabelPath();
}
function dragLabel() {
const tr = parseTransform(elSelected.attr("transform"));
- const dx = +tr[0] - d3.event.x, dy = +tr[1] - d3.event.y;
-
- d3.event.on("drag", function() {
- const x = d3.event.x, y = d3.event.y;
- const transform = `translate(${(dx+x)},${(dy+y)})`;
+ const dx = +tr[0] - d3.event.x,
+ dy = +tr[1] - d3.event.y;
+
+ d3.event.on("drag", function () {
+ const x = d3.event.x,
+ y = d3.event.y;
+ const transform = `translate(${dx + x},${dy + y})`;
elSelected.attr("transform", transform);
debug.select("#controlPoints").attr("transform", transform);
});
}
function showGroupSection() {
- document.querySelectorAll("#labelEditor > button").forEach(el => el.style.display = "none");
+ document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "none"));
document.getElementById("labelGroupSection").style.display = "inline-block";
}
function hideGroupSection() {
- document.querySelectorAll("#labelEditor > button").forEach(el => el.style.display = "inline-block");
+ document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "inline-block"));
document.getElementById("labelGroupSection").style.display = "none";
document.getElementById("labelGroupInput").style.display = "none";
document.getElementById("labelGroupInput").value = "";
- document.getElementById("labelGroupSelect").style.display = "inline-block";
+ document.getElementById("labelGroupSelect").style.display = "inline-block";
}
function changeGroup() {
@@ -178,12 +209,18 @@ function editLabel() {
} else {
labelGroupInput.style.display = "none";
labelGroupSelect.style.display = "inline-block";
- }
+ }
}
function createNewGroup() {
- if (!this.value) {tip("Please provide a valid group name"); return;}
- const group = this.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, "");
+ if (!this.value) {
+ tip("Please provide a valid group name");
+ return;
+ }
+ const group = this.value
+ .toLowerCase()
+ .replace(/ /g, "_")
+ .replace(/[^\w\s]/gi, "");
if (document.getElementById(group)) {
tip("Element with this id already exists. Please provide a unique name", false, "error");
@@ -223,57 +260,64 @@ function editLabel() {
alertMessage.innerHTML = `Are you sure you want to remove
${basic ? "all elements in the group" : "the entire label group"}?
Labels to be removed: ${count}`;
- $("#alert").dialog({resizable: false, title: "Remove route group",
+ $("#alert").dialog({
+ resizable: false,
+ title: "Remove route group",
buttons: {
- Remove: function() {
+ Remove: function () {
$(this).dialog("close");
$("#labelEditor").dialog("close");
hideGroupSection();
- labels.select("#"+group).selectAll("text").each(function() {
- document.getElementById("textPath_" + this.id).remove();
- this.remove();
- });
- if (!basic) labels.select("#"+group).remove();
+ labels
+ .select("#" + group)
+ .selectAll("text")
+ .each(function () {
+ document.getElementById("textPath_" + this.id).remove();
+ this.remove();
+ });
+ if (!basic) labels.select("#" + group).remove();
},
- Cancel: function() {$(this).dialog("close");}
+ Cancel: function () {
+ $(this).dialog("close");
+ }
}
});
}
-
+
function showTextSection() {
- document.querySelectorAll("#labelEditor > button").forEach(el => el.style.display = "none");
+ document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "none"));
document.getElementById("labelTextSection").style.display = "inline-block";
}
function hideTextSection() {
- document.querySelectorAll("#labelEditor > button").forEach(el => el.style.display = "inline-block");
+ document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "inline-block"));
document.getElementById("labelTextSection").style.display = "none";
}
-
+
function changeText() {
const input = document.getElementById("labelText").value;
const el = elSelected.select("textPath").node();
- const example = d3.select(elSelected.node().parentNode)
- .append("text").attr("x", 0).attr("x", 0)
- .attr("font-size", el.getAttribute("font-size")).node();
+ const example = d3.select(elSelected.node().parentNode).append("text").attr("x", 0).attr("x", 0).attr("font-size", el.getAttribute("font-size")).node();
const lines = input.split("|");
const top = (lines.length - 1) / -2; // y offset
- const inner = lines.map((l, d) => {
- example.innerHTML = l;
- const left = example.getBBox().width / -2; // x offset
- return `${l}`;
- }).join("");
+ const inner = lines
+ .map((l, d) => {
+ example.innerHTML = l;
+ const left = example.getBBox().width / -2; // x offset
+ return `${l}`;
+ })
+ .join("");
el.innerHTML = inner;
example.remove();
- if (elSelected.attr("id").slice(0,10) === "stateLabel") tip("Use States Editor to change an actual state name, not just a label", false, "warning");
+ if (elSelected.attr("id").slice(0, 10) === "stateLabel") tip("Use States Editor to change an actual state name, not just a label", false, "warning");
}
function generateRandomName() {
let name = "";
- if (elSelected.attr("id").slice(0,10) === "stateLabel") {
+ if (elSelected.attr("id").slice(0, 10) === "stateLabel") {
const id = +elSelected.attr("id").slice(10);
const culture = pack.states[id].culture;
name = Names.getState(Names.getCulture(culture, 4, 7, ""), culture);
@@ -293,12 +337,12 @@ function editLabel() {
}
function showSizeSection() {
- document.querySelectorAll("#labelEditor > button").forEach(el => el.style.display = "none");
+ document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "none"));
document.getElementById("labelSizeSection").style.display = "inline-block";
}
function hideSizeSection() {
- document.querySelectorAll("#labelEditor > button").forEach(el => el.style.display = "inline-block");
+ document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "inline-block"));
document.getElementById("labelSizeSection").style.display = "none";
}
@@ -317,7 +361,7 @@ function editLabel() {
const bbox = elSelected.node().getBBox();
const c = [bbox.x + bbox.width / 2, bbox.y + bbox.height / 2];
const path = defs.select("#textPath_" + elSelected.attr("id"));
- path.attr("d", `M${c[0]-bbox.width},${c[1]}h${bbox.width*2}`);
+ path.attr("d", `M${c[0] - bbox.width},${c[1]}h${bbox.width * 2}`);
drawControlPointsAndLine();
}
@@ -329,15 +373,19 @@ function editLabel() {
function removeLabel() {
alertMessage.innerHTML = "Are you sure you want to remove the label?";
- $("#alert").dialog({resizable: false, title: "Remove label",
+ $("#alert").dialog({
+ resizable: false,
+ title: "Remove label",
buttons: {
- Remove: function() {
+ Remove: function () {
$(this).dialog("close");
defs.select("#textPath_" + elSelected.attr("id")).remove();
elSelected.remove();
$("#labelEditor").dialog("close");
},
- Cancel: function() {$(this).dialog("close");}
+ Cancel: function () {
+ $(this).dialog("close");
+ }
}
});
}
diff --git a/modules/ui/layers.js b/modules/ui/layers.js
index 622cfc96..baae4c88 100644
--- a/modules/ui/layers.js
+++ b/modules/ui/layers.js
@@ -47,8 +47,7 @@ function changePreset(preset) {
.querySelectorAll("li")
.forEach(function (e) {
if (layers.includes(e.id) && !layerIsOn(e.id)) e.click();
- // turn on
- else if (!layers.includes(e.id) && layerIsOn(e.id)) e.click(); // turn off
+ else if (!layers.includes(e.id) && layerIsOn(e.id)) e.click();
});
layersPreset.value = preset;
localStorage.setItem("preset", preset);
@@ -121,6 +120,7 @@ function restoreLayers() {
if (layerIsOn("toggleReligions")) drawReligions();
if (layerIsOn("toggleIce")) drawIce();
if (layerIsOn("toggleEmblems")) drawEmblems();
+ if (layerIsOn("toggleMarkers")) drawMarkers();
// some layers are rendered each time, remove them if they are not on
if (!layerIsOn("toggleBorders")) borders.selectAll("path").remove();
@@ -196,7 +196,8 @@ function drawHeightmap() {
for (const i of d3.range(20, 101)) {
if (paths[i].length < 10) continue;
const color = getColor(i, scheme);
- if (terracing) terrs.append("path").attr("d", paths[i]).attr("transform", "translate(.7,1.4)").attr("fill", d3.color(color).darker(terracing)).attr("data-height", i);
+ if (terracing)
+ terrs.append("path").attr("d", paths[i]).attr("transform", "translate(.7,1.4)").attr("fill", d3.color(color).darker(terracing)).attr("data-height", i);
terrs.append("path").attr("d", paths[i]).attr("fill", color).attr("data-height", i);
}
@@ -798,7 +799,10 @@ function drawReligions() {
if (!vArray[r]) vArray[r] = [];
vArray[r].push(points);
body[r] += "M" + points.join("L");
- gap[r] += "M" + vertices.p[chain[0][0]] + chain.reduce((r2, v, i, d) => (!i ? r2 : !v[2] ? r2 + "L" + vertices.p[v[0]] : d[i + 1] && !d[i + 1][2] ? r2 + "M" + vertices.p[v[0]] : r2), "");
+ gap[r] +=
+ "M" +
+ vertices.p[chain[0][0]] +
+ chain.reduce((r2, v, i, d) => (!i ? r2 : !v[2] ? r2 + "L" + vertices.p[v[0]] : d[i + 1] && !d[i + 1][2] ? r2 + "M" + vertices.p[v[0]] : r2), "");
}
const bodyData = body.map((p, i) => [p.length > 10 ? p : null, i, religions[i].color]).filter(d => d[0]);
@@ -965,7 +969,14 @@ function drawStates() {
const bodyString = bodyData.map(d => ``).join("");
const gapString = gapData.map(d => ``).join("");
const clipString = bodyData.map(d => ``).join("");
- const haloString = haloData.map(d => ``).join("");
+ const haloString = haloData
+ .map(
+ d =>
+ ``
+ )
+ .join("");
statesBody.html(bodyString + gapString);
defs.select("#statePaths").html(clipString);
@@ -1217,7 +1228,10 @@ function getProvincesVertices() {
if (!vArray[p]) vArray[p] = [];
vArray[p].push(points);
body[p] += "M" + points.join("L");
- gap[p] += "M" + vertices.p[chain[0][0]] + chain.reduce((r, v, i, d) => (!i ? r : !v[2] ? r + "L" + vertices.p[v[0]] : d[i + 1] && !d[i + 1][2] ? r + "M" + vertices.p[v[0]] : r), "");
+ gap[p] +=
+ "M" +
+ vertices.p[chain[0][0]] +
+ chain.reduce((r, v, i, d) => (!i ? r : !v[2] ? r + "L" + vertices.p[v[0]] : d[i + 1] && !d[i + 1][2] ? r + "M" + vertices.p[v[0]] : r), "");
}
// find province visual center
@@ -1298,7 +1312,12 @@ function drawGrid() {
const maxWidth = Math.max(+mapWidthInput.value, graphWidth);
const maxHeight = Math.max(+mapHeightInput.value, graphHeight);
- d3.select(pattern).attr("stroke", stroke).attr("stroke-width", width).attr("stroke-dasharray", dasharray).attr("stroke-linecap", linecap).attr("patternTransform", tr);
+ d3.select(pattern)
+ .attr("stroke", stroke)
+ .attr("stroke-width", width)
+ .attr("stroke-dasharray", dasharray)
+ .attr("stroke-linecap", linecap)
+ .attr("patternTransform", tr);
gridOverlay
.append("rect")
.attr("width", maxWidth)
@@ -1416,8 +1435,8 @@ function toggleTexture(event) {
turnButtonOn("toggleTexture");
// append default texture image selected by default. Don't append on load to not harm performance
if (!texture.selectAll("*").size()) {
- const x = +styleTextureShiftX.value,
- y = +styleTextureShiftY.value;
+ const x = +styleTextureShiftX.value;
+ const y = +styleTextureShiftY.value;
const image = texture
.append("image")
.attr("id", "textureImage")
@@ -1425,18 +1444,14 @@ function toggleTexture(event) {
.attr("y", y)
.attr("width", graphWidth - x)
.attr("height", graphHeight - y)
- .attr("xlink:href", getDefaultTexture())
.attr("preserveAspectRatio", "xMidYMid slice");
- if (styleTextureInput.value !== "default") getBase64(styleTextureInput.value, base64 => image.attr("xlink:href", base64));
+ getBase64(styleTextureInput.value, base64 => image.attr("xlink:href", base64));
}
$("#texture").fadeIn();
zoom.scaleBy(svg, 1.00001); // enforce browser re-draw
if (event && isCtrlClick(event)) editStyle("texture");
} else {
- if (event && isCtrlClick(event)) {
- editStyle("texture");
- return;
- }
+ if (event && isCtrlClick(event)) return editStyle("texture");
$("#texture").fadeOut();
turnButtonOff("toggleTexture");
}
@@ -1505,18 +1520,53 @@ function toggleMilitary() {
function toggleMarkers(event) {
if (!layerIsOn("toggleMarkers")) {
turnButtonOn("toggleMarkers");
- $("#markers").fadeIn();
+ drawMarkers();
if (event && isCtrlClick(event)) editStyle("markers");
} else {
- if (event && isCtrlClick(event)) {
- editStyle("markers");
- return;
- }
- $("#markers").fadeOut();
+ if (event && isCtrlClick(event)) return editStyle("markers");
+ markers.selectAll("*").remove();
turnButtonOff("toggleMarkers");
}
}
+function drawMarkers() {
+ const rescale = +markers.attr("rescale");
+ const pinned = +markers.attr("pinned");
+
+ const markersData = pinned ? pack.markers.filter(({pinned}) => pinned) : pack.markers;
+ const html = markersData.map(marker => drawMarker(marker, rescale));
+ markers.html(html.join(""));
+}
+
+const getPin = (shape = "bubble", fill = "#fff", stroke = "#000") => {
+ if (shape === "bubble")
+ return ``;
+ if (shape === "pin")
+ return ``;
+ if (shape === "square") return ``;
+ if (shape === "squarish") return ``;
+ if (shape === "diamond") return ``;
+ if (shape === "hex") return ``;
+ if (shape === "hexy") return ``;
+ if (shape === "shieldy") return ``;
+ if (shape === "shield") return ``;
+ if (shape === "pentagon") return ``;
+ if (shape === "heptagon") return ``;
+ if (shape === "circle") return ``;
+ if (shape === "no") return "";
+};
+
+function drawMarker(marker, rescale = 1) {
+ const {i, icon, x, y, dx = 50, dy = 50, px = 12, size = 30, pin, fill, stroke} = marker;
+ const id = `marker${i}`;
+ const zoomSize = rescale ? Math.max(rn(size / 5 + 24 / scale, 2), 1) : size;
+ const viewX = rn(x - zoomSize / 2, 1);
+ const viewY = rn(y - zoomSize, 1);
+ const pinHTML = getPin(pin, fill, stroke);
+
+ return ``;
+}
+
function toggleLabels(event) {
if (!layerIsOn("toggleLabels")) {
turnButtonOn("toggleLabels");
@@ -1620,21 +1670,21 @@ function drawEmblems() {
const validBurgs = burgs.filter(b => b.i && !b.removed && b.coa && b.coaSize != 0);
const getStateEmblemsSize = () => {
- const startSize = Math.min(Math.max((graphHeight + graphWidth) / 40, 10), 100);
+ const startSize = minmax((graphHeight + graphWidth) / 40, 10, 100);
const statesMod = 1 + validStates.length / 100 - (15 - validStates.length) / 200; // states number modifier
const sizeMod = +document.getElementById("emblemsStateSizeInput").value || 1;
return rn((startSize / statesMod) * sizeMod); // target size ~50px on 1536x754 map with 15 states
};
const getProvinceEmblemsSize = () => {
- const startSize = Math.min(Math.max((graphHeight + graphWidth) / 100, 5), 70);
+ const startSize = minmax((graphHeight + graphWidth) / 100, 5, 70);
const provincesMod = 1 + validProvinces.length / 1000 - (115 - validProvinces.length) / 1000; // states number modifier
const sizeMod = +document.getElementById("emblemsProvinceSizeInput").value || 1;
return rn((startSize / provincesMod) * sizeMod); // target size ~20px on 1536x754 map with 115 provinces
};
const getBurgEmblemSize = () => {
- const startSize = Math.min(Math.max((graphHeight + graphWidth) / 185, 2), 50);
+ const startSize = minmax((graphHeight + graphWidth) / 185, 2, 50);
const burgsMod = 1 + validBurgs.length / 1000 - (450 - validBurgs.length) / 1000; // states number modifier
const sizeMod = +document.getElementById("emblemsBurgSizeInput").value || 1;
return rn((startSize / burgsMod) * sizeMod); // target size ~8.5px on 1536x754 map with 450 burgs
@@ -1684,15 +1734,21 @@ function drawEmblems() {
}
const burgNodes = nodes.filter(node => node.type === "burg");
- const burgString = burgNodes.map(d => ``).join("");
+ const burgString = burgNodes
+ .map(d => ``)
+ .join("");
emblems.select("#burgEmblems").attr("font-size", sizeBurgs).html(burgString);
const provinceNodes = nodes.filter(node => node.type === "province");
- const provinceString = provinceNodes.map(d => ``).join("");
+ const provinceString = provinceNodes
+ .map(d => ``)
+ .join("");
emblems.select("#provinceEmblems").attr("font-size", sizeProvinces).html(provinceString);
const stateNodes = nodes.filter(node => node.type === "state");
- const stateString = stateNodes.map(d => ``).join("");
+ const stateString = stateNodes
+ .map(d => ``)
+ .join("");
emblems.select("#stateEmblems").attr("font-size", sizeStates).html(stateString);
invokeActiveZooming();
diff --git a/modules/ui/markers-editor.js b/modules/ui/markers-editor.js
index c1ea19f6..d1f1cba4 100644
--- a/modules/ui/markers-editor.js
+++ b/modules/ui/markers-editor.js
@@ -1,287 +1,260 @@
"use strict";
-function editMarker() {
+function editMarker(markerI) {
if (customization) return;
- closeDialogs("#markerEditor, .stable");
- $("#markerEditor").dialog();
+ closeDialogs(".stable");
+
+ const [element, marker] = getElement(markerI, d3.event);
+ if (!marker || !element) return;
+
+ elSelected = d3.select(element).raise().call(d3.drag().on("start", dragMarker)).classed("draggable", true);
+
+ if (document.getElementById("notesEditor").offsetParent) editNotes(element.id, element.id);
+
+ // dom elements
+ const markerType = document.getElementById("markerType");
+ const markerIcon = document.getElementById("markerIcon");
+ const markerIconSelect = document.getElementById("markerIconSelect");
+ const markerIconSize = document.getElementById("markerIconSize");
+ const markerIconShiftX = document.getElementById("markerIconShiftX");
+ const markerIconShiftY = document.getElementById("markerIconShiftY");
+ const markerSize = document.getElementById("markerSize");
+ const markerPin = document.getElementById("markerPin");
+ const markerFill = document.getElementById("markerFill");
+ const markerStroke = document.getElementById("markerStroke");
+
+ const markerNotes = document.getElementById("markerNotes");
+ const markerLock = document.getElementById("markerLock");
+ const addMarker = document.getElementById("addMarker");
+ const markerAdd = document.getElementById("markerAdd");
+ const markerRemove = document.getElementById("markerRemove");
- elSelected = d3.select(d3.event.target).call(d3.drag().on("start", dragMarker)).classed("draggable", true);
updateInputs();
- if (modules.editMarker) return;
- modules.editMarker = true;
-
$("#markerEditor").dialog({
- title: "Edit Marker", resizable: false,
- position: {my: "center top+30", at: "bottom", of: d3.event, collision: "fit"},
+ title: "Edit Marker",
+ resizable: false,
+ position: {my: "left top", at: "left+10 top+10", of: "svg", collision: "fit"},
close: closeMarkerEditor
});
- // add listeners
- document.getElementById("markerGroup").addEventListener("click", toggleGroupSection);
- document.getElementById("markerAddGroup").addEventListener("click", toggleGroupInput);
- document.getElementById("markerSelectGroup").addEventListener("change", changeGroup);
- document.getElementById("markerInputGroup").addEventListener("change", createGroup);
- document.getElementById("markerRemoveGroup").addEventListener("click", removeGroup);
+ const listeners = [
+ listen(markerType, "change", changeMarkerType),
+ listen(markerIcon, "input", changeMarkerIcon),
+ listen(markerIconSelect, "click", selectMarkerIcon),
+ listen(markerIconSize, "input", changeIconSize),
+ listen(markerIconShiftX, "input", changeIconShiftX),
+ listen(markerIconShiftY, "input", changeIconShiftY),
+ listen(markerSize, "input", changeMarkerSize),
+ listen(markerPin, "change", changeMarkerPin),
+ listen(markerFill, "input", changePinFill),
+ listen(markerStroke, "input", changePinStroke),
+ listen(markerNotes, "click", editMarkerLegend),
+ listen(markerLock, "click", toggleMarkerLock),
+ listen(markerAdd, "click", toggleAddMarker),
+ listen(markerRemove, "click", confirmMarkerDeletion)
+ ];
- document.getElementById("markerIcon").addEventListener("click", toggleIconSection);
- document.getElementById("markerIconSize").addEventListener("input", changeIconSize);
- document.getElementById("markerIconShiftX").addEventListener("input", changeIconShiftX);
- document.getElementById("markerIconShiftY").addEventListener("input", changeIconShiftY);
- document.getElementById("markerIconSelect").addEventListener("click", selectMarkerIcon);
+ function getElement(markerI, event) {
+ if (event) {
+ const element = event.target?.closest("svg");
+ const marker = pack.markers.find(({i}) => Number(element.id.slice(6)) === i);
+ return [element, marker];
+ }
- document.getElementById("markerStyle").addEventListener("click", toggleStyleSection);
- document.getElementById("markerSize").addEventListener("input", changeMarkerSize);
- document.getElementById("markerBaseStroke").addEventListener("input", changePinStroke);
- document.getElementById("markerBaseFill").addEventListener("input", changePinFill);
- document.getElementById("markerIconStrokeWidth").addEventListener("input", changeIconStrokeWidth);
- document.getElementById("markerIconStroke").addEventListener("input", changeIconStroke);
- document.getElementById("markerIconFill").addEventListener("input", changeIconFill);
+ const element = document.getElementById(`marker${markerI}`);
+ const marker = pack.markers.find(({i}) => i === markerI);
+ return [element, marker];
+ }
- document.getElementById("markerToggleBubble").addEventListener("click", togglePinVisibility);
- document.getElementById("markerLegendButton").addEventListener("click", editMarkerLegend);
- document.getElementById("markerAdd").addEventListener("click", toggleAddMarker);
- document.getElementById("markerRemove").addEventListener("click", removeMarker);
-
- updateGroupOptions();
+ function getSameTypeMarkers() {
+ const currentType = marker.type;
+ if (!currentType) return [marker];
+ return pack.markers.filter(({type}) => type === currentType);
+ }
function dragMarker() {
- const tr = parseTransform(this.getAttribute("transform"));
- const x = +tr[0] - d3.event.x, y = +tr[1] - d3.event.y;
-
- d3.event.on("drag", function() {
- const transform = `translate(${(x + d3.event.x)},${(y + d3.event.y)})`;
- this.setAttribute("transform", transform);
+ const dx = +this.getAttribute("x") - d3.event.x;
+ const dy = +this.getAttribute("y") - d3.event.y;
+
+ d3.event.on("drag", function () {
+ const {x, y} = d3.event;
+ this.setAttribute("x", dx + x);
+ this.setAttribute("y", dy + y);
+ });
+
+ d3.event.on("end", function () {
+ const {x, y} = d3.event;
+ this.setAttribute("x", rn(dx + x, 2));
+ this.setAttribute("y", rn(dy + y, 2));
+
+ const size = marker.size || 30;
+ const zoomSize = Math.max(rn(size / 5 + 24 / scale, 2), 1);
+
+ marker.x = rn(x + dx + zoomSize / 2, 1);
+ marker.y = rn(y + dy + zoomSize, 1);
+ marker.cell = findCell(marker.x, marker.y);
});
}
function updateInputs() {
- const id = elSelected.attr("data-id");
- const symbol = d3.select("#defs-markers").select(id);
- const icon = symbol.select("text");
+ const {icon, type = "", size = 30, dx = 50, dy = 50, px = 12, stroke = "#000000", fill = "#ffffff", pin = "bubble", lock} = marker;
- markerSelectGroup.value = id.slice(1);
- markerIconSize.value = parseFloat(icon.attr("font-size"));
- markerIconShiftX.value = parseFloat(icon.attr("x"));
- markerIconShiftY.value = parseFloat(icon.attr("y"));
+ markerType.value = type;
+ markerIcon.value = icon;
+ markerIconSize.value = px;
+ markerIconShiftX.value = dx;
+ markerIconShiftY.value = dy;
+ markerSize.value = size;
+ markerPin.value = pin;
+ markerFill.value = fill;
+ markerStroke.value = stroke;
- markerSize.value = elSelected.attr("data-size");
- markerBaseStroke.value = symbol.select("path").attr("fill");
- markerBaseFill.value = symbol.select("circle").attr("fill");
-
- markerIconStrokeWidth.value = icon.attr("stroke-width");
- markerIconStroke.value = icon.attr("stroke");
- markerIconFill.value = icon.attr("fill");
-
- markerToggleBubble.className = symbol.select("circle").attr("opacity") === "0" ? "icon-info" : "icon-info-circled";
- markerIconSelect.innerHTML = icon.text();
+ markerLock.className = lock ? "icon-lock" : "icon-lock-open";
}
- function toggleGroupSection() {
- if (markerGroupSection.style.display === "inline-block") {
- markerEditor.querySelectorAll("button:not(#markerGroup)").forEach(b => b.style.display = "inline-block");
- markerGroupSection.style.display = "none";
- } else {
- markerEditor.querySelectorAll("button:not(#markerGroup)").forEach(b => b.style.display = "none");
- markerGroupSection.style.display = "inline-block";
- }
+ function changeMarkerType() {
+ marker.type = this.value;
}
- function updateGroupOptions() {
- markerSelectGroup.innerHTML = "";
- d3.select("#defs-markers").selectAll("symbol").each(function() {
- markerSelectGroup.options.add(new Option(this.id, this.id));
+ function changeMarkerIcon() {
+ const icon = this.value;
+ getSameTypeMarkers().forEach(marker => {
+ marker.icon = icon;
+ redrawIcon(marker);
});
- markerSelectGroup.value = elSelected.attr("data-id").slice(1);
- }
-
- function toggleGroupInput() {
- if (markerInputGroup.style.display === "inline-block") {
- markerSelectGroup.style.display = "inline-block";
- markerInputGroup.style.display = "none";
- } else {
- markerSelectGroup.style.display = "none";
- markerInputGroup.style.display = "inline-block";
- markerInputGroup.focus();
- }
- }
-
- function changeGroup() {
- elSelected.attr("xlink:href", "#"+this.value);
- elSelected.attr("data-id", "#"+this.value);
- }
-
- function createGroup() {
- let newGroup = this.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, "");
- if (Number.isFinite(+newGroup.charAt(0))) newGroup = "m" + newGroup;
- if (document.getElementById(newGroup)) {
- tip("Element with this id already exists. Please provide a unique name", false, "error");
- return;
- }
-
- markerInputGroup.value = "";
- // clone old group assigning new id
- const id = elSelected.attr("data-id");
- const clone = d3.select("#defs-markers").select(id).node().cloneNode(true);
- clone.id = newGroup;
- document.getElementById("defs-markers").insertBefore(clone, null);
- elSelected.attr("xlink:href", "#"+newGroup).attr("data-id", "#"+newGroup);
-
- // select new group
- markerSelectGroup.options.add(new Option(newGroup, newGroup, false, true));
- toggleGroupInput();
- }
-
- function removeGroup() {
- const id = elSelected.attr("data-id");
- const used = document.querySelectorAll("use[data-id='"+id+"']");
- const count = used.length === 1 ? "1 element" : used.length + " elements";
- alertMessage.innerHTML = "Are you sure you want to remove all markers of that type (" + count + ")?";
-
- $("#alert").dialog({resizable: false, title: "Remove marker type",
- buttons: {
- Remove: function() {
- $(this).dialog("close");
- if (id !== "#marker0") d3.select("#defs-markers").select(id).remove();
- used.forEach(e => {
- const index = notes.findIndex(n => n.id === e.id);
- if (index != -1) notes.splice(index, 1);
- e.remove();
- });
- updateGroupOptions();
- updateGroupOptions();
- $("#markerEditor").dialog("close");
- },
- Cancel: function() {$(this).dialog("close");}
- }
- });
- }
-
- function toggleIconSection() {
- if (markerIconSection.style.display === "inline-block") {
- markerEditor.querySelectorAll("button:not(#markerIcon)").forEach(b => b.style.display = "inline-block");
- markerIconSection.style.display = "none";
- markerIconSelect.style.display = "none";
- } else {
- markerEditor.querySelectorAll("button:not(#markerIcon)").forEach(b => b.style.display = "none");
- markerIconSection.style.display = "inline-block";
- markerIconSelect.style.display = "inline-block";
- }
}
function selectMarkerIcon() {
- selectIcon(this.innerHTML, v => {
- this.innerHTML = v;
- const id = elSelected.attr("data-id");
- d3.select("#defs-markers").select(id).select("text").text(v);
+ selectIcon(marker.icon, icon => {
+ markerIcon.value = icon;
+ getSameTypeMarkers().forEach(marker => {
+ marker.icon = icon;
+ redrawIcon(marker);
+ });
});
}
function changeIconSize() {
- const id = elSelected.attr("data-id");
- d3.select("#defs-markers").select(id).select("text").attr("font-size", this.value + "px");
+ const px = +this.value;
+ getSameTypeMarkers().forEach(marker => {
+ marker.px = px;
+ redrawIcon(marker);
+ });
}
function changeIconShiftX() {
- const id = elSelected.attr("data-id");
- d3.select("#defs-markers").select(id).select("text").attr("x", this.value + "%");
+ const dx = +this.value;
+ getSameTypeMarkers().forEach(marker => {
+ marker.dx = dx;
+ redrawIcon(marker);
+ });
}
function changeIconShiftY() {
- const id = elSelected.attr("data-id");
- d3.select("#defs-markers").select(id).select("text").attr("y", this.value + "%");
- }
-
- function toggleStyleSection() {
- if (markerStyleSection.style.display === "inline-block") {
- markerEditor.querySelectorAll("button:not(#markerStyle)").forEach(b => b.style.display = "inline-block");
- markerStyleSection.style.display = "none";
- } else {
- markerEditor.querySelectorAll("button:not(#markerStyle)").forEach(b => b.style.display = "none");
- markerStyleSection.style.display = "inline-block";
- }
+ const dy = +this.value;
+ getSameTypeMarkers().forEach(marker => {
+ marker.dy = dy;
+ redrawIcon(marker);
+ });
}
function changeMarkerSize() {
- const id = elSelected.attr("data-id");
- document.querySelectorAll("use[data-id='"+id+"']").forEach(e => {
- const x = +e.dataset.x, y = +e.dataset.y;
- const desired = e.dataset.size = +markerSize.value;
- const size = Math.max(desired * 5 + 25 / scale, 1);
+ const size = +this.value;
+ const rescale = +markers.attr("rescale");
- e.setAttribute("x", x - size / 2);
- e.setAttribute("y", y - size / 2);
- e.setAttribute("width", size);
- e.setAttribute("height", size);
+ getSameTypeMarkers().forEach(marker => {
+ marker.size = size;
+ const {i, x, y, hidden} = marker;
+ const el = !hidden && document.getElementById(`marker${i}`);
+ if (!el) return;
+
+ const zoomedSize = rescale ? Math.max(rn(size / 5 + 24 / scale, 2), 1) : size;
+ el.setAttribute("width", zoomedSize);
+ el.setAttribute("height", zoomedSize);
+ el.setAttribute("x", rn(x - zoomedSize / 2, 1));
+ el.setAttribute("y", rn(y - zoomedSize, 1));
});
- invokeActiveZooming();
}
- function changePinStroke() {
- const id = elSelected.attr("data-id");
- d3.select(id).select("path").attr("fill", this.value);
- d3.select(id).select("circle").attr("stroke", this.value);
+ function changeMarkerPin() {
+ const pin = this.value;
+ getSameTypeMarkers().forEach(marker => {
+ marker.pin = pin;
+ redrawPin(marker);
+ });
}
function changePinFill() {
- const id = elSelected.attr("data-id");
- d3.select(id).select("circle").attr("fill", this.value);
- }
-
- function changeIconStrokeWidth() {
- const id = elSelected.attr("data-id");
- d3.select("#defs-markers").select(id).select("text").attr("stroke-width", this.value);
- }
-
- function changeIconStroke() {
- const id = elSelected.attr("data-id");
- d3.select("#defs-markers").select(id).select("text").attr("stroke", this.value);
- }
-
- function changeIconFill() {
- const id = elSelected.attr("data-id");
- d3.select("#defs-markers").select(id).select("text").attr("fill", this.value);
- }
-
- function togglePinVisibility() {
- const id = elSelected.attr("data-id");
- let show = 1;
- if (this.className === "icon-info-circled") {this.className = "icon-info"; show = 0; }
- else this.className = "icon-info-circled";
- d3.select(id).select("circle").attr("opacity", show);
- d3.select(id).select("path").attr("opacity", show);
- }
-
- function editMarkerLegend() {
- const id = elSelected.attr("id");
- editNotes(id, id);
- }
-
- function toggleAddMarker() {
- document.getElementById("addMarker").click();
- }
-
- function removeMarker() {
- alertMessage.innerHTML = "Are you sure you want to remove the marker?";
- $("#alert").dialog({resizable: false, title: "Remove marker",
- buttons: {
- Remove: function() {
- $(this).dialog("close");
- const index = notes.findIndex(n => n.id === elSelected.attr("id"));
- if (index != -1) notes.splice(index, 1);
- elSelected.remove();
- $("#markerEditor").dialog("close");
- },
- Cancel: function() {$(this).dialog("close");}
- }
+ const fill = this.value;
+ getSameTypeMarkers().forEach(marker => {
+ marker.fill = fill;
+ redrawPin(marker);
});
}
+ function changePinStroke() {
+ const stroke = this.value;
+ getSameTypeMarkers().forEach(marker => {
+ marker.stroke = stroke;
+ redrawPin(marker);
+ });
+ }
+
+ function redrawIcon({i, hidden, icon, dx = 50, dy = 50, px = 12}) {
+ const iconElement = !hidden && document.querySelector(`#marker${i} > text`);
+ if (iconElement) {
+ iconElement.innerHTML = icon;
+ iconElement.setAttribute("x", dx + "%");
+ iconElement.setAttribute("y", dy + "%");
+ iconElement.setAttribute("font-size", px + "px");
+ }
+ }
+
+ function redrawPin({i, hidden, pin = "bubble", fill = "#fff", stroke = "#000"}) {
+ const pinGroup = !hidden && document.querySelector(`#marker${i} > g`);
+ if (pinGroup) pinGroup.innerHTML = getPin(pin, fill, stroke);
+ }
+
+ function editMarkerLegend() {
+ const id = element.id;
+ editNotes(id, id);
+ }
+
+ function toggleMarkerLock() {
+ marker.lock = !marker.lock;
+ markerLock.classList.toggle("icon-lock-open");
+ markerLock.classList.toggle("icon-lock");
+ }
+
+ function toggleAddMarker() {
+ markerAdd.classList.toggle("pressed");
+ addMarker.click();
+ }
+
+ function confirmMarkerDeletion() {
+ confirmationDialog({
+ title: "Remove marker",
+ message: "Are you sure you want to remove this marker? The action cannot be reverted",
+ confirm: "Remove",
+ onConfirm: deleteMarker
+ });
+ }
+
+ function deleteMarker() {
+ notes = notes.filter(note => note.id !== element.id);
+ pack.markers = pack.markers.filter(m => m.i !== marker.i);
+ element.remove();
+ $("#markerEditor").dialog("close");
+ if (document.getElementById("markersOverviewRefresh").offsetParent) markersOverviewRefresh.click();
+ }
+
function closeMarkerEditor() {
+ listeners.forEach(removeListener => removeListener());
+
unselect();
- if (addMarker.classList.contains("pressed")) addMarker.classList.remove("pressed");
- if (markerAdd.classList.contains("pressed")) markerAdd.classList.remove("pressed");
+ addMarker.classList.remove("pressed");
+ markerAdd.classList.remove("pressed");
restoreDefaultEvents();
clearMainTip();
}
}
-
diff --git a/modules/ui/markers-overview.js b/modules/ui/markers-overview.js
new file mode 100644
index 00000000..10ac506d
--- /dev/null
+++ b/modules/ui/markers-overview.js
@@ -0,0 +1,196 @@
+"use strict";
+function overviewMarkers() {
+ if (customization) return;
+ closeDialogs("#markersOverview, .stable");
+ if (!layerIsOn("toggleMarkers")) toggleMarkers();
+
+ const markerGroup = document.getElementById("markers");
+ const body = document.getElementById("markersBody");
+ const markersInverPin = document.getElementById("markersInverPin");
+ const markersInverLock = document.getElementById("markersInverLock");
+ const markersFooterNumber = document.getElementById("markersFooterNumber");
+ const markersOverviewRefresh = document.getElementById("markersOverviewRefresh");
+ const markersAddFromOverview = document.getElementById("markersAddFromOverview");
+ const markersGenerationConfig = document.getElementById("markersGenerationConfig");
+ const markersRemoveAll = document.getElementById("markersRemoveAll");
+ const markersExport = document.getElementById("markersExport");
+
+ addLines();
+
+ $("#markersOverview").dialog({
+ title: "Markers Overview",
+ resizable: false,
+ width: fitContent(),
+ close: close,
+ position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
+ });
+
+ const listeners = [
+ listen(body, "click", handleLineClick),
+ listen(markersInverPin, "click", invertPin),
+ listen(markersInverLock, "click", invertLock),
+ listen(markersOverviewRefresh, "click", addLines),
+ listen(markersAddFromOverview, "click", toggleAddMarker),
+ listen(markersGenerationConfig, "click", configMarkersGeneration),
+ listen(markersRemoveAll, "click", triggerRemoveAll),
+ listen(markersExport, "click", exportMarkers)
+ ];
+
+ function handleLineClick(ev) {
+ const el = ev.target;
+ const i = +el.parentNode.dataset.i;
+
+ if (el.classList.contains("icon-pencil")) return openEditor(i);
+ if (el.classList.contains("icon-dot-circled")) return focusOnMarker(i);
+ if (el.classList.contains("icon-pin")) return pinMarker(el, i);
+ if (el.classList.contains("locks")) return toggleLockStatus(el, i);
+ if (el.classList.contains("icon-trash-empty")) return triggerRemove(i);
+ }
+
+ function addLines() {
+ const lines = pack.markers
+ .map(({i, type, icon, pinned, lock}) => {
+ return `
+
${icon} ${type}
+
+
+
+
+
+
`;
+ })
+ .join("");
+
+ body.innerHTML = lines;
+ markersFooterNumber.innerText = pack.markers.length;
+
+ applySorting(markersHeader);
+ }
+
+ function invertPin() {
+ let anyPinned = false;
+
+ pack.markers.forEach(marker => {
+ const pinned = !marker.pinned;
+ if (pinned) {
+ marker.pinned = true;
+ anyPinned = true;
+ } else delete marker.pinned;
+ });
+
+ markerGroup.setAttribute("pinned", anyPinned ? 1 : null);
+ drawMarkers();
+ addLines();
+ }
+
+ function invertLock() {
+ pack.markers = pack.markers.map(marker => ({...marker, lock: !marker.lock}));
+ addLines();
+ }
+
+ function openEditor(i) {
+ const marker = pack.markers.find(marker => marker.i === i);
+ if (!marker) return;
+
+ const {x, y} = marker;
+ zoomTo(x, y, 8, 2000);
+ editMarker(i);
+ }
+
+ function focusOnMarker(i) {
+ highlightElement(document.getElementById(`marker${i}`), 2);
+ }
+
+ function pinMarker(el, i) {
+ const marker = pack.markers.find(marker => marker.i === i);
+ if (marker.pinned) {
+ delete marker.pinned;
+ const anyPinned = pack.markers.some(marker => marker.pinned);
+ if (!anyPinned) markerGroup.removeAttribute("pinned");
+ } else {
+ marker.pinned = true;
+ markerGroup.setAttribute("pinned", 1);
+ }
+ el.classList.toggle("inactive");
+ drawMarkers();
+ }
+
+ function toggleLockStatus(el, i) {
+ const marker = pack.markers.find(marker => marker.i === i);
+ if (marker.lock) {
+ delete marker.lock;
+ el.className = "locks pointer icon-lock-open inactive";
+ } else {
+ marker.lock = true;
+ el.className = "locks pointer icon-lock";
+ }
+ }
+
+ function triggerRemove(i) {
+ confirmationDialog({
+ title: "Remove marker",
+ message: "Are you sure you want to remove this marker? The action cannot be reverted",
+ confirm: "Remove",
+ onConfirm: () => removeMarker(i)
+ });
+ }
+
+ function toggleAddMarker() {
+ markersAddFromOverview.classList.toggle("pressed");
+ addMarker.click();
+ }
+
+ function removeMarker(i) {
+ notes = notes.filter(note => note.id !== `marker${i}`);
+ pack.markers = pack.markers.filter(marker => marker.i !== i);
+ document.getElementById(`marker${i}`)?.remove();
+ addLines();
+ }
+
+ function triggerRemoveAll() {
+ confirmationDialog({
+ title: "Remove all markers",
+ message: "Are you sure you want to remove all non-locked markers? The action cannot be reverted",
+ confirm: "Remove all",
+ onConfirm: removeAllMarkers
+ });
+ }
+
+ function removeAllMarkers() {
+ pack.markers = pack.markers.filter(({i, lock}) => {
+ if (lock) return true;
+
+ const id = `marker${i}`;
+ document.getElementById(id)?.remove();
+ notes = notes.filter(note => note.id !== id);
+ return false;
+ });
+
+ addLines();
+ }
+
+ function exportMarkers() {
+ const headers = "Id,Type,Icon,Name,Note,X,Y\n";
+
+ const body = pack.markers.map(marker => {
+ const {i, type, icon, x, y} = marker;
+ const id = `marker${i}`;
+ const note = notes.find(note => note.id === id);
+ const legend = escape(note.legend);
+ return [id, type, icon, note.name, legend, x, y].join(",");
+ });
+
+ const data = headers + body.join("\n");
+ const fileName = getFileName("Markers") + ".csv";
+ downloadFile(data, fileName);
+ }
+
+ function close() {
+ listeners.forEach(removeListener => removeListener());
+
+ addMarker.classList.remove("pressed");
+ markerAdd.classList.remove("pressed");
+ restoreDefaultEvents();
+ clearMainTip();
+ }
+}
diff --git a/modules/ui/measurers.js b/modules/ui/measurers.js
index 1d97a2d1..3fe8fe5b 100644
--- a/modules/ui/measurers.js
+++ b/modules/ui/measurers.js
@@ -20,7 +20,16 @@ class Rulers {
for (const rulerString of rulers) {
const [type, pointsString] = rulerString.split(": ");
const points = pointsString.split(" ").map(el => el.split(",").map(n => +n));
- const Type = type === "Ruler" ? Ruler : type === "Opisometer" ? Opisometer : type === "RouteOpisometer" ? RouteOpisometer : type === "Planimeter" ? Planimeter : null;
+ const Type =
+ type === "Ruler"
+ ? Ruler
+ : type === "Opisometer"
+ ? Opisometer
+ : type === "RouteOpisometer"
+ ? RouteOpisometer
+ : type === "Planimeter"
+ ? Planimeter
+ : null;
this.create(Type, points);
}
}
@@ -527,17 +536,18 @@ class Planimeter extends Measurer {
}
// Scale bar
-function drawScaleBar() {
+function drawScaleBar(requestedScale) {
if (scaleBar.style("display") === "none") return; // no need to re-draw hidden element
scaleBar.selectAll("*").remove(); // fully redraw every time
+ const scaleLevel = requestedScale || scale;
- const dScale = distanceScaleInput.value;
+ const distanceScale = distanceScaleInput.value;
const unit = distanceUnitInput.value;
+ const size = +barSizeInput.value;
// calculate size
- const init = 100; // actual length in pixels if scale, dScale and size = 1;
- const size = +barSizeInput.value;
- let val = (init * size * dScale) / scale; // bar length in distance unit
+ const init = 100;
+ let val = (init * size * distanceScale) / scaleLevel; // bar length in distance unit
if (val > 900) val = rn(val, -3);
// round to 1000
else if (val > 90) val = rn(val, -2);
@@ -545,13 +555,13 @@ function drawScaleBar() {
else if (val > 9) val = rn(val, -1);
// round to 10
else val = rn(val); // round to 1
- const l = (val * scale) / dScale; // actual length in pixels on this scale
+ const length = (val * scaleLevel) / distanceScale; // actual length in pixels on this scale
scaleBar
.append("line")
.attr("x1", 0.5)
.attr("y1", 0)
- .attr("x2", l + size - 0.5)
+ .attr("x2", length + size - 0.5)
.attr("y2", 0)
.attr("stroke-width", size)
.attr("stroke", "white");
@@ -559,16 +569,16 @@ function drawScaleBar() {
.append("line")
.attr("x1", 0)
.attr("y1", size)
- .attr("x2", l + size)
+ .attr("x2", length + size)
.attr("y2", size)
.attr("stroke-width", size)
.attr("stroke", "#3d3d3d");
- const dash = size + " " + rn(l / 5 - size, 2);
+ const dash = size + " " + rn(length / 5 - size, 2);
scaleBar
.append("line")
.attr("x1", 0)
.attr("y1", 0)
- .attr("x2", l + size)
+ .attr("x2", length + size)
.attr("y2", 0)
.attr("stroke-width", rn(size * 3, 2))
.attr("stroke-dasharray", dash)
@@ -580,16 +590,16 @@ function drawScaleBar() {
.data(d3.range(0, 6))
.enter()
.append("text")
- .attr("x", d => rn((d * l) / 5, 2))
+ .attr("x", d => rn((d * length) / 5, 2))
.attr("y", 0)
.attr("dy", "-.5em")
.attr("font-size", fontSize)
- .text(d => rn((((d * l) / 5) * dScale) / scale) + (d < 5 ? "" : " " + unit));
+ .text(d => rn((((d * length) / 5) * distanceScale) / scaleLevel) + (d < 5 ? "" : " " + unit));
if (barLabel.value !== "") {
scaleBar
.append("text")
- .attr("x", (l + 1) / 2)
+ .attr("x", (length + 1) / 2)
.attr("y", 2 * size)
.attr("dominant-baseline", "text-before-edge")
.attr("font-size", fontSize)
diff --git a/modules/ui/military-overview.js b/modules/ui/military-overview.js
index 95f897cb..38b25a41 100644
--- a/modules/ui/military-overview.js
+++ b/modules/ui/military-overview.js
@@ -75,14 +75,21 @@ function overviewMilitary() {
const sortData = options.military.map(u => `data-${u.name}="${getForces(u)}"`).join(" ");
const lineData = options.military.map(u => `${getForces(u)}
`).join(" ");
- lines += `
-
+ lines += `
`;
}
@@ -145,7 +152,15 @@ function overviewMilitary() {
if (!layerIsOn("toggleStates")) return;
const d = regions.select("#state" + state).attr("d");
- const path = debug.append("path").attr("class", "highlight").attr("d", d).attr("fill", "none").attr("stroke", "red").attr("stroke-width", 1).attr("opacity", 1).attr("filter", "url(#blur1)");
+ const path = debug
+ .append("path")
+ .attr("class", "highlight")
+ .attr("d", d)
+ .attr("fill", "none")
+ .attr("stroke", "red")
+ .attr("stroke-width", 1)
+ .attr("opacity", 1)
+ .attr("filter", "url(#blur1)");
const l = path.node().getTotalLength(),
dur = (l + 5000) / 2;
@@ -199,9 +214,9 @@ function overviewMilitary() {
function militaryCustomize() {
const types = ["melee", "ranged", "mounted", "machinery", "naval", "armored", "aviation", "magical"];
- const table = document.getElementById("militaryOptions").querySelector("tbody");
+ const tableBody = document.getElementById("militaryOptions").querySelector("tbody");
removeUnitLines();
- options.military.map(u => addUnitLine(u));
+ options.military.map(unit => addUnitLine(unit));
$("#militaryOptions").dialog({
title: "Edit Military Units",
@@ -218,43 +233,135 @@ function overviewMilitary() {
},
open: function () {
const buttons = $(this).dialog("widget").find(".ui-dialog-buttonset > button");
- buttons[0].addEventListener("mousemove", () => tip("Apply military units settings.
All forces will be recalculated!"));
+ buttons[0].addEventListener("mousemove", () =>
+ tip("Apply military units settings.
All forces will be recalculated!")
+ );
buttons[1].addEventListener("mousemove", () => tip("Add new military unit to the table"));
buttons[2].addEventListener("mousemove", () => tip("Restore default military units and settings"));
buttons[3].addEventListener("mousemove", () => tip("Close the window without saving the changes"));
}
});
+ if (modules.overviewMilitaryCustomize) return;
+ modules.overviewMilitaryCustomize = true;
+
+ tableBody.addEventListener("click", event => {
+ const el = event.target;
+ if (el.tagName !== "BUTTON") return;
+ const type = el.dataset.type;
+
+ if (type === "icon") return selectIcon(el.innerHTML, v => (el.innerHTML = v));
+ if (type === "biomes") {
+ const {i, name, color} = biomesData;
+ const biomesArray = Array(i.length).fill(null);
+ const biomes = biomesArray.map((_, i) => ({i, name: name[i], color: color[i]}));
+ return selectLimitation(el, biomes);
+ }
+ if (type === "states") return selectLimitation(el, pack.states);
+ if (type === "cultures") return selectLimitation(el, pack.cultures);
+ if (type === "religions") return selectLimitation(el, pack.religions);
+ });
+
function removeUnitLines() {
- table.querySelectorAll("tr").forEach(el => el.remove());
+ tableBody.querySelectorAll("tr").forEach(el => el.remove());
}
- function addUnitLine(u) {
+ function getLimitValue(attr) {
+ return attr?.join(",") || "";
+ }
+
+ function getLimitText(attr) {
+ return attr?.length ? "some" : "all";
+ }
+
+ function getLimitTip(attr, data) {
+ if (!attr || !attr.length) return "";
+ return attr.map(i => data?.[i]?.name || "").join(", ");
+ }
+
+ function addUnitLine(unit) {
+ const {type, icon, name, rural, urban, power, crew, separate} = unit;
const row = document.createElement("tr");
- row.innerHTML = `
|
-
|
-
|
-
|
-
|
-
|
-
|
-
-
- |
+ const typeOptions = types.map(t => `
`).join(" ");
+
+ const getLimitButton = attr =>
+ `
`;
+
+ row.innerHTML = `
|
+
|
+
${getLimitButton("biomes")} |
+
${getLimitButton("states")} |
+
${getLimitButton("cultures")} |
+
${getLimitButton("religions")} |
+
|
+
|
+
|
+
|
+
|
+
+
+ |
| `;
- row.querySelector("button").addEventListener("click", function (e) {
- selectIcon(this.innerHTML, v => (this.innerHTML = v));
- });
- table.appendChild(row);
+ tableBody.appendChild(row);
}
function restoreDefaultUnits() {
removeUnitLines();
- Military.getDefaultOptions().map(u => addUnitLine(u));
+ Military.getDefaultOptions().map(unit => addUnitLine(unit));
+ }
+
+ function selectLimitation(el, data) {
+ const type = el.dataset.type;
+ const value = el.dataset.value;
+ const initial = value ? value.split(",").map(v => +v) : [];
+
+ const filtered = data.filter(datum => datum.i && !datum.removed);
+ const lines = filtered.map(
+ ({i, name, fullName, color}) =>
+ `
| ⬤ |
+
+
+ |
`
+ );
+ alertMessage.innerHTML = `
Limit unit by ${type}:`;
+
+ $("#alert").dialog({
+ width: fitContent(),
+ title: `Limit unit`,
+ buttons: {
+ Invert: function () {
+ alertMessage.querySelectorAll("input").forEach(el => (el.checked = !el.checked));
+ },
+ Apply: function () {
+ const inputs = Array.from(alertMessage.querySelectorAll("input"));
+ const selected = inputs.reduce((acc, input) => {
+ if (input.checked) acc.push(input.dataset.i);
+ return acc;
+ }, []);
+
+ if (!selected.length) return tip("Select at least one element", false, "error");
+
+ const allAreSelected = selected.length === inputs.length;
+ el.dataset.value = allAreSelected ? "" : selected.join(",");
+ el.innerHTML = allAreSelected ? "all" : "some";
+ el.setAttribute("title", getLimitTip(selected, data));
+ $(this).dialog("close");
+ },
+ Cancel: function () {
+ $(this).dialog("close");
+ }
+ }
+ });
}
function applyMilitaryOptions() {
- const unitLines = Array.from(table.querySelectorAll("tr"));
+ const unitLines = Array.from(tableBody.querySelectorAll("tr"));
const names = unitLines.map(r => r.querySelector("input").value.replace(/[&\/\\#, +()$~%.'":*?<>{}]/g, "_"));
if (new Set(names).size !== names.length) {
tip("All units should have unique names", false, "error");
@@ -263,14 +370,22 @@ function overviewMilitary() {
$("#militaryOptions").dialog("close");
options.military = unitLines.map((r, i) => {
- const [icon, name, rural, urban, crew, power, type, separate] = Array.from(r.querySelectorAll("input, select, button")).map(d => {
- let value = d.value;
- if (d.type === "number") value = +d.value || 0;
- if (d.type === "checkbox") value = +d.checked || 0;
- if (d.type === "button") value = d.innerHTML || "⠀";
- return value;
+ const elements = Array.from(r.querySelectorAll("input, button, select"));
+ const [icon, name, biomes, states, cultures, religions, rural, urban, crew, power, type, separate] = elements.map(el => {
+ const {type, value} = el.dataset || {};
+ if (type === "icon") return el.innerHTML || "⠀";
+ if (type) return value ? value.split(",").map(v => parseInt(v)) : null;
+ if (el.type === "number") return +el.value || 0;
+ if (el.type === "checkbox") return +el.checked || 0;
+ return el.value;
});
- return {icon, name: names[i], rural, urban, crew, power, type, separate};
+
+ const unit = {icon, name: names[i], rural, urban, crew, power, type, separate};
+ if (biomes) unit.biomes = biomes;
+ if (states) unit.states = states;
+ if (cultures) unit.cultures = cultures;
+ if (religions) unit.religions = religions;
+ return unit;
});
localStorage.setItem("military", JSON.stringify(options.military));
Military.generate();
diff --git a/modules/ui/notes-editor.js b/modules/ui/notes-editor.js
index 5a343966..3ad4b979 100644
--- a/modules/ui/notes-editor.js
+++ b/modules/ui/notes-editor.js
@@ -1,4 +1,5 @@
"use strict";
+
function editNotes(id, name) {
// update list of objects
const select = document.getElementById("notesSelect");
@@ -8,11 +9,12 @@ function editNotes(id, name) {
}
// initiate pell (html editor)
+ const notesText = document.getElementById("notesText");
+ notesText.innerHTML = "";
const editor = Pell.init({
- element: document.getElementById("notesText"),
+ element: notesText,
onChange: html => {
- const id = document.getElementById("notesSelect").value;
- const note = notes.find(note => note.id === id);
+ const note = notes.find(note => note.id === select.value);
if (!note) return;
note.legend = html;
showNote(note);
@@ -43,8 +45,7 @@ function editNotes(id, name) {
title: "Notes Editor",
minWidth: "40em",
width: "50vw",
- position: {my: "center", at: "center", of: "svg"},
- close: () => (notesText.innerHTML = "")
+ position: {my: "center", at: "center", of: "svg"}
});
if (modules.editNotes) return;
@@ -108,7 +109,7 @@ function editNotes(id, name) {
return;
}
- highlightElement(element); // if element is found
+ highlightElement(element, 3); // if element is found
}
function downloadLegends() {
diff --git a/modules/ui/options.js b/modules/ui/options.js
index 9b4f3411..82b3b8a4 100644
--- a/modules/ui/options.js
+++ b/modules/ui/options.js
@@ -89,19 +89,20 @@ function showSupporters() {
Maxwell Hill,Drunken_Legends,rob bee,Jesse Holmes,YYako,Detocroix,Anoplexian,Hannah,Paul,Sandra Krohn,Lucid,Richard Keating,Allen Varney,Rick Falkvinge,
Seth Fusion,Adam Butler,Gus,StroboWolf,Sadie Blackthorne,Zewen Senpai,Dell McKnight,Oneiris,Darinius Dragonclaw Studios,Christopher Whitney,Rhodes HvZ,
Jeppe Skov Jensen,María Martín López,Martin Seeger,Annie Rishor,Aram Sabatés,MadNomadMedia,Eric Foley,Vito Martono,James H. Anthony,Kevin Cossutta,
- Thirty-OneR ,ThatGuyGW ,Dee Chiu,MontyBoosh ,Achillain ,Jaden ,SashaTK,Steve Johnson,Eric Foley,Vito Martono,James H. Anthony,Kevin Cossutta,Thirty-OneR,
- ThatGuyGW,Dee Chiu,MontyBoosh,Achillain,Jaden,SashaTK,Steve Johnson,Pierrick Bertrand,Jared Kennedy,Dylan Devenny,Kyle Robertson,Andrew Rostaing,Daniel Gill,
- Char,Jack,Barna Csíkos,Ian Rousseau,Nicholas Grabstas,Tom Van Orden jr,Bryan Brake,Akylos,Riley Seaman,MaxOliver,Evan-DiLeo,Alex Debus,Joshua Vaught,
- Kyle S,Eric Moore,Dean Dunakin,Uniquenameosaurus,WarWizardGames,Chance Mena,Jan Ka,Miguel Alejandro,Dalton Clark,Simon Drapeau,Radovan Zapletal,Jmmat6,
- Justa Badge,Blargh Blarghmoomoo,Vanessa Anjos,Grant A. Murray,Akirsop,Rikard Wolff,Jake Fish,teco 47,Antiroo,Jakob Siegel,Guilherme Aguiar,Jarno Hallikainen,
- Justin Mcclain,Kristin Chernoff,Rowland Kingman,Esther Busch,Grayson McClead,Austin,Hakon the Viking,Chad Riley,Cooper Counts,Patrick Jones,Clonetone,
- PlayByMail.Net,Brad Wardell,Lance Saba,Egoensis,Brea Richards,Tiber,Chris Bloom,Maxim Lowe,Aquelion,Page One Project,Spencer Morris,Paul Ingram,
- Dust Bunny,Adrian Wright,Eric Alexander Cartaya,GameNight,Thomas Mortensen Hansen,Zklaus,Drinarius,Ed Wright,Lon Varnadore,Crys Cain,Heaven N Lee,
- Jeffrey Henning,Lazer Elf,Jordan Bellah,Alex Beard,Kass Frisson,Petro Lombaard,Emanuel Pietri,Rox,PinkEvil,Gavin Madrigal,Martin Lorber,Prince of Morgoth,
- Jaryd Armstrong,Andrew Pirkola,ThyHolyDevil,Gary Smith,Tyshaun Wise,Ethan Cook,Jon Stroman,Nobody679,良义 金,Chris Gray,Phoenix Boatwright,Mackenzie,
- Milo Cohen,Jason Matthew Wuerfel,Rasmus Legêne,Andrew Hines,Wexxler,Espen Sæverud,Binks,Dominick Ormsby,Linn Browning,Václav Švec,Alan Buehne,
- George J.Lekkas,Alexandre Boivin,Tommy Mayfield,Skylar Mangum-Turner,Karen Blythe,Stefan Gugerel,Mike Conley,Xavier privé,Hope You're Well,
- Mark Sprietsma,Robert Landry,Nick Mowry"`;
+ Thirty-OneR,ThatGuyGW,Dee Chiu,MontyBoosh,Achillain,Jaden,SashaTK,Steve Johnson,Pierrick Bertrand,Jared Kennedy,Dylan Devenny,Kyle Robertson,
+ Andrew Rostaing,Daniel Gill,Char,Jack,Barna Csíkos,Ian Rousseau,Nicholas Grabstas,Tom Van Orden jr,Bryan Brake,Akylos,Riley Seaman,MaxOliver,Evan-DiLeo,
+ Alex Debus,Joshua Vaught,Kyle S,Eric Moore,Dean Dunakin,Uniquenameosaurus,WarWizardGames,Chance Mena,Jan Ka,Miguel Alejandro,Dalton Clark,Simon Drapeau,
+ Radovan Zapletal,Jmmat6,Justa Badge,Blargh Blarghmoomoo,Vanessa Anjos,Grant A. Murray,Akirsop,Rikard Wolff,Jake Fish,teco 47,Antiroo,Jakob Siegel,
+ Guilherme Aguiar,Jarno Hallikainen,Justin Mcclain,Kristin Chernoff,Rowland Kingman,Esther Busch,Grayson McClead,Austin,Hakon the Viking,Chad Riley,
+ Cooper Counts,Patrick Jones,Clonetone,PlayByMail.Net,Brad Wardell,Lance Saba,Egoensis,Brea Richards,Tiber,Chris Bloom,Maxim Lowe,Aquelion,
+ Page One Project,Spencer Morris,Paul Ingram,Dust Bunny,Adrian Wright,Eric Alexander Cartaya,GameNight,Thomas Mortensen Hansen,Zklaus,Drinarius,
+ Ed Wright,Lon Varnadore,Crys Cain,Heaven N Lee,Jeffrey Henning,Lazer Elf,Jordan Bellah,Alex Beard,Kass Frisson,Petro Lombaard,Emanuel Pietri,Rox,
+ PinkEvil,Gavin Madrigal,Martin Lorber,Prince of Morgoth,Jaryd Armstrong,Andrew Pirkola,ThyHolyDevil,Gary Smith,Tyshaun Wise,Ethan Cook,Jon Stroman,
+ Nobody679,良义 金,Chris Gray,Phoenix Boatwright,Mackenzie,Milo Cohen,Jason Matthew Wuerfel,Rasmus Legêne,Andrew Hines,Wexxler,Espen Sæverud,Binks,
+ Dominick Ormsby,Linn Browning,Václav Švec,Alan Buehne,George J.Lekkas,Alexandre Boivin,Tommy Mayfield,Skylar Mangum-Turner,Karen Blythe,Stefan Gugerel,
+ Mike Conley,Xavier privé,Hope You're Well,Mark Sprietsma,Robert Landry,Nick Mowry,steve hall,Markell,Josh Wren,Neutrix,BLRageQuit,Rocky,
+ Dario Spadavecchia,Bas Kroot,John Patrick Callahan Jr,Alexandra Vesey,D,Exp1nt,james,Braxton Istace,w,Rurikid,AntiBlock,Redsauz,BigE0021,
+ Jonathan Williams,ojacid .,Brian Wilson,A Patreon of the Ahts,Shubham Jakhotiya`;
const array = supporters
.replace(/(?:\r\n|\r|\n)/g, "")
@@ -150,7 +151,9 @@ optionsContent.addEventListener("input", function (event) {
else if (id === "regionsInput" || id === "regionsOutput") changeStatesNumber(value);
else if (id === "emblemShape") changeEmblemShape(value);
else if (id === "tooltipSizeInput" || id === "tooltipSizeOutput") changeTooltipSize(value);
- else if (id === "transparencyInput") changeDialogsTransparency(value);
+ else if (id === "themeHueInput") changeThemeHue(value);
+ else if (id === "themeColorInput") changeDialogsTheme(themeColorInput.value, transparencyInput.value);
+ else if (id === "transparencyInput") changeDialogsTheme(themeColorInput.value, value);
});
optionsContent.addEventListener("change", function (event) {
@@ -158,23 +161,24 @@ optionsContent.addEventListener("change", function (event) {
const value = event.target.value;
if (id === "zoomExtentMin" || id === "zoomExtentMax") changeZoomExtent(value);
- else if (id === "optionsSeed") generateMapWithSeed();
+ else if (id === "optionsSeed") generateMapWithSeed("seed change");
else if (id === "uiSizeInput" || id === "uiSizeOutput") changeUIsize(value);
if (id === "shapeRendering") viewbox.attr("shape-rendering", value);
else if (id === "yearInput") changeYear();
else if (id === "eraInput") changeEra();
+ else if (id === "stateLabelsModeInput") options.stateLabelsMode = value;
});
optionsContent.addEventListener("click", function (event) {
const id = event.target.id;
if (id === "toggleFullscreen") toggleFullscreen();
- else if (id === "optionsSeedGenerate") generateMapWithSeed();
else if (id === "optionsMapHistory") showSeedHistoryDialog();
else if (id === "optionsCopySeed") copyMapURL();
else if (id === "optionsEraRegenerate") regenerateEra();
else if (id === "zoomExtentDefault") restoreDefaultZoomExtent();
else if (id === "translateExtent") toggleTranslateExtent(event.target);
else if (id === "speakerTest") testSpeaker();
+ else if (id === "themeColorRestore") restoreDefaultThemeColor();
});
function mapSizeInputChange() {
@@ -208,8 +212,8 @@ function changeMapSize() {
// just apply canvas size that was already set
function applyMapSize() {
- const zoomMin = +zoomExtentMin.value,
- zoomMax = +zoomExtentMax.value;
+ const zoomMin = +zoomExtentMin.value;
+ const zoomMax = +zoomExtentMax.value;
graphWidth = +mapWidthInput.value;
graphHeight = +mapHeightInput.value;
svgWidth = Math.min(graphWidth, window.innerWidth);
@@ -277,12 +281,9 @@ function testSpeaker() {
speechSynthesis.speak(speaker);
}
-function generateMapWithSeed() {
- if (optionsSeed.value == seed) {
- tip("The current map already has this seed", false, "error");
- return;
- }
- regeneratePrompt();
+function generateMapWithSeed(source) {
+ if (optionsSeed.value == seed) return tip("The current map already has this seed", false, "error");
+ regeneratePrompt(source);
}
function showSeedHistoryDialog() {
@@ -313,7 +314,7 @@ function restoreSeed(id) {
mapHeightInput.value = mapHistory[id].height;
templateInput.value = mapHistory[id].template;
if (locked("template")) unlock("template");
- regeneratePrompt();
+ regeneratePrompt("seed history");
}
function restoreDefaultZoomExtent() {
@@ -417,7 +418,7 @@ function changeUIsize(value) {
if (+value > max) value = max;
uiSizeInput.value = uiSizeOutput.value = value;
- document.getElementsByTagName("body")[0].style.fontSize = value * 11 + "px";
+ document.getElementsByTagName("body")[0].style.fontSize = rn(value * 10, 2) + "px";
document.getElementById("options").style.width = value * 300 + "px";
}
@@ -429,26 +430,56 @@ function changeTooltipSize(value) {
tooltip.style.fontSize = `calc(${value}px + 0.5vw)`;
}
-// change transparency for modal windows
-function changeDialogsTransparency(value) {
- transparencyInput.value = transparencyOutput.value = value;
- const alpha = (100 - +value) / 100;
- const optionsColor = "rgba(164, 139, 149, " + alpha + ")";
- const dialogsColor = "rgba(255, 255, 255, " + alpha + ")";
- const optionButtonsColor = "rgba(145, 110, 127, " + Math.min(alpha + 0.3, 1) + ")";
- const optionLiColor = "rgba(153, 123, 137, " + Math.min(alpha + 0.3, 1) + ")";
- document.getElementById("options").style.backgroundColor = optionsColor;
- document.getElementById("dialogs").style.backgroundColor = dialogsColor;
- document.querySelectorAll(".tabcontent button").forEach(el => (el.style.backgroundColor = optionButtonsColor));
- document.querySelectorAll(".tabcontent li").forEach(el => (el.style.backgroundColor = optionLiColor));
- document.querySelectorAll("button.options").forEach(el => (el.style.backgroundColor = optionLiColor));
+const THEME_COLOR = "#997787";
+function restoreDefaultThemeColor() {
+ localStorage.removeItem("themeColor");
+ changeDialogsTheme(THEME_COLOR, transparencyInput.value);
+}
+
+function changeThemeHue(hue) {
+ const {s, l} = d3.hsl(themeColorInput.value);
+ const newColor = d3.hsl(+hue, s, l).hex();
+ changeDialogsTheme(newColor, transparencyInput.value);
+}
+
+// change color and transparency for modal windows
+function changeDialogsTheme(themeColor, transparency) {
+ transparencyInput.value = transparencyOutput.value = transparency;
+ const alpha = (100 - +transparency) / 100;
+ const alphaReduced = Math.min(alpha + 0.3, 1);
+
+ const {h, s, l} = d3.hsl(themeColor || THEME_COLOR);
+ themeColorInput.value = themeColor || THEME_COLOR;
+ themeHueInput.value = h;
+
+ const getRGBA = (hue, saturation, lightness, alpha) => {
+ const color = d3.hsl(hue, saturation, lightness, alpha);
+ return color.toString();
+ };
+
+ const theme = [
+ {name: "--bg-main", h, s, l, alpha},
+ {name: "--bg-lighter", h, s, l: l + 0.02, alpha},
+ {name: "--bg-light", h, s: s - 0.02, l: l + 0.06, alpha},
+ {name: "--light-solid", h, s: s + 0.01, l: l + 0.05, alpha: 1},
+ {name: "--dark-solid", h, s, l: l - 0.2, alpha: 1},
+ {name: "--header", h, s: s, l: l - 0.03, alpha: alphaReduced},
+ {name: "--header-active", h, s: s, l: l - 0.09, alpha: alphaReduced},
+ {name: "--bg-disabled", h, s: s - 0.04, l: l + 0.09, alphaReduced},
+ {name: "--bg-dialogs", h: 0, s: 0, l: 0.98, alpha}
+ ];
+
+ const sx = document.documentElement.style;
+ theme.forEach(({name, h, s, l, alpha}) => {
+ sx.setProperty(name, getRGBA(h, s, l, alpha));
+ });
}
function changeZoomExtent(value) {
- const min = Math.max(+zoomExtentMin.value, 0.01),
- max = Math.min(+zoomExtentMax.value, 200);
+ const min = Math.max(+zoomExtentMin.value, 0.01);
+ const max = Math.min(+zoomExtentMax.value, 200);
zoom.scaleExtent([min, max]);
- const scale = Math.max(Math.min(+value, 200), 0.01);
+ const scale = minmax(+value, 0.01, 200);
zoom.scaleTo(svg, scale);
}
@@ -484,13 +515,12 @@ function applyStoredOptions() {
.map(w => +w);
if (localStorage.getItem("military")) options.military = JSON.parse(localStorage.getItem("military"));
- changeDialogsTransparency(localStorage.getItem("transparency") || 5);
if (localStorage.getItem("tooltipSize")) changeTooltipSize(localStorage.getItem("tooltipSize"));
if (localStorage.getItem("regions")) changeStatesNumber(localStorage.getItem("regions"));
uiSizeInput.max = uiSizeOutput.max = getUImaxSize();
if (localStorage.getItem("uiSize")) changeUIsize(localStorage.getItem("uiSize"));
- else changeUIsize(Math.max(Math.min(rn(mapWidthInput.value / 1280, 1), 2.5), 1));
+ else changeUIsize(minmax(rn(mapWidthInput.value / 1280, 1), 1, 2.5));
// search params overwrite stored and default options
const params = new URL(window.location.href).searchParams;
@@ -499,8 +529,14 @@ function applyStoredOptions() {
if (width) mapWidthInput.value = width;
if (height) mapHeightInput.value = height;
+ const transparency = localStorage.getItem("transparency") || 5;
+ const themeColor = localStorage.getItem("themeColor");
+ changeDialogsTheme(themeColor, transparency);
+
// set shape rendering
viewbox.attr("shape-rendering", shapeRendering.value);
+
+ options.stateLabelsMode = stateLabelsModeInput.value;
}
// randomize options if randomization is allowed (not locked or options='default')
@@ -531,10 +567,9 @@ function randomizeOptions() {
// 'Units Editor' settings
const US = navigator.language === "en-US";
- const UK = navigator.language === "en-GB";
if (randomize || !locked("distanceScale")) distanceScaleOutput.value = distanceScaleInput.value = gauss(3, 1, 1, 5);
- if (!stored("distanceUnit")) distanceUnitInput.value = US || UK ? "mi" : "km";
- if (!stored("heightUnit")) heightUnit.value = US || UK ? "ft" : "m";
+ if (!stored("distanceUnit")) distanceUnitInput.value = US ? "mi" : "km";
+ if (!stored("heightUnit")) heightUnit.value = US ? "ft" : "m";
if (!stored("temperatureScale")) temperatureScale.value = US ? "°F" : "°C";
// World settings
@@ -621,23 +656,17 @@ function restoreDefaultOptions() {
// Sticked menu Options listeners
document.getElementById("sticked").addEventListener("click", function (event) {
const id = event.target.id;
- if (id === "newMapButton") regeneratePrompt();
+ if (id === "newMapButton") regeneratePrompt("sticky button");
else if (id === "saveButton") showSavePane();
else if (id === "exportButton") showExportPane();
else if (id === "loadButton") showLoadPane();
else if (id === "zoomReset") resetZoom(1000);
});
-function regeneratePrompt() {
- if (customization) {
- tip("New map cannot be generated when edit mode is active, please exit the mode and retry", false, "error");
- return;
- }
+function regeneratePrompt(source) {
+ if (customization) return tip("New map cannot be generated when edit mode is active, please exit the mode and retry", false, "error");
const workingTime = (Date.now() - last(mapHistory).created) / 60000; // minutes
- if (workingTime < 5) {
- regenerateMap();
- return;
- }
+ if (workingTime < 5) return regenerateMap(source);
alertMessage.innerHTML = `Are you sure you want to generate a new map?
All unsaved changes made to the current map will be lost`;
@@ -650,7 +679,7 @@ function regeneratePrompt() {
},
Generate: function () {
closeDialogs();
- regenerateMap();
+ regenerateMap(source);
}
}
});
@@ -770,6 +799,9 @@ function openSaveTiles() {
status.innerHTML = "";
let loading = null;
+ const inputs = document.getElementById("saveTilesScreen").querySelectorAll("input");
+ inputs.forEach(input => input.addEventListener("input", updateTilesOptions));
+
$("#saveTilesScreen").dialog({
resizable: false,
title: "Download tiles",
@@ -790,17 +822,13 @@ function openSaveTiles() {
}
},
close: () => {
+ inputs.forEach(input => input.removeEventListener("input", updateTilesOptions));
debug.selectAll("*").remove();
clearInterval(loading);
}
});
}
-document
- .getElementById("saveTilesScreen")
- .querySelectorAll("input")
- .forEach(el => el.addEventListener("input", updateTilesOptions));
-
function updateTilesOptions() {
if (this?.tagName === "INPUT") {
const {nextElementSibling: next, previousElementSibling: prev} = this;
diff --git a/modules/ui/provinces-editor.js b/modules/ui/provinces-editor.js
index 7987a56b..84eca9b0 100644
--- a/modules/ui/provinces-editor.js
+++ b/modules/ui/provinces-editor.js
@@ -34,6 +34,7 @@ function editProvinces() {
document.getElementById("provincesManually").addEventListener("click", enterProvincesManualAssignent);
document.getElementById("provincesManuallyApply").addEventListener("click", applyProvincesManualAssignent);
document.getElementById("provincesManuallyCancel").addEventListener("click", () => exitProvincesManualAssignment());
+ document.getElementById("provincesRelease").addEventListener("click", triggerProvincesRelease);
document.getElementById("provincesAdd").addEventListener("click", enterAddProvinceMode);
document.getElementById("provincesRecolor").addEventListener("click", recolorProvinces);
@@ -129,19 +130,27 @@ function editProvinces() {
const separable = p.burg && p.burg !== pack.states[p.state].capital;
const focused = defs.select("#fog #focusProvince" + p.i).size();
COArenderer.trigger("provinceCOA" + p.i, p.coa);
- lines += `
-
+ lines += `
`;
@@ -222,74 +231,64 @@ function editProvinces() {
function capitalZoomIn(p) {
const capital = pack.provinces[p].burg;
const l = burgLabels.select("[data-id='" + capital + "']");
- const x = +l.attr("x"),
- y = +l.attr("y");
+ const x = +l.attr("x");
+ const y = +l.attr("y");
zoomTo(x, y, 8, 2000);
}
function triggerIndependencePromps(p) {
- alertMessage.innerHTML = "Are you sure you want to declare province independence?
It will turn province into a new state";
- $("#alert").dialog({
- resizable: false,
+ confirmationDialog({
title: "Declare independence",
- buttons: {
- Declare: function () {
- declareProvinceIndependence(p);
- $(this).dialog("close");
- },
- Cancel: function () {
- $(this).dialog("close");
- }
+ message: "Are you sure you want to declare province independence?
It will turn province into a new state",
+ confirm: "Declare",
+ onConfirm: () => {
+ const [oldStateId, newStateId] = declareProvinceIndependence(p);
+ updateStatesPostRelease([oldStateId], [newStateId]);
}
});
}
- function declareProvinceIndependence(p) {
- const states = pack.states,
- provinces = pack.provinces,
- cells = pack.cells;
- if (provinces[p].burgs.some(b => pack.burgs[b].capital)) {
- tip("Cannot declare independence of a province having capital burg. Please change capital first", false, "error");
- return;
- }
+ function declareProvinceIndependence(provinceId) {
+ const {states, provinces, cells, burgs} = pack;
+ const province = provinces[provinceId];
+ const {name, burg: burgId, burgs: provinceBurgs} = province;
- const oldState = pack.provinces[p].state;
- const newState = pack.states.length;
+ if (provinceBurgs.some(b => burgs[b].capital))
+ return tip("Cannot declare independence of a province having capital burg. Please change capital first", false, "error");
+ if (!burgId) return tip("Cannot declare independence of a province without burg", false, "error");
+
+ const oldStateId = province.state;
+ const newStateId = states.length;
// turn province burg into a capital
- const burg = provinces[p].burg;
- if (!burg) return;
- pack.burgs[burg].capital = 1;
- moveBurgToGroup(burg, "cities");
+ burgs[burgId].capital = 1;
+ moveBurgToGroup(burgId, "cities");
// move all burgs to a new state
- provinces[p].burgs.forEach(b => (pack.burgs[b].state = newState));
+ province.burgs.forEach(b => (burgs[b].state = newStateId));
// difine new state attributes
- const center = pack.burgs[burg].cell;
- const culture = pack.burgs[burg].culture;
- const name = provinces[p].name;
+ const {cell: center, culture} = burgs[burgId];
const color = getRandomColor();
-
- const coa = provinces[p].coa;
- const coaEl = document.getElementById("provinceCOA" + p);
- if (coaEl) coaEl.id = "stateCOA" + newState;
- emblems.select(`#provinceEmblems > use[data-i='${p}']`).remove();
+ const coa = province.coa;
+ const coaEl = document.getElementById("provinceCOA" + provinceId);
+ if (coaEl) coaEl.id = "stateCOA" + newStateId;
+ emblems.select(`#provinceEmblems > use[data-i='${provinceId}']`).remove();
// update cells
cells.i
- .filter(i => cells.province[i] === p)
+ .filter(i => cells.province[i] === provinceId)
.forEach(i => {
cells.province[i] = 0;
- cells.state[i] = newState;
+ cells.state[i] = newStateId;
});
// update diplomacy and reverse relations
const diplomacy = states.map(s => {
if (!s.i || s.removed) return "x";
- let relations = states[oldState].diplomacy[s.i]; // relations between Nth state and old overlord
- if (s.i === oldState) relations = "Enemy";
- // new state is Enemy to its old overlord
+ let relations = states[oldStateId].diplomacy[s.i]; // relations between Nth state and old overlord
+ // new state is Enemy to its old owner
+ if (s.i === oldStateId) relations = "Enemy";
else if (relations === "Ally") relations = "Suspicion";
else if (relations === "Friendly") relations = "Suspicion";
else if (relations === "Suspicion") relations = "Neutral";
@@ -301,28 +300,51 @@ function editProvinces() {
return relations;
});
diplomacy.push("x");
- states[0].diplomacy.push([`Independance declaration`, `${name} declared its independance from ${states[oldState].name}`]);
+ states[0].diplomacy.push([`Independance declaration`, `${name} declared its independance from ${states[oldStateId].name}`]);
// create new state
- states.push({i: newState, name, diplomacy, provinces: [], color, expansionism: 0.5, capital: burg, type: "Generic", center, culture, military: [], alert: 1, coa});
- BurgsAndStates.collectStatistics();
- BurgsAndStates.defineStateForms([newState]);
-
- if (layerIsOn("toggleProvinces")) toggleProvinces();
- if (!layerIsOn("toggleStates")) toggleStates();
- else drawStates();
- if (!layerIsOn("toggleBorders")) toggleBorders();
- else drawBorders();
- BurgsAndStates.drawStateLabels([newState, oldState]);
+ states.push({
+ i: newStateId,
+ name,
+ diplomacy,
+ provinces: [],
+ color,
+ expansionism: 0.5,
+ capital: burgId,
+ type: "Generic",
+ center,
+ culture,
+ military: [],
+ alert: 1,
+ coa
+ });
// remove old province
- unfog("focusProvince" + p);
- if (states[oldState].provinces.includes(p)) states[oldState].provinces.splice(states[oldState].provinces.indexOf(p), 1);
- provinces[p] = {i: p, removed: true};
+ states[oldStateId].provinces = states[oldStateId].provinces.filter(p => p !== provinceId);
+ provinces[provinceId] = {i: provinceId, removed: true};
- // draw emblem
- COArenderer.add("state", newState, coa, pack.states[newState].pole[0], pack.states[newState].pole[1]);
+ return [oldStateId, newStateId];
+ }
+ function updateStatesPostRelease(oldStates, newStates) {
+ const allStates = unique([...oldStates, ...newStates]);
+
+ layerIsOn("toggleProvinces") && toggleProvinces();
+ layerIsOn("toggleStates") ? drawStates() : toggleStates();
+ layerIsOn("toggleBorders") ? drawBorders() : toggleBorders();
+
+ BurgsAndStates.collectStatistics();
+ BurgsAndStates.defineStateForms(newStates);
+ BurgsAndStates.drawStateLabels(allStates);
+
+ // redraw emblems
+ allStates.forEach(stateId => {
+ emblems.select(`#stateEmblems > use[data-i='${stateId}']`)?.remove();
+ const {coa, pole} = pack.states[stateId];
+ COArenderer.add("state", stateId, coa, ...pole);
+ });
+
+ unfog();
closeDialogs();
editStates();
}
@@ -541,7 +563,17 @@ function editProvinces() {
const provinces = pack.provinces
.filter(p => p.i && !p.removed)
.map(p => {
- return {id: p.i + states.length - 1, i: p.i, state: p.state, color: p.color, name: p.name, fullName: p.fullName, area: p.area, urban: p.urban, rural: p.rural};
+ return {
+ id: p.i + states.length - 1,
+ i: p.i,
+ state: p.state,
+ color: p.color,
+ name: p.name,
+ fullName: p.fullName,
+ area: p.area,
+ urban: p.urban,
+ rural: p.rural
+ };
});
const data = states.concat(provinces);
const root = d3
@@ -564,7 +596,13 @@ function editProvinces() {
`;
alertMessage.innerHTML += `
`;
- const svg = d3.select("#alertMessage").insert("svg", "#provinceInfo").attr("id", "provincesTree").attr("width", width).attr("height", height).attr("font-size", "10px");
+ const svg = d3
+ .select("#alertMessage")
+ .insert("svg", "#provinceInfo")
+ .attr("id", "provincesTree")
+ .attr("width", width)
+ .attr("height", height)
+ .attr("font-size", "10px");
const graph = svg.append("g").attr("transform", `translate(10, 0)`);
document.getElementById("provincesTreeType").addEventListener("change", updateChart);
@@ -589,7 +627,14 @@ function editProvinces() {
const rural = rn(d.data.rural * populationRate);
const urban = rn(d.data.urban * populationRate * urbanization);
- const value = provincesTreeType.value === "area" ? "Area: " + area : provincesTreeType.value === "rural" ? "Rural population: " + si(rural) : provincesTreeType.value === "urban" ? "Urban population: " + si(urban) : "Population: " + si(rural + urban);
+ const value =
+ provincesTreeType.value === "area"
+ ? "Area: " + area
+ : provincesTreeType.value === "rural"
+ ? "Rural population: " + si(rural)
+ : provincesTreeType.value === "urban"
+ ? "Urban population: " + si(urban)
+ : "Population: " + si(rural + urban);
provinceInfo.innerHTML = `${name}. ${state}. ${value}`;
provinceHighlightOn(ev);
@@ -637,7 +682,8 @@ function editProvinces() {
}
function updateChart() {
- const value = this.value === "area" ? d => d.area : this.value === "rural" ? d => d.rural : this.value === "urban" ? d => d.urban : d => d.rural + d.urban;
+ const value =
+ this.value === "area" ? d => d.area : this.value === "rural" ? d => d.rural : this.value === "urban" ? d => d.urban : d => d.rural + d.urban;
root.sum(value);
node.data(treeLayout(root).leaves());
@@ -681,6 +727,34 @@ function editProvinces() {
provs.selectAll("text").call(d3.drag().on("drag", dragLabel)).classed("draggable", true);
}
+ function triggerProvincesRelease() {
+ confirmationDialog({
+ title: "Release provinces",
+ message: `Are you sure you want to release all provinces?
+ It will turn all separable provinces into independent states.
+ Capital province and provinces without any burgs will state as they are`,
+ confirm: "Release",
+ onConfirm: () => {
+ const oldStateIds = [];
+ const newStateIds = [];
+
+ body.querySelectorAll(":scope > div").forEach(el => {
+ const provinceId = +el.dataset.id;
+ const province = pack.provinces[provinceId];
+ if (!province.burg) return;
+ if (province.burg === pack.states[province.state].capital) return;
+ if (province.burgs.some(burgId => pack.burgs[burgId].capital)) return;
+
+ const [oldStateId, newStateId] = declareProvinceIndependence(provinceId);
+ oldStateIds.push(oldStateId);
+ newStateIds.push(newStateId);
+ });
+
+ updateStatesPostRelease(unique(oldStateIds), newStateIds);
+ }
+ });
+ }
+
function enterProvincesManualAssignent() {
if (!layerIsOn("toggleProvinces")) toggleProvinces();
if (!layerIsOn("toggleBorders")) toggleBorders();
@@ -783,7 +857,13 @@ function editProvinces() {
if (pack.cells.province[i] === provinceNew) exists.remove();
else exists.attr("data-province", provinceNew).attr("fill", fill);
} else {
- temp.append("polygon").attr("points", getPackPolygon(i)).attr("data-cell", i).attr("data-province", provinceNew).attr("fill", fill).attr("stroke", "#555");
+ temp
+ .append("polygon")
+ .attr("points", getPackPolygon(i))
+ .attr("data-cell", i)
+ .attr("data-province", provinceNew)
+ .attr("fill", fill)
+ .attr("stroke", "#555");
}
});
}
@@ -839,10 +919,8 @@ function editProvinces() {
}
function enterAddProvinceMode() {
- if (this.classList.contains("pressed")) {
- exitAddProvinceMode();
- return;
- }
+ if (this.classList.contains("pressed")) return exitAddProvinceMode();
+
customization = 12;
this.classList.add("pressed");
tip("Click on the map to place a new province center", true);
@@ -851,24 +929,17 @@ function editProvinces() {
}
function addProvince() {
- const cells = pack.cells,
- provinces = pack.provinces;
+ const {cells, provinces} = pack;
const point = d3.mouse(this);
const center = findCell(point[0], point[1]);
- if (cells.h[center] < 20) {
- tip("You cannot place province into the water. Please click on a land cell", false, "error");
- return;
- }
+ if (cells.h[center] < 20) return tip("You cannot place province into the water. Please click on a land cell", false, "error");
+
const oldProvince = cells.province[center];
- if (oldProvince && provinces[oldProvince].center === center) {
- tip("The cell is already a center of a different province. Select other cell", false, "error");
- return;
- }
+ if (oldProvince && provinces[oldProvince].center === center)
+ return tip("The cell is already a center of a different province. Select other cell", false, "error");
+
const state = cells.state[center];
- if (!state) {
- tip("You cannot create a province in neutral lands. Please assign this land to a state first", false, "error");
- return;
- }
+ if (!state) return tip("You cannot create a province in neutral lands. Please assign this land to a state first", false, "error");
if (d3.event.shiftKey === false) exitAddProvinceMode();
@@ -879,8 +950,8 @@ function editProvinces() {
const name = burg ? pack.burgs[burg].name : Names.getState(Names.getCultureShort(c), c);
const formName = oldProvince ? provinces[oldProvince].formName : "Province";
const fullName = name + " " + formName;
- const stateColor = pack.states[state].color,
- rndColor = getRandomColor();
+ const stateColor = pack.states[state].color;
+ const rndColor = getRandomColor();
const color = stateColor[0] === "#" ? d3.color(d3.interpolate(stateColor, rndColor)(0.2)).hex() : rndColor;
// generate emblem
diff --git a/modules/ui/rivers-overview.js b/modules/ui/rivers-overview.js
index fcde2ae5..4c19bf35 100644
--- a/modules/ui/rivers-overview.js
+++ b/modules/ui/rivers-overview.js
@@ -91,7 +91,7 @@ function overviewRivers() {
function zoomToRiver() {
const r = +this.parentNode.dataset.id;
const river = rivers.select("#river" + r).node();
- highlightElement(river);
+ highlightElement(river, 3);
}
function toggleBasinsHightlight() {
diff --git a/modules/ui/states-editor.js b/modules/ui/states-editor.js
index 68b35a77..5d63cf1c 100644
--- a/modules/ui/states-editor.js
+++ b/modules/ui/states-editor.js
@@ -126,9 +126,15 @@ function editStates() {
const capital = pack.burgs[s.capital].name;
COArenderer.trigger("stateCOA" + s.i, s.coa);
- lines += `
-
+ lines += `
+
@@ -143,7 +149,9 @@ function editStates() {
${si(population)}
-
+
${s.cells}
@@ -200,7 +208,15 @@ function editStates() {
if (customization || !state) return;
const d = regions.select("#state" + state).attr("d");
- const path = debug.append("path").attr("class", "highlight").attr("d", d).attr("fill", "none").attr("stroke", "red").attr("stroke-width", 1).attr("opacity", 1).attr("filter", "url(#blur1)");
+ const path = debug
+ .append("path")
+ .attr("class", "highlight")
+ .attr("d", d)
+ .attr("fill", "none")
+ .attr("stroke", "red")
+ .attr("stroke-width", 1)
+ .attr("opacity", 1)
+ .attr("filter", "url(#blur1)");
const l = path.node().getTotalLength(),
dur = (l + 5000) / 2;
@@ -498,6 +514,7 @@ function editStates() {
pack.cells.province.forEach((pr, i) => {
if (pr === p) pack.cells.province[i] = 0;
});
+
const coaId = "provinceCOA" + p;
if (document.getElementById(coaId)) document.getElementById(coaId).remove();
emblems.select(`#provinceEmblems > use[data-i='${p}']`).remove();
@@ -564,19 +581,20 @@ function editStates() {
function showStatesChart() {
// build hierarchy tree
- const data = pack.states.filter(s => !s.removed);
+ const statesData = pack.states.filter(s => !s.removed);
+ if (statesData.length < 2) return tip("There are no states to show", false, "error");
+
const root = d3
.stratify()
.id(d => d.i)
- .parentId(d => (d.i ? 0 : null))(data)
+ .parentId(d => (d.i ? 0 : null))(statesData)
.sum(d => d.area)
.sort((a, b) => b.value - a.value);
- const width = 150 + 200 * uiSizeOutput.value,
- height = 150 + 200 * uiSizeOutput.value;
+ const size = 150 + 200 * uiSizeOutput.value;
const margin = {top: 0, right: -50, bottom: 0, left: -50};
- const w = width - margin.left - margin.right;
- const h = height - margin.top - margin.bottom;
+ const w = size - margin.left - margin.right;
+ const h = size - margin.top - margin.bottom;
const treeLayout = d3.pack().size([w, h]).padding(3);
// prepare svg
@@ -588,7 +606,16 @@ function editStates() {
`;
alertMessage.innerHTML += `
`;
- const svg = d3.select("#alertMessage").insert("svg", "#statesInfo").attr("id", "statesTree").attr("width", width).attr("height", height).style("font-family", "Almendra SC").attr("text-anchor", "middle").attr("dominant-baseline", "central");
+
+ const svg = d3
+ .select("#alertMessage")
+ .insert("svg", "#statesInfo")
+ .attr("id", "statesTree")
+ .attr("width", size)
+ .attr("height", size)
+ .style("font-family", "Almendra SC")
+ .attr("text-anchor", "middle")
+ .attr("dominant-baseline", "central");
const graph = svg.append("g").attr("transform", `translate(-50, 0)`);
document.getElementById("statesTreeType").addEventListener("change", updateChart);
@@ -632,7 +659,16 @@ function editStates() {
const urban = rn(d.data.urban * populationRate * urbanization);
const option = statesTreeType.value;
- const value = option === "area" ? "Area: " + area : option === "rural" ? "Rural population: " + si(rural) : option === "urban" ? "Urban population: " + si(urban) : option === "burgs" ? "Burgs number: " + d.data.burgs : "Population: " + si(rural + urban);
+ const value =
+ option === "area"
+ ? "Area: " + area
+ : option === "rural"
+ ? "Rural population: " + si(rural)
+ : option === "urban"
+ ? "Urban population: " + si(urban)
+ : option === "burgs"
+ ? "Burgs number: " + d.data.burgs
+ : "Population: " + si(rural + urban);
statesInfo.innerHTML = `${state}. ${value}`;
stateHighlightOn(ev);
@@ -646,7 +682,16 @@ function editStates() {
}
function updateChart() {
- const value = this.value === "area" ? d => d.area : this.value === "rural" ? d => d.rural : this.value === "urban" ? d => d.urban : this.value === "burgs" ? d => d.burgs : d => d.rural + d.urban;
+ const value =
+ this.value === "area"
+ ? d => d.area
+ : this.value === "rural"
+ ? d => d.rural
+ : this.value === "urban"
+ ? d => d.urban
+ : this.value === "burgs"
+ ? d => d.burgs
+ : d => d.rural + d.urban;
root.sum(value);
node.data(treeLayout(root).leaves());
@@ -731,7 +776,11 @@ function editStates() {
$("#statesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
tip("Click on state to select, drag the circle to change state", true);
- viewbox.style("cursor", "crosshair").on("click", selectStateOnMapClick).call(d3.drag().on("start", dragStateBrush)).on("touchmove mousemove", moveStateBrush);
+ viewbox
+ .style("cursor", "crosshair")
+ .on("click", selectStateOnMapClick)
+ .call(d3.drag().on("start", dragStateBrush))
+ .on("touchmove mousemove", moveStateBrush);
body.querySelector("div").classList.add("selected");
}
@@ -797,9 +846,9 @@ function editStates() {
}
function applyStatesManualAssignent() {
- const cells = pack.cells,
- affectedStates = [],
- affectedProvinces = [];
+ const {cells} = pack;
+ const affectedStates = [];
+ const affectedProvinces = [];
statesBody
.select("#temp")
@@ -815,77 +864,145 @@ function editStates() {
if (affectedStates.length) {
refreshStatesEditor();
- if (!layerIsOn("toggleStates")) toggleStates();
- else drawStates();
+ layerIsOn("toggleStates") ? drawStates() : toggleStates();
if (adjustLabels.checked) BurgsAndStates.drawStateLabels([...new Set(affectedStates)]);
adjustProvinces([...new Set(affectedProvinces)]);
- if (!layerIsOn("toggleBorders")) toggleBorders();
- else drawBorders();
+ layerIsOn("toggleBorders") ? drawBorders() : toggleBorders();
if (layerIsOn("toggleProvinces")) drawProvinces();
}
+
exitStatesManualAssignment();
}
function adjustProvinces(affectedProvinces) {
- const {cells, provinces, states} = pack;
- const form = {Zone: 1, Area: 1, Territory: 2, Province: 1};
+ const {cells, provinces, states, burgs} = pack;
- affectedProvinces.forEach(p => {
- if (!p) return; // do nothing if neutral lands are captured
- const old = provinces[p].state;
-
- // remove province from state provinces list
- if (states[old]?.provinces?.includes(p)) states[old].provinces.splice(states[old].provinces.indexOf(p), 1);
+ affectedProvinces.forEach(provinceId => {
+ if (!provinces[provinceId]) return; // lands without province captured => do nothing
// find states owning at least 1 province cell
- const provCells = cells.i.filter(i => cells.province[i] === p);
+ const provCells = cells.i.filter(i => cells.province[i] === provinceId);
const provStates = [...new Set(provCells.map(i => cells.state[i]))];
- // assign province to its center owner; if center is neutral, remove province
- const owner = cells.state[provinces[p].center];
- if (owner) {
- const name = provinces[p].name;
+ // province is captured completely => change owner or remove
+ if (provinceId && provStates.length === 1) return changeProvinceOwner(provinceId, provStates[0], provCells);
- // if province is a historical part of another state's province, unite with old province
- const part = states[owner].provinces.find(n => name.includes(provinces[n].name));
- if (part) {
- provinces[p].removed = true;
- provCells.filter(i => cells.state[i] === owner).forEach(i => (cells.province[i] = part));
- } else {
- provinces[p].state = owner;
- states[owner].provinces.push(p);
- provinces[p].color = getMixedColor(states[owner].color);
- }
- } else {
- provinces[p].removed = true;
- provCells.filter(i => !cells.state[i]).forEach(i => (cells.province[i] = 0));
- }
-
- // create new provinces for non-main part
- provStates
- .filter(s => s && s !== owner)
- .forEach(s =>
- createProvince(
- p,
- s,
- provCells.filter(i => cells.state[i] === s)
- )
- );
+ // province is captured partially => split province
+ splitProvince(provinceId, provStates, provCells);
});
- function createProvince(initProv, state, provCells) {
- const province = provinces.length;
- provCells.forEach(i => (cells.province[i] = province));
+ function changeProvinceOwner(provinceId, newOwnerId, provinceCells) {
+ const province = provinces[provinceId];
+ const prevOwner = states[province.state];
- const burgCell = provCells.find(i => cells.burg[i]);
- const center = burgCell ? burgCell : provCells[0];
- const burg = burgCell ? cells.burg[burgCell] : 0;
+ // remove province from old owner list
+ prevOwner.provinces = prevOwner.provinces.filter(province => province !== provinceId);
- const name = burgCell && P(0.7) ? getAdjective(pack.burgs[burg].name) : getAdjective(states[state].name) + " " + provinces[initProv].name.split(" ").slice(-1)[0];
- const formName = name.split(" ").length > 1 ? provinces[initProv].formName : rw(form);
- const fullName = name + " " + formName;
- const color = getMixedColor(states[state].color);
- provinces.push({i: province, state, center, burg, name, formName, fullName, color});
+ if (newOwnerId) {
+ // new owner is a state => change owner
+ province.state = newOwnerId;
+ states[newOwnerId].provinces.push(provinceId);
+ } else {
+ // new owner is neutral => remove province
+ provinces[provinceId] = {i: provinceId, removed: true};
+ provinceCells.forEach(i => {
+ cells.province[i] = 0;
+ });
+ }
+ }
+
+ function splitProvince(provinceId, provinceStates, provinceCells) {
+ const province = provinces[provinceId];
+ const prevOwner = states[province.state];
+ const provinceCenterOwner = cells.state[province.center];
+
+ provinceStates.forEach(stateId => {
+ const stateProvinceCells = provinceCells.filter(i => cells.state[i] === stateId);
+
+ if (stateId === provinceCenterOwner) {
+ // province center is owned by the same state => do nothing for this state
+ if (stateId === prevOwner.i) return;
+
+ // province center is captured by neutrals => remove province
+ if (!stateId) {
+ provinces[provinceId] = {i: provinceId, removed: true};
+ stateProvinceCells.forEach(i => {
+ cells.province[i] = 0;
+ });
+ return;
+ }
+
+ // reassign province ownership to province center owner
+ prevOwner.provinces = prevOwner.provinces.filter(province => province !== provinceId);
+ province.state = stateId;
+ province.color = getMixedColor(states[stateId].color);
+ states[stateId].provinces.push(provinceId);
+ return;
+ }
+
+ // province cells captured by neutrals => remove captured cells from province
+ if (!stateId) {
+ stateProvinceCells.forEach(i => {
+ cells.province[i] = 0;
+ });
+ return;
+ }
+
+ // a few province cells owned by state => add to closes province
+ if (stateProvinceCells.length < 20) {
+ const closestProvince = findClosestProvince(provinceId, stateId, stateProvinceCells);
+ if (closestProvince) {
+ stateProvinceCells.forEach(i => {
+ cells.province[i] = closestProvince;
+ });
+ return;
+ }
+ }
+
+ // some province cells owned by state => create new province
+ createProvince(province, stateId, stateProvinceCells);
+ });
+ }
+
+ function createProvince(oldProvince, stateId, provinceCells) {
+ const newProvinceId = provinces.length;
+ const burgCell = provinceCells.find(i => cells.burg[i]);
+ const center = burgCell ? burgCell : provinceCells[0];
+ const burgId = burgCell ? cells.burg[burgCell] : 0;
+ const burg = burgId ? burgs[burgId] : null;
+ const culture = cells.culture[center];
+
+ const nameByBurg = burgCell && P(0.5);
+ const name = nameByBurg ? burg.name : oldProvince.name || Names.getState(Names.getCultureShort(culture), culture);
+
+ const formOptions = ["Zone", "Area", "Territory", "Province"];
+ const formName = burgCell && oldProvince.formName ? oldProvince.formName : ra(formOptions);
+
+ const color = getMixedColor(states[stateId].color);
+
+ const kinship = nameByBurg ? 0.8 : 0.4;
+ const type = BurgsAndStates.getType(center, burg?.port);
+ const coa = COA.generate(burg?.coa || states[stateId].coa, kinship, burg ? null : 0.9, type);
+ coa.shield = COA.getShield(culture, stateId);
+
+ provinces.push({i: newProvinceId, state: stateId, center, burg: burgId, name, formName, fullName: `${name} ${formName}`, color, coa});
+
+ provinceCells.forEach(i => {
+ cells.province[i] = newProvinceId;
+ });
+
+ states[stateId].provinces.push(newProvinceId);
+ }
+
+ function findClosestProvince(provinceId, stateId, sourceCells) {
+ const borderCell = sourceCells.find(i =>
+ cells.c[i].some(c => {
+ return cells.state[c] === stateId && cells.province[c] && cells.province[c] !== provinceId;
+ })
+ );
+
+ const closesProvince = borderCell && cells.c[borderCell].map(c => cells.province[c]).find(province => province && province !== provinceId);
+ return closesProvince;
}
}
@@ -921,20 +1038,14 @@ function editStates() {
}
function addState() {
- const states = pack.states,
- burgs = pack.burgs,
- cells = pack.cells;
+ const {cells, states, burgs} = pack;
const point = d3.mouse(this);
const center = findCell(point[0], point[1]);
- if (cells.h[center] < 20) {
- tip("You cannot place state into the water. Please click on a land cell", false, "error");
- return;
- }
+ if (cells.h[center] < 20) return tip("You cannot place state into the water. Please click on a land cell", false, "error");
+
let burg = cells.burg[center];
- if (burg && burgs[burg].capital) {
- tip("Existing capital cannot be selected as a new state capital! Select other cell", false, "error");
- return;
- }
+ if (burg && burgs[burg].capital) return tip("Existing capital cannot be selected as a new state capital! Select other cell", false, "error");
+
if (!burg) burg = addBurg(point); // add new burg
const oldState = cells.state[center];
@@ -985,7 +1096,22 @@ function editStates() {
cells.state[center] = newState;
cells.province[center] = 0;
- states.push({i: newState, name, diplomacy, provinces: [], color, expansionism: 0.5, capital: burg, type: "Generic", center, culture, military: [], alert: 1, coa, pole});
+ states.push({
+ i: newState,
+ name,
+ diplomacy,
+ provinces: [],
+ color,
+ expansionism: 0.5,
+ capital: burg,
+ type: "Generic",
+ center,
+ culture,
+ military: [],
+ alert: 1,
+ coa,
+ pole
+ });
BurgsAndStates.collectStatistics();
BurgsAndStates.defineStateForms([newState]);
adjustProvinces([cells.province[center]]);
@@ -1028,7 +1154,8 @@ function editStates() {
function downloadStatesData() {
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
- let data = "Id,State,Full Name,Form,Color,Capital,Culture,Type,Expansionism,Cells,Burgs,Area " + unit + ",Total Population,Rural Population,Urban Population\n"; // headers
+ let data =
+ "Id,State,Full Name,Form,Color,Capital,Culture,Type,Expansionism,Cells,Burgs,Area " + unit + ",Total Population,Rural Population,Urban Population\n"; // headers
body.querySelectorAll(":scope > div").forEach(function (el) {
const key = parseInt(el.dataset.id);
const statePack = pack.states[key];
diff --git a/modules/ui/style.js b/modules/ui/style.js
index 84723bdb..bbaa6a93 100644
--- a/modules/ui/style.js
+++ b/modules/ui/style.js
@@ -75,7 +75,11 @@ function selectStyleElement() {
}
// stroke color and width
- if (["armies", "routes", "lakes", "borders", "cults", "relig", "cells", "coastline", "prec", "ice", "icons", "coordinates", "zones", "gridOverlay"].includes(sel)) {
+ if (
+ ["armies", "routes", "lakes", "borders", "cults", "relig", "cells", "coastline", "prec", "ice", "icons", "coordinates", "zones", "gridOverlay"].includes(
+ sel
+ )
+ ) {
styleStroke.style.display = "block";
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke");
styleStrokeWidth.style.display = "block";
@@ -331,7 +335,6 @@ styleFilterInput.addEventListener("change", function () {
styleTextureInput.addEventListener("change", function () {
if (this.value === "none") texture.select("image").attr("xlink:href", "");
- if (this.value === "default") texture.select("image").attr("xlink:href", getDefaultTexture());
else getBase64(this.value, base64 => texture.select("image").attr("xlink:href", base64));
});
@@ -784,12 +787,42 @@ function applyDefaultStyle() {
biomes.attr("opacity", null).attr("filter", null).attr("mask", "url(#land)");
ice.attr("opacity", 0.9).attr("fill", "#e8f0f6").attr("stroke", "#e8f0f6").attr("stroke-width", 1).attr("filter", "url(#dropShadow05)");
- stateBorders.attr("opacity", 0.8).attr("stroke", "#56566d").attr("stroke-width", 1).attr("stroke-dasharray", "2").attr("stroke-linecap", "butt").attr("filter", null);
- provinceBorders.attr("opacity", 0.8).attr("stroke", "#56566d").attr("stroke-width", 0.5).attr("stroke-dasharray", "0 2").attr("stroke-linecap", "round").attr("filter", null);
+ stateBorders
+ .attr("opacity", 0.8)
+ .attr("stroke", "#56566d")
+ .attr("stroke-width", 1)
+ .attr("stroke-dasharray", "2")
+ .attr("stroke-linecap", "butt")
+ .attr("filter", null);
+ provinceBorders
+ .attr("opacity", 0.8)
+ .attr("stroke", "#56566d")
+ .attr("stroke-width", 0.5)
+ .attr("stroke-dasharray", "0 2")
+ .attr("stroke-linecap", "round")
+ .attr("filter", null);
cells.attr("opacity", null).attr("stroke", "#808080").attr("stroke-width", 0.1).attr("filter", null).attr("mask", null);
- gridOverlay.attr("opacity", 0.8).attr("type", "pointyHex").attr("scale", 1).attr("dx", 0).attr("dy", 0).attr("stroke", "#777777").attr("stroke-width", 0.5).attr("stroke-dasharray", null).attr("filter", null).attr("mask", null);
- coordinates.attr("opacity", 1).attr("data-size", 12).attr("font-size", 12).attr("stroke", "#d4d4d4").attr("stroke-width", 1).attr("stroke-dasharray", 5).attr("filter", null).attr("mask", null);
+ gridOverlay
+ .attr("opacity", 0.8)
+ .attr("type", "pointyHex")
+ .attr("scale", 1)
+ .attr("dx", 0)
+ .attr("dy", 0)
+ .attr("stroke", "#777777")
+ .attr("stroke-width", 0.5)
+ .attr("stroke-dasharray", null)
+ .attr("filter", null)
+ .attr("mask", null);
+ coordinates
+ .attr("opacity", 1)
+ .attr("data-size", 12)
+ .attr("font-size", 12)
+ .attr("stroke", "#d4d4d4")
+ .attr("stroke-width", 1)
+ .attr("stroke-dasharray", 5)
+ .attr("filter", null)
+ .attr("mask", null);
compass.attr("opacity", 0.8).attr("transform", null).attr("filter", null).attr("mask", "url(#water)").attr("shape-rendering", "optimizespeed");
if (!d3.select("#initial").size()) d3.select("#rose").attr("transform", "translate(80 80) scale(.25)");
@@ -810,26 +843,68 @@ function applyDefaultStyle() {
lakes.select("#lava").attr("opacity", 0.7).attr("fill", "#90270d").attr("stroke", "#f93e0c").attr("stroke-width", 2).attr("filter", "url(#crumpled)");
lakes.select("#dry").attr("opacity", 1).attr("fill", "#c9bfa7").attr("stroke", "#8e816f").attr("stroke-width", 0.7).attr("filter", null);
- coastline.select("#sea_island").attr("opacity", 0.5).attr("stroke", "#1f3846").attr("stroke-width", 0.7).attr("auto-filter", 1).attr("filter", "url(#dropShadow)");
+ coastline
+ .select("#sea_island")
+ .attr("opacity", 0.5)
+ .attr("stroke", "#1f3846")
+ .attr("stroke-width", 0.7)
+ .attr("auto-filter", 1)
+ .attr("filter", "url(#dropShadow)");
coastline.select("#lake_island").attr("opacity", 1).attr("stroke", "#7c8eaf").attr("stroke-width", 0.35).attr("filter", null);
terrain.attr("opacity", null).attr("set", "simple").attr("size", 1).attr("density", 0.4).attr("filter", null).attr("mask", null);
rivers.attr("opacity", null).attr("fill", "#5d97bb").attr("filter", null);
ruler.attr("opacity", null).attr("filter", null);
- roads.attr("opacity", 0.9).attr("stroke", "#d06324").attr("stroke-width", 0.7).attr("stroke-dasharray", "2").attr("stroke-linecap", "butt").attr("filter", null).attr("mask", null);
- trails.attr("opacity", 0.9).attr("stroke", "#d06324").attr("stroke-width", 0.25).attr("stroke-dasharray", ".8 1.6").attr("stroke-linecap", "butt").attr("filter", null).attr("mask", null);
- searoutes.attr("opacity", 0.8).attr("stroke", "#ffffff").attr("stroke-width", 0.45).attr("stroke-dasharray", "1 2").attr("stroke-linecap", "round").attr("filter", null).attr("mask", null);
+ roads
+ .attr("opacity", 0.9)
+ .attr("stroke", "#d06324")
+ .attr("stroke-width", 0.7)
+ .attr("stroke-dasharray", "2")
+ .attr("stroke-linecap", "butt")
+ .attr("filter", null)
+ .attr("mask", null);
+ trails
+ .attr("opacity", 0.9)
+ .attr("stroke", "#d06324")
+ .attr("stroke-width", 0.25)
+ .attr("stroke-dasharray", ".8 1.6")
+ .attr("stroke-linecap", "butt")
+ .attr("filter", null)
+ .attr("mask", null);
+ searoutes
+ .attr("opacity", 0.8)
+ .attr("stroke", "#ffffff")
+ .attr("stroke-width", 0.45)
+ .attr("stroke-dasharray", "1 2")
+ .attr("stroke-linecap", "round")
+ .attr("filter", null)
+ .attr("mask", null);
statesBody.attr("opacity", 0.4).attr("filter", null);
statesHalo.attr("data-width", 10).attr("stroke-width", 10).attr("opacity", 0.4).attr("filter", "blur(5px)");
provs.attr("opacity", 0.7).attr("fill", "#000000").attr("font-family", "Georgia").attr("data-size", 10).attr("font-size", 10).attr("filter", null);
- temperature.attr("opacity", null).attr("fill", "#000000").attr("stroke-width", 1.8).attr("fill-opacity", 0.3).attr("font-size", "8px").attr("stroke-dasharray", null).attr("filter", null).attr("mask", null);
+ temperature
+ .attr("opacity", null)
+ .attr("fill", "#000000")
+ .attr("stroke-width", 1.8)
+ .attr("fill-opacity", 0.3)
+ .attr("font-size", "8px")
+ .attr("stroke-dasharray", null)
+ .attr("filter", null)
+ .attr("mask", null);
texture.attr("opacity", null).attr("filter", null).attr("mask", "url(#land)");
texture.select("#textureImage").attr("x", 0).attr("y", 0);
- zones.attr("opacity", 0.6).attr("stroke", "#333333").attr("stroke-width", 0).attr("stroke-dasharray", null).attr("stroke-linecap", "butt").attr("filter", null).attr("mask", null);
+ zones
+ .attr("opacity", 0.6)
+ .attr("stroke", "#333333")
+ .attr("stroke-width", 0)
+ .attr("stroke-dasharray", null)
+ .attr("stroke-linecap", "butt")
+ .attr("filter", null)
+ .attr("mask", null);
// ocean and svg default style
svg.attr("background-color", "#000000").attr("data-filter", null).attr("filter", null);
@@ -838,24 +913,95 @@ function applyDefaultStyle() {
svg.select("#oceanicPattern").attr("href", "./images/pattern1.png").attr("opacity", 0.2);
// heightmap style
- terrs.attr("opacity", null).attr("filter", null).attr("mask", "url(#land)").attr("stroke", "none").attr("scheme", "bright").attr("terracing", 0).attr("skip", 5).attr("relax", 0).attr("curve", 0);
+ terrs
+ .attr("opacity", null)
+ .attr("filter", null)
+ .attr("mask", "url(#land)")
+ .attr("stroke", "none")
+ .attr("scheme", "bright")
+ .attr("terracing", 0)
+ .attr("skip", 5)
+ .attr("relax", 0)
+ .attr("curve", 0);
// legend
- legend.attr("font-family", "Almendra SC").attr("font-size", 13).attr("data-size", 13).attr("data-x", 99).attr("data-y", 93).attr("data-columns", 8).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("font-size", 13)
+ .attr("data-size", 13)
+ .attr("data-x", 99)
+ .attr("data-y", 93)
+ .attr("data-columns", 8)
+ .attr("stroke-width", 2.5)
+ .attr("stroke", "#812929")
+ .attr("stroke-dasharray", "0 4 10 4")
+ .attr("stroke-linecap", "round");
legend.select("#legendBox").attr("fill", "#ffffff").attr("fill-opacity", 0.8);
const citiesSize = Math.max(rn(8 - regionsInput.value / 20), 3);
- burgLabels.select("#cities").attr("fill", "#3e3e4b").attr("opacity", 1).style("text-shadow", "white 0 0 4px").attr("font-family", "Almendra SC").attr("font-size", citiesSize).attr("data-size", citiesSize);
- burgIcons.select("#cities").attr("opacity", 1).attr("size", 1).attr("stroke-width", 0.24).attr("fill", "#ffffff").attr("stroke", "#3e3e4b").attr("fill-opacity", 0.7).attr("stroke-dasharray", "").attr("stroke-linecap", "butt");
+ burgLabels
+ .select("#cities")
+ .attr("fill", "#3e3e4b")
+ .attr("opacity", 1)
+ .style("text-shadow", "white 0 0 4px")
+ .attr("font-family", "Almendra SC")
+ .attr("font-size", citiesSize)
+ .attr("data-size", citiesSize);
+ burgIcons
+ .select("#cities")
+ .attr("opacity", 1)
+ .attr("size", 1)
+ .attr("stroke-width", 0.24)
+ .attr("fill", "#ffffff")
+ .attr("stroke", "#3e3e4b")
+ .attr("fill-opacity", 0.7)
+ .attr("stroke-dasharray", "")
+ .attr("stroke-linecap", "butt");
anchors.select("#cities").attr("opacity", 1).attr("fill", "#ffffff").attr("stroke", "#3e3e4b").attr("stroke-width", 1.2).attr("size", 2);
- burgLabels.select("#towns").attr("fill", "#3e3e4b").attr("opacity", 1).style("text-shadow", "white 0 0 4px").attr("font-family", "Almendra SC").attr("font-size", 3).attr("data-size", 4);
- burgIcons.select("#towns").attr("opacity", 1).attr("size", 0.5).attr("stroke-width", 0.12).attr("fill", "#ffffff").attr("stroke", "#3e3e4b").attr("fill-opacity", 0.7).attr("stroke-dasharray", "").attr("stroke-linecap", "butt");
+ burgLabels
+ .select("#towns")
+ .attr("fill", "#3e3e4b")
+ .attr("opacity", 1)
+ .style("text-shadow", "white 0 0 4px")
+ .attr("font-family", "Almendra SC")
+ .attr("font-size", 3)
+ .attr("data-size", 4);
+ burgIcons
+ .select("#towns")
+ .attr("opacity", 1)
+ .attr("size", 0.5)
+ .attr("stroke-width", 0.12)
+ .attr("fill", "#ffffff")
+ .attr("stroke", "#3e3e4b")
+ .attr("fill-opacity", 0.7)
+ .attr("stroke-dasharray", "")
+ .attr("stroke-linecap", "butt");
anchors.select("#towns").attr("opacity", 1).attr("fill", "#ffffff").attr("stroke", "#3e3e4b").attr("stroke-width", 1.2).attr("size", 1);
const stateLabelSize = Math.max(rn(24 - regionsInput.value / 6), 6);
- labels.select("#states").attr("fill", "#3e3e4b").attr("opacity", 1).attr("stroke", "#3a3a3a").attr("stroke-width", 0).style("text-shadow", "white 0 0 4px").attr("font-family", "Almendra SC").attr("font-size", stateLabelSize).attr("data-size", stateLabelSize).attr("filter", null);
- labels.select("#addedLabels").attr("fill", "#3e3e4b").attr("opacity", 1).attr("stroke", "#3a3a3a").attr("stroke-width", 0).style("text-shadow", "white 0 0 4px").attr("font-family", "Almendra SC").attr("font-size", 18).attr("data-size", 18).attr("filter", null);
+ labels
+ .select("#states")
+ .attr("fill", "#3e3e4b")
+ .attr("opacity", 1)
+ .attr("stroke", "#3a3a3a")
+ .attr("stroke-width", 0)
+ .style("text-shadow", "white 0 0 4px")
+ .attr("font-family", "Almendra SC")
+ .attr("font-size", stateLabelSize)
+ .attr("data-size", stateLabelSize)
+ .attr("filter", null);
+ labels
+ .select("#addedLabels")
+ .attr("fill", "#3e3e4b")
+ .attr("opacity", 1)
+ .attr("stroke", "#3a3a3a")
+ .attr("stroke-width", 0)
+ .style("text-shadow", "white 0 0 4px")
+ .attr("font-family", "Almendra SC")
+ .attr("font-size", 18)
+ .attr("data-size", 18)
+ .attr("filter", null);
fogging.attr("opacity", 0.98).attr("fill", "#30426f");
emblems.attr("opacity", 0.9).attr("stroke-width", 1).attr("filter", null);
@@ -887,40 +1033,49 @@ function applyStyle(style) {
function changeStylePreset(preset) {
if (customization) return tip("Please exit the customization mode first", false, "error");
- alertMessage.innerHTML = "Are you sure you want to change the style preset? All unsaved style changes will be lost";
- $("#alert").dialog({
- resizable: false,
- title: "Change style preset",
- width: "23em",
- buttons: {
- Change: function () {
- const customPreset = localStorage.getItem(preset);
- if (customPreset) {
- if (JSON.isValid(customPreset)) applyStyle(JSON.parse(customPreset));
- else {
- tip("Cannot parse stored style JSON. Default style applied", false, "error", 5000);
- applyDefaultStyle();
- }
- } else if (defaultStyles[preset]) {
- const style = defaultStyles[preset];
- if (JSON.isValid(style)) applyStyle(JSON.parse(style));
- else tip("Cannot parse style JSON", false, "error", 5000);
- } else applyDefaultStyle();
-
- removeStyleButton.style.display = stylePreset.selectedOptions[0].dataset.system ? "none" : "inline-block";
- updateElements(); // change elements
- selectStyleElement(); // re-select element to trigger values update
- updateMapFilter();
- localStorage.setItem("presetStyle", preset); // save preset to use it onload
- stylePreset.dataset.old = stylePreset.value; // save current value
- $(this).dialog("close");
- },
- Cancel: function () {
- stylePreset.value = stylePreset.dataset.old;
- $(this).dialog("close");
+ if (sessionStorage.getItem("styleChangeWarningShown")) {
+ changeStyle();
+ } else {
+ sessionStorage.setItem("styleChangeWarningShown", true);
+ alertMessage.innerHTML = "Are you sure you want to change the style preset? All unsaved style changes will be lost";
+ $("#alert").dialog({
+ resizable: false,
+ title: "Change style preset",
+ width: "23em",
+ buttons: {
+ Change: function () {
+ changeStyle();
+ $(this).dialog("close");
+ },
+ Cancel: function () {
+ stylePreset.value = stylePreset.dataset.old;
+ $(this).dialog("close");
+ }
}
- }
- });
+ });
+ }
+
+ function changeStyle() {
+ const customPreset = localStorage.getItem(preset);
+ if (customPreset) {
+ if (JSON.isValid(customPreset)) applyStyle(JSON.parse(customPreset));
+ else {
+ tip("Cannot parse stored style JSON. Default style applied", false, "error", 5000);
+ applyDefaultStyle();
+ }
+ } else if (defaultStyles[preset]) {
+ const style = defaultStyles[preset];
+ if (JSON.isValid(style)) applyStyle(JSON.parse(style));
+ else tip("Cannot parse style JSON", false, "error", 5000);
+ } else applyDefaultStyle();
+
+ removeStyleButton.style.display = stylePreset.selectedOptions[0].dataset.system ? "none" : "inline-block";
+ updateElements(); // change elements
+ selectStyleElement(); // re-select element to trigger values update
+ updateMapFilter();
+ localStorage.setItem("presetStyle", preset); // save preset to use it onload
+ stylePreset.dataset.old = stylePreset.value; // save current value
+ }
}
function updateElements() {
@@ -971,8 +1126,9 @@ function addStylePreset() {
position: {my: "center", at: "center", of: "svg"}
});
- const currentStyle = document.getElementById("stylePreset").selectedOptions[0].text;
- document.getElementById("styleSaverName").value = currentStyle;
+ const currentPreset = document.getElementById("stylePreset").selectedOptions[0];
+ const styleName = currentPreset ? currentPreset.text : "custom";
+ document.getElementById("styleSaverName").value = styleName;
styleSaverJSON.value = JSON.stringify(getStyle(), null, 2);
checkName();
@@ -1092,6 +1248,11 @@ function addStylePreset() {
applyOption(stylePreset, preset, styleSaverName.value); // add option
localStorage.setItem("presetStyle", preset); // mark preset as default
localStorage.setItem(preset, styleSaverJSON.value); // save preset
+
+ applyStyle(JSON.parse(styleSaverJSON.value));
+ updateMapFilter();
+ invokeActiveZooming();
+
$("#styleSaver").dialog("close");
removeStyleButton.style.display = "inline-block";
tip("Style preset is saved", false, "success", 4000);
@@ -1121,6 +1282,10 @@ function removeStylePreset() {
localStorage.removeItem(stylePreset.value);
stylePreset.selectedOptions[0].remove();
removeStyleButton.style.display = "none";
+
+ applyDefaultStyle();
+ updateMapFilter();
+ invokeActiveZooming();
}
// GLOBAL FILTERS
diff --git a/modules/ui/tools.js b/modules/ui/tools.js
index 81edffe8..f928c25c 100644
--- a/modules/ui/tools.js
+++ b/modules/ui/tools.js
@@ -1,15 +1,15 @@
-// module to control the Tools options (click to edit, to re-geenerate, tp add)
"use strict";
+// module to control the Tools options (click to edit, to re-geenerate, tp add)
toolsContent.addEventListener("click", function (event) {
if (customization) {
tip("Please exit the customization mode first", false, "warning");
return;
}
- if (event.target.tagName !== "BUTTON") return;
+ if (!["BUTTON", "I"].includes(event.target.tagName)) return;
const button = event.target.id;
- // Click to open Editor buttons
+ // click on open Editor buttons
if (button === "editHeightmapButton") editHeightmap();
else if (button === "editBiomesButton") editBiomes();
else if (button === "editStatesButton") editStates();
@@ -25,9 +25,10 @@ toolsContent.addEventListener("click", function (event) {
else if (button === "overviewBurgsButton") overviewBurgs();
else if (button === "overviewRiversButton") overviewRivers();
else if (button === "overviewMilitaryButton") overviewMilitary();
+ else if (button === "overviewMarkersButton") overviewMarkers();
else if (button === "overviewCellsButton") viewCellDetails();
- // Click to Regenerate buttons
+ // click on Regenerate buttons
if (event.target.parentNode.id === "regenerateFeature") {
if (sessionStorage.getItem("regenerateFeatureDontAsk")) {
processFeatureRegeneration(event, button);
@@ -49,7 +50,9 @@ toolsContent.addEventListener("click", function (event) {
},
open: function () {
const pane = $(this).dialog("widget").find(".ui-dialog-buttonpane");
- $('
').prependTo(pane);
+ $(
+ ''
+ ).prependTo(pane);
},
close: function () {
const box = $(this).dialog("widget").find(".checkbox")[0];
@@ -60,7 +63,10 @@ toolsContent.addEventListener("click", function (event) {
});
}
- // Click to Add buttons
+ // click on Configure regenerate buttons
+ if (button === "configRegenerateMarkers") configMarkersGeneration();
+
+ // click on Add buttons
if (button === "addLabel") toggleAddLabel();
else if (button === "addBurgTool") toggleAddBurg();
else if (button === "addRiver") toggleAddRiver();
@@ -88,7 +94,7 @@ function processFeatureRegeneration(event, button) {
else if (button === "regenerateCultures") regenerateCultures();
else if (button === "regenerateMilitary") regenerateMilitary();
else if (button === "regenerateIce") regenerateIce();
- else if (button === "regenerateMarkers") regenerateMarkers(event);
+ else if (button === "regenerateMarkers") regenerateMarkers();
else if (button === "regenerateZones") regenerateZones(event);
}
@@ -262,7 +268,8 @@ function regenerateBurgs() {
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) ** 0.8) + states.length : +manorsInput.value + states.length;
+ 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
@@ -413,23 +420,11 @@ function regenerateIce() {
drawIce();
}
-function regenerateMarkers(event) {
- 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();
-
- addMarkers(number);
- if (!layerIsOn("toggleMarkers")) toggleMarkers();
- }
+function regenerateMarkers() {
+ Markers.regenerate();
+ turnButtonOn("toggleMarkers");
+ drawMarkers();
+ if (document.getElementById("markersOverviewRefresh").offsetParent) markersOverviewRefresh.click();
}
function regenerateZones(event) {
@@ -474,8 +469,23 @@ function addLabelOnClick() {
const name = Names.getCulture(culture);
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("font-size", 18).attr("data-size", 18).attr("filter", null);
+ // use most recently selected label group
+ const lastSelected = labelGroupSelect.value;
+ const groupId = ["", "states", "burgLabels"].includes(lastSelected) ? "#addedLabels" : "#" + lastSelected;
+
+ let group = labels.select(groupId);
+ 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("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;
@@ -674,7 +684,7 @@ function addRouteOnClick() {
}
function toggleAddMarker() {
- const pressed = document.getElementById("addMarker").classList.contains("pressed");
+ const pressed = document.getElementById("addMarker")?.classList.contains("pressed");
if (pressed) {
unpressClickToAddButton();
return;
@@ -682,45 +692,115 @@ function toggleAddMarker() {
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
addMarker.classList.add("pressed");
- closeDialogs(".stable");
+ markersAddFromOverview.classList.add("pressed");
+
viewbox.style("cursor", "crosshair").on("click", addMarkerOnClick);
tip("Click on map to add a marker. Hold Shift to add multiple", true);
if (!layerIsOn("toggleMarkers")) toggleMarkers();
}
function addMarkerOnClick() {
+ const {markers} = pack;
const point = d3.mouse(this);
- const x = rn(point[0], 2),
- y = rn(point[1], 2);
- const id = getNextId("markerElement");
+ const x = rn(point[0], 2);
+ const y = rn(point[1], 2);
+ const i = markers.length ? last(markers).i + 1 : 0;
- const selected = markerSelectGroup.value;
- 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;
+ const isMarkerSelected = markers.length && elSelected?.node()?.parentElement?.id === "markers";
+ const selectedMarker = isMarkerSelected ? markers.find(marker => marker.i === +elSelected.attr("id").slice(6)) : null;
+ const baseMarker = selectedMarker || {icon: "❓"};
+ const marker = {...baseMarker, i, x, y};
- 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.push(marker);
+ const markersElement = document.getElementById("markers");
+ const rescale = +markersElement.getAttribute("rescale");
+ markersElement.insertAdjacentHTML("beforeend", drawMarker(marker, rescale));
- if (d3.event.shiftKey === false) unpressClickToAddButton();
+ if (d3.event.shiftKey === false) {
+ document.getElementById("markerAdd").classList.remove("pressed");
+ document.getElementById("markersAddFromOverview").classList.remove("pressed");
+ unpressClickToAddButton();
+ }
+}
+
+function configMarkersGeneration() {
+ drawConfigTable();
+
+ function drawConfigTable() {
+ const {markers} = pack;
+ const config = Markers.getConfig();
+ const headers = `
+ | Type |
+ Icon |
+ Multiplier |
+ Number |
+
`;
+ const lines = config.map(({type, icon, multiplier}, index) => {
+ const inputId = `markerIconInput${index}`;
+ return `
+ |
+
+
+
+ |
+ |
+ ${markers.filter(marker => marker.type === type).length} |
+
`;
+ });
+ const table = `${headers}${lines.join("")}
`;
+ alertMessage.innerHTML = table;
+
+ alertMessage.querySelectorAll("i").forEach(selectIconButton => {
+ selectIconButton.addEventListener("click", function () {
+ const input = this.previousElementSibling;
+ selectIcon(input.value, icon => (input.value = icon));
+ });
+ });
+ }
+
+ const applyChanges = () => {
+ const rows = alertMessage.querySelectorAll("tbody > tr");
+ const rowsData = Array.from(rows).map(row => {
+ const inputs = row.querySelectorAll("input");
+ return {
+ type: inputs[0].value,
+ icon: inputs[1].value,
+ multiplier: parseFloat(inputs[2].value)
+ };
+ });
+
+ const config = Markers.getConfig();
+ const newConfig = config.map((markerType, index) => {
+ const {type, icon, multiplier} = rowsData[index];
+ return {...markerType, type, icon, multiplier};
+ });
+
+ Markers.setConfig(newConfig);
+ };
+
+ $("#alert").dialog({
+ resizable: false,
+ title: "Markers generation settings",
+ position: {my: "left top", at: "left+10 top+10", of: "svg", collision: "fit"},
+ buttons: {
+ Regenerate: () => {
+ applyChanges();
+ regenerateMarkers();
+ drawConfigTable();
+ },
+ Close: function () {
+ $(this).dialog("close");
+ }
+ },
+ open: function () {
+ const buttons = $(this).dialog("widget").find(".ui-dialog-buttonset > button");
+ buttons[0].addEventListener("mousemove", () => tip("Apply changes and regenerate markers"));
+ buttons[1].addEventListener("mousemove", () => tip("Close the window"));
+ },
+ close: function () {
+ $(this).dialog("destroy");
+ }
+ });
}
function viewCellDetails() {
diff --git a/modules/ui/units-editor.js b/modules/ui/units-editor.js
index 34725e33..3131f64c 100644
--- a/modules/ui/units-editor.js
+++ b/modules/ui/units-editor.js
@@ -31,6 +31,8 @@ function editUnits() {
document.getElementById("populationRateInput").addEventListener("change", changePopulationRate);
document.getElementById("urbanizationOutput").addEventListener("input", changeUrbanizationRate);
document.getElementById("urbanizationInput").addEventListener("change", changeUrbanizationRate);
+ document.getElementById("urbanDensityOutput").addEventListener("input", changeUrbanDensity);
+ document.getElementById("urbanDensityInput").addEventListener("change", changeUrbanDensity);
document.getElementById("addLinearRuler").addEventListener("click", addRuler);
document.getElementById("addOpisometer").addEventListener("click", toggleOpisometerMode);
@@ -94,6 +96,10 @@ function editUnits() {
urbanization = +this.value;
}
+ function changeUrbanDensity() {
+ urbanDensity = +this.value;
+ }
+
function restoreDefaultUnits() {
// distanceScale
distanceScale = 3;
@@ -137,8 +143,10 @@ function editUnits() {
// population
populationRate = populationRateOutput.value = populationRateInput.value = 1000;
urbanization = urbanizationOutput.value = urbanizationInput.value = 1;
+ urbanDensity = urbanDensityOutput.value = urbanDensityInput.value = 10;
localStorage.removeItem("populationRate");
localStorage.removeItem("urbanization");
+ localStorage.removeItem("urbanDensity");
}
function addRuler() {
diff --git a/modules/ui/world-configurator.js b/modules/ui/world-configurator.js
index aa83eb82..22dccacb 100644
--- a/modules/ui/world-configurator.js
+++ b/modules/ui/world-configurator.js
@@ -18,6 +18,9 @@ function editWorld() {
buttons[2].addEventListener("mousemove", () => tip("Click to set map size to cover the Tropical latitudes"));
buttons[3].addEventListener("mousemove", () => tip("Click to set map size to cover the Southern latitudes"));
buttons[4].addEventListener("mousemove", () => tip("Click to restore default wind directions"));
+ },
+ close: function () {
+ $(this).dialog("destroy");
}
});
diff --git a/modules/utils.js b/modules/utils.js
deleted file mode 100644
index 84389a3d..00000000
--- a/modules/utils.js
+++ /dev/null
@@ -1,821 +0,0 @@
-// FMG helper functions
-"use strict";
-
-// add boundary points to pseudo-clip voronoi cells
-function getBoundaryPoints(width, height, spacing) {
- const offset = rn(-1 * spacing);
- const bSpacing = spacing * 2;
- const w = width - offset * 2;
- const h = height - offset * 2;
- const numberX = Math.ceil(w / bSpacing) - 1;
- const numberY = Math.ceil(h / bSpacing) - 1;
- let points = [];
- for (let i = 0.5; i < numberX; i++) {
- let x = Math.ceil((w * i) / numberX + offset);
- points.push([x, offset], [x, h + offset]);
- }
- for (let i = 0.5; i < numberY; i++) {
- let y = Math.ceil((h * i) / numberY + offset);
- points.push([offset, y], [w + offset, y]);
- }
- return points;
-}
-
-// get points on a regular square grid and jitter them a bit
-function getJitteredGrid(width, height, spacing) {
- const radius = spacing / 2; // square radius
- const jittering = radius * 0.9; // max deviation
- const jitter = () => Math.random() * 2 * jittering - jittering;
-
- let points = [];
- for (let y = radius; y < height; y += spacing) {
- for (let x = radius; x < width; x += spacing) {
- const xj = Math.min(rn(x + jitter(), 2), width);
- const yj = Math.min(rn(y + jitter(), 2), height);
- points.push([xj, yj]);
- }
- }
- return points;
-}
-
-// return cell index on a regular square grid
-function findGridCell(x, y) {
- return Math.floor(Math.min(y / grid.spacing, grid.cellsY - 1)) * grid.cellsX + Math.floor(Math.min(x / grid.spacing, grid.cellsX - 1));
-}
-
-// return array of cell indexes in radius on a regular square grid
-function findGridAll(x, y, radius) {
- const c = grid.cells.c;
- let r = Math.floor(radius / grid.spacing);
- let found = [findGridCell(x, y)];
- if (!r || radius === 1) return found;
- if (r > 0) found = found.concat(c[found[0]]);
- if (r > 1) {
- let frontier = c[found[0]];
- while (r > 1) {
- let cycle = frontier.slice();
- frontier = [];
- cycle.forEach(function (s) {
- c[s].forEach(function (e) {
- if (found.indexOf(e) !== -1) return;
- found.push(e);
- frontier.push(e);
- });
- });
- r--;
- }
- }
-
- return found;
-}
-
-// return closest pack points quadtree datum
-function find(x, y, radius = Infinity) {
- return pack.cells.q.find(x, y, radius);
-}
-
-// return closest cell index
-function findCell(x, y, radius = Infinity) {
- const found = pack.cells.q.find(x, y, radius);
- return found ? found[2] : undefined;
-}
-
-// return array of cell indexes in radius
-function findAll(x, y, radius) {
- const found = pack.cells.q.findAll(x, y, radius);
- return found.map(r => r[2]);
-}
-
-// get polygon points for packed cells knowing cell id
-function getPackPolygon(i) {
- return pack.cells.v[i].map(v => pack.vertices.p[v]);
-}
-
-// get polygon points for initial cells knowing cell id
-function getGridPolygon(i) {
- return grid.cells.v[i].map(v => grid.vertices.p[v]);
-}
-
-// mbostock's poissonDiscSampler
-function* poissonDiscSampler(x0, y0, x1, y1, r, k = 3) {
- if (!(x1 >= x0) || !(y1 >= y0) || !(r > 0)) throw new Error();
-
- const width = x1 - x0;
- const height = y1 - y0;
- const r2 = r * r;
- const r2_3 = 3 * r2;
- const cellSize = r * Math.SQRT1_2;
- const gridWidth = Math.ceil(width / cellSize);
- const gridHeight = Math.ceil(height / cellSize);
- const grid = new Array(gridWidth * gridHeight);
- const queue = [];
-
- function far(x, y) {
- const i = (x / cellSize) | 0;
- const j = (y / cellSize) | 0;
- const i0 = Math.max(i - 2, 0);
- const j0 = Math.max(j - 2, 0);
- const i1 = Math.min(i + 3, gridWidth);
- const j1 = Math.min(j + 3, gridHeight);
- for (let j = j0; j < j1; ++j) {
- const o = j * gridWidth;
- for (let i = i0; i < i1; ++i) {
- const s = grid[o + i];
- if (s) {
- const dx = s[0] - x;
- const dy = s[1] - y;
- if (dx * dx + dy * dy < r2) return false;
- }
- }
- }
- return true;
- }
-
- function sample(x, y) {
- queue.push((grid[gridWidth * ((y / cellSize) | 0) + ((x / cellSize) | 0)] = [x, y]));
- return [x + x0, y + y0];
- }
-
- yield sample(width / 2, height / 2);
-
- pick: while (queue.length) {
- const i = (Math.random() * queue.length) | 0;
- const parent = queue[i];
-
- for (let j = 0; j < k; ++j) {
- const a = 2 * Math.PI * Math.random();
- const r = Math.sqrt(Math.random() * r2_3 + r2);
- const x = parent[0] + r * Math.cos(a);
- const y = parent[1] + r * Math.sin(a);
- if (0 <= x && x < width && 0 <= y && y < height && far(x, y)) {
- yield sample(x, y);
- continue pick;
- }
- }
-
- const r = queue.pop();
- if (i < queue.length) queue[i] = r;
- }
-}
-
-// filter land cells
-function isLand(i) {
- return pack.cells.h[i] >= 20;
-}
-
-// filter water cells
-function isWater(i) {
- return pack.cells.h[i] < 20;
-}
-
-// convert RGB color string to HEX without #
-function toHEX(rgb) {
- if (rgb.charAt(0) === "#") {
- return rgb;
- }
- rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
- return rgb && rgb.length === 4 ? "#" + ("0" + parseInt(rgb[1], 10).toString(16)).slice(-2) + ("0" + parseInt(rgb[2], 10).toString(16)).slice(-2) + ("0" + parseInt(rgb[3], 10).toString(16)).slice(-2) : "";
-}
-
-// return array of standard shuffled colors
-function getColors(number) {
- const c12 = ["#dababf", "#fb8072", "#80b1d3", "#fdb462", "#b3de69", "#fccde5", "#c6b9c1", "#bc80bd", "#ccebc5", "#ffed6f", "#8dd3c7", "#eb8de7"];
- const cRB = d3.scaleSequential(d3.interpolateRainbow);
- const colors = d3.shuffle(d3.range(number).map(i => (i < 12 ? c12[i] : d3.color(cRB((i - 12) / (number - 12))).hex())));
- return colors;
-}
-
-function getRandomColor() {
- return d3.color(d3.scaleSequential(d3.interpolateRainbow)(Math.random())).hex();
-}
-
-// mix a color with a random color
-function getMixedColor(color, mix = 0.2, bright = 0.3) {
- const c = color && color[0] === "#" ? color : getRandomColor(); // if provided color is not hex (e.g. harching), generate random one
- return d3.color(d3.interpolate(c, getRandomColor())(mix)).brighter(bright).hex();
-}
-
-// conver temperature from °C to other scales
-function convertTemperature(c) {
- switch (temperatureScale.value) {
- case "°C":
- return c + "°C";
- case "°F":
- return rn((c * 9) / 5 + 32) + "°F";
- case "K":
- return rn(c + 273.15) + "K";
- case "°R":
- return rn(((c + 273.15) * 9) / 5) + "°R";
- case "°De":
- return rn(((100 - c) * 3) / 2) + "°De";
- case "°N":
- return rn((c * 33) / 100) + "°N";
- case "°Ré":
- return rn((c * 4) / 5) + "°Ré";
- case "°Rø":
- return rn((c * 21) / 40 + 7.5) + "°Rø";
- default:
- return c + "°C";
- }
-}
-
-// random number in a range
-function rand(min, max) {
- if (min === undefined && max === undefined) return Math.random();
- if (max === undefined) {
- max = min;
- min = 0;
- }
- return Math.floor(Math.random() * (max - min + 1)) + min;
-}
-
-// probability shorthand
-function P(probability) {
- if (probability >= 1) return true;
- if (probability <= 0) return false;
- return Math.random() < probability;
-}
-
-function each(n) {
- return i => i % n === 0;
-}
-
-// random number (normal or gaussian distribution)
-function gauss(expected = 100, deviation = 30, min = 0, max = 300, round = 0) {
- return rn(Math.max(Math.min(d3.randomNormal(expected, deviation)(), max), min), round);
-}
-
-// probability shorthand for floats
-function Pint(float) {
- return ~~float + +P(float % 1);
-}
-
-// round value to d decimals
-function rn(v, d = 0) {
- const m = Math.pow(10, d);
- return Math.round(v * m) / m;
-}
-
-// round string to d decimals
-function round(s, d = 1) {
- return s.replace(/[\d\.-][\d\.e-]*/g, function (n) {
- return rn(n, d);
- });
-}
-
-// corvent number to short string with SI postfix
-function si(n) {
- if (n >= 1e9) return rn(n / 1e9, 1) + "B";
- if (n >= 1e8) return rn(n / 1e6) + "M";
- if (n >= 1e6) return rn(n / 1e6, 1) + "M";
- if (n >= 1e4) return rn(n / 1e3) + "K";
- if (n >= 1e3) return rn(n / 1e3, 1) + "K";
- return rn(n);
-}
-
-// getInteger number from user input data
-function getInteger(value) {
- const metric = value.slice(-1);
- if (metric === "K") return parseInt(value.slice(0, -1) * 1e3);
- if (metric === "M") return parseInt(value.slice(0, -1) * 1e6);
- if (metric === "B") return parseInt(value.slice(0, -1) * 1e9);
- return parseInt(value);
-}
-
-// remove parent element (usually if child is clicked)
-function removeParent() {
- this.parentNode.parentNode.removeChild(this.parentNode);
-}
-
-// return string with 1st char capitalized
-function capitalize(string) {
- return string.charAt(0).toUpperCase() + string.slice(1);
-}
-
-// transform string to array [translateX,translateY,rotateDeg,rotateX,rotateY,scale]
-function parseTransform(string) {
- if (!string) {
- return [0, 0, 0, 0, 0, 1];
- }
- const a = string
- .replace(/[a-z()]/g, "")
- .replace(/[ ]/g, ",")
- .split(",");
- return [a[0] || 0, a[1] || 0, a[2] || 0, a[3] || 0, a[4] || 0, a[5] || 1];
-}
-
-// findAll d3.quandtree search from https://bl.ocks.org/lwthatcher/b41479725e0ff2277c7ac90df2de2b5e
-void (function addFindAll() {
- const Quad = function (node, x0, y0, x1, y1) {
- this.node = node;
- this.x0 = x0;
- this.y0 = y0;
- this.x1 = x1;
- this.y1 = y1;
- };
-
- const tree_filter = function (x, y, radius) {
- var t = {x, y, x0: this._x0, y0: this._y0, x3: this._x1, y3: this._y1, quads: [], node: this._root};
- if (t.node) {
- t.quads.push(new Quad(t.node, t.x0, t.y0, t.x3, t.y3));
- }
- radiusSearchInit(t, radius);
-
- var i = 0;
- while ((t.q = t.quads.pop())) {
- i++;
-
- // Stop searching if this quadrant can’t contain a closer node.
- if (!(t.node = t.q.node) || (t.x1 = t.q.x0) > t.x3 || (t.y1 = t.q.y0) > t.y3 || (t.x2 = t.q.x1) < t.x0 || (t.y2 = t.q.y1) < t.y0) continue;
-
- // Bisect the current quadrant.
- if (t.node.length) {
- t.node.explored = true;
- var xm = (t.x1 + t.x2) / 2,
- ym = (t.y1 + t.y2) / 2;
-
- t.quads.push(new Quad(t.node[3], xm, ym, t.x2, t.y2), new Quad(t.node[2], t.x1, ym, xm, t.y2), new Quad(t.node[1], xm, t.y1, t.x2, ym), new Quad(t.node[0], t.x1, t.y1, xm, ym));
-
- // Visit the closest quadrant first.
- if ((t.i = ((y >= ym) << 1) | (x >= xm))) {
- t.q = t.quads[t.quads.length - 1];
- t.quads[t.quads.length - 1] = t.quads[t.quads.length - 1 - t.i];
- t.quads[t.quads.length - 1 - t.i] = t.q;
- }
- }
-
- // Visit this point. (Visiting coincident points isn’t necessary!)
- else {
- var dx = x - +this._x.call(null, t.node.data),
- dy = y - +this._y.call(null, t.node.data),
- d2 = dx * dx + dy * dy;
- radiusSearchVisit(t, d2);
- }
- }
- return t.result;
- };
- d3.quadtree.prototype.findAll = tree_filter;
-
- var radiusSearchInit = function (t, radius) {
- t.result = [];
- (t.x0 = t.x - radius), (t.y0 = t.y - radius);
- (t.x3 = t.x + radius), (t.y3 = t.y + radius);
- t.radius = radius * radius;
- };
-
- var radiusSearchVisit = function (t, d2) {
- t.node.data.scanned = true;
- if (d2 < t.radius) {
- do {
- t.result.push(t.node.data);
- t.node.data.selected = true;
- } while ((t.node = t.node.next));
- }
- };
-})();
-
-// get segment of any point on polyline
-function getSegmentId(points, point, step = 10) {
- if (points.length === 2) return 1;
- const d2 = (p1, p2) => (p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2;
-
- let minSegment = 1;
- let minDist = Infinity;
-
- for (let i = 0; i < points.length - 1; i++) {
- const p1 = points[i];
- const p2 = points[i + 1];
-
- const length = Math.sqrt(d2(p1, p2));
- const segments = Math.ceil(length / step);
- const dx = (p2[0] - p1[0]) / segments;
- const dy = (p2[1] - p1[1]) / segments;
-
- for (let s = 0; s < segments; s++) {
- const x = p1[0] + s * dx;
- const y = p1[1] + s * dy;
- const dist2 = d2(point, [x, y]);
-
- if (dist2 >= minDist) continue;
- minDist = dist2;
- minSegment = i + 1;
- }
- }
-
- return minSegment;
-}
-
-// normalization function
-function normalize(val, min, max) {
- return Math.min(Math.max((val - min) / (max - min), 0), 1);
-}
-
-// return a random integer from min to max biased towards one end based on exponent distribution (the bigger ex the higher bias towards min)
-// from https://gamedev.stackexchange.com/a/116875
-function biased(min, max, ex) {
- return Math.round(min + (max - min) * Math.pow(Math.random(), ex));
-}
-
-// return array of values common for both array a and array b
-function common(a, b) {
- const setB = new Set(b);
- return [...new Set(a)].filter(a => setB.has(a));
-}
-
-// clip polygon by graph bbox
-function clipPoly(points, secure = 0) {
- return polygonclip(points, [0, 0, graphWidth, graphHeight], secure);
-}
-
-// check if char is vowel or can serve as vowel
-function vowel(c) {
- return `aeiouyɑ'əøɛœæɶɒɨɪɔɐʊɤɯаоиеёэыуюяàèìòùỳẁȁȅȉȍȕáéíóúýẃőűâêîôûŷŵäëïöüÿẅãẽĩõũỹąęįǫųāēīōūȳăĕĭŏŭǎěǐǒǔȧėȯẏẇạẹịọụỵẉḛḭṵṳ`.includes(c);
-}
-
-// remove vowels from the end of the string
-function trimVowels(string) {
- while (string.length > 3 && vowel(last(string))) {
- string = string.slice(0, -1);
- }
- return string;
-}
-
-// get adjective form from noun
-function getAdjective(string) {
- // special cases for some suffixes
- if (string.length > 8 && string.slice(-6) === "orszag") return string.slice(0, -6);
- if (string.length > 6 && string.slice(-4) === "stan") return string.slice(0, -4);
- if (P(0.5) && string.slice(-4) === "land") return string + "ic";
- if (string.slice(-4) === " Guo") string = string.slice(0, -4);
-
- // don't change is name ends on suffix
- if (string.slice(-2) === "an") return string;
- if (string.slice(-3) === "ese") return string;
- if (string.slice(-1) === "i") return string;
-
- const end = string.slice(-1); // last letter of string
- if (end === "a") return (string += "n");
- if (end === "o") return (string = trimVowels(string) + "an");
- if (vowel(end) || end === "c") return (string += "an"); // ceiuy
- if (end === "m" || end === "n") return (string += "ese");
- if (end === "q") return (string += "i");
- return trimVowels(string) + "ian";
-}
-
-// get ordinal out of integer: 1 => 1st
-const nth = n => n + (["st", "nd", "rd"][((((n + 90) % 100) - 10) % 10) - 1] || "th");
-
-// get two-letters code (abbreviation) from string
-function abbreviate(name, restricted = []) {
- const parsed = name.replace("Old ", "O ").replace(/[()]/g, ""); // remove Old prefix and parentheses
- const words = parsed.split(" ");
- const letters = words.join("");
-
- let code = words.length === 2 ? words[0][0] + words[1][0] : letters.slice(0, 2);
- for (let i = 1; i < letters.length - 1 && restricted.includes(code); i++) {
- code = letters[0] + letters[i].toUpperCase();
- }
- return code;
-}
-
-// conjunct array: [A,B,C] => "A, B and C"
-function list(array) {
- if (!Intl.ListFormat) return array.join(", ");
- const conjunction = new Intl.ListFormat(window.lang || "en", {style: "long", type: "conjunction"});
- return conjunction.format(array);
-}
-
-// split string into 2 almost equal parts not breaking words
-function splitInTwo(str) {
- const half = str.length / 2;
- const ar = str.split(" ");
- if (ar.length < 2) return ar; // only one word
- let first = "",
- last = "",
- middle = "",
- rest = "";
-
- ar.forEach((w, d) => {
- if (d + 1 !== ar.length) w += " ";
- rest += w;
- if (!first || rest.length < half) first += w;
- else if (!middle) middle = w;
- else last += w;
- });
-
- if (!last) return [first, middle];
- if (first.length < last.length) return [first + middle, last];
- return [first, middle + last];
-}
-
-// return the last element of array
-function last(array) {
- return array[array.length - 1];
-}
-
-// return random value from the array
-function ra(array) {
- return array[Math.floor(Math.random() * array.length)];
-}
-
-// return random value from weighted array {"key1":weight1, "key2":weight2}
-function rw(object) {
- const array = [];
- for (const key in object) {
- for (let i = 0; i < object[key]; i++) {
- array.push(key);
- }
- }
- return array[Math.floor(Math.random() * array.length)];
-}
-
-// return value in range [0, 100] (height range)
-function lim(v) {
- return Math.max(Math.min(v, 100), 0);
-}
-
-// get number from string in format "1-3" or "2" or "0.5"
-function getNumberInRange(r) {
- if (typeof r !== "string") {
- ERROR && console.error("The value should be a string", r);
- return 0;
- }
- if (!isNaN(+r)) return ~~r + +P(r - ~~r);
- const sign = r[0] === "-" ? -1 : 1;
- if (isNaN(+r[0])) r = r.slice(1);
- const range = r.includes("-") ? r.split("-") : null;
- if (!range) {
- ERROR && console.error("Cannot parse the number. Check the format", r);
- return 0;
- }
- const count = rand(range[0] * sign, +range[1]);
- if (isNaN(count) || count < 0) {
- ERROR && console.error("Cannot parse number. Check the format", r);
- return 0;
- }
- return count;
-}
-
-// return center point of common edge of 2 pack cells
-function getMiddlePoint(cell1, cell2) {
- const {cells, vertices} = pack;
-
- const commonVertices = cells.v[cell1].filter(vertex => vertices.c[vertex].some(cell => cell === cell2));
- const [x1, y1] = vertices.p[commonVertices[0]];
- const [x2, y2] = vertices.p[commonVertices[1]];
-
- const x = (x1 + x2) / 2;
- const y = (y1 + y2) / 2;
-
- return [x, y];
-}
-
-// helper function non-used for the generation
-function drawCellsValue(data) {
- debug.selectAll("text").remove();
- debug
- .selectAll("text")
- .data(data)
- .enter()
- .append("text")
- .attr("x", (d, i) => pack.cells.p[i][0])
- .attr("y", (d, i) => pack.cells.p[i][1])
- .text(d => d);
-}
-
-// helper function non-used for the generation
-function drawPolygons(data) {
- const max = d3.max(data),
- min = d3.min(data),
- scheme = getColorScheme();
- data = data.map(d => 1 - normalize(d, min, max));
-
- debug.selectAll("polygon").remove();
- debug
- .selectAll("polygon")
- .data(data)
- .enter()
- .append("polygon")
- .attr("points", (d, i) => getPackPolygon(i))
- .attr("fill", d => scheme(d))
- .attr("stroke", d => scheme(d));
-}
-
-// polyfill for composedPath
-function getComposedPath(node) {
- let parent;
- if (node.parentNode) parent = node.parentNode;
- else if (node.host) parent = node.host;
- else if (node.defaultView) parent = node.defaultView;
- if (parent !== undefined) return [node].concat(getComposedPath(parent));
- return [node];
-}
-
-// polyfill for replaceAll
-if (!String.prototype.replaceAll) {
- String.prototype.replaceAll = function (str, newStr) {
- if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") return this.replace(str, newStr);
- return this.replace(new RegExp(str, "g"), newStr);
- };
-}
-
-// get next unused id
-function getNextId(core, i = 1) {
- while (document.getElementById(core + i)) i++;
- return core + i;
-}
-
-function debounce(func, ms) {
- let isCooldown = false;
-
- return function () {
- if (isCooldown) return;
- func.apply(this, arguments);
- isCooldown = true;
- setTimeout(() => (isCooldown = false), ms);
- };
-}
-
-function throttle(func, ms) {
- let isThrottled = false;
- let savedArgs;
- let savedThis;
-
- function wrapper() {
- if (isThrottled) {
- savedArgs = arguments;
- savedThis = this;
- return;
- }
-
- func.apply(this, arguments);
- isThrottled = true;
-
- setTimeout(function () {
- isThrottled = false;
- if (savedArgs) {
- wrapper.apply(savedThis, savedArgs);
- savedArgs = savedThis = null;
- }
- }, ms);
- }
-
- return wrapper;
-}
-
-// parse error to get the readable string in Chrome and Firefox
-function parseError(error) {
- const isFirefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
- const errorString = isFirefox ? error.toString() + " " + error.stack : error.stack;
- const regex = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gi;
- const errorNoURL = errorString.replace(regex, url => "" + last(url.split("/")) + "");
- const errorParsed = errorNoURL.replace(/at /gi, "
at ");
- return errorParsed;
-}
-
-// polyfills
-if (Array.prototype.flat === undefined) {
- Array.prototype.flat = function () {
- return this.reduce((acc, val) => (Array.isArray(val) ? acc.concat(val.flat()) : acc.concat(val)), []);
- };
-}
-
-// check if string is a valid for JSON parse
-JSON.isValid = str => {
- try {
- JSON.parse(str);
- } catch (e) {
- return false;
- }
- return true;
-};
-
-function getBase64(url, callback) {
- const xhr = new XMLHttpRequest();
- xhr.onload = function () {
- const reader = new FileReader();
- reader.onloadend = function () {
- callback(reader.result);
- };
- reader.readAsDataURL(xhr.response);
- };
- xhr.open("GET", url);
- xhr.responseType = "blob";
- xhr.send();
-}
-
-function getDefaultTexture() {
- return "data:image/jpeg;base64,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gkkkoosmbdWhIFoIESRaLaFus5yAJJaEzFSTWtFICyS1ItFIUJFCQRAUEAhZJbYgqxKsEkSqtFooFtEkkgTOcxakULbVa1RJC2ySSAASQatkkJJbbVurRQUzIpCSW0hRbSAW0okhEKQqSFoM5zJne+kKBSJISLaAAtqJIq226FottkmcYULdSZRbVotskkzbqrEBC21JAAhaQ1rcmZIpBJASLbaQoBAERKsKKRAUiABCkSS6skqgKkBIFBAW20W0FJJJIJMohaQttttEkkmtaARmZAFoIEBRaLRRJAABakC2yQW0W0ZzkCkCAItuM4LrWqRLbItSRIiVbq0gLRSILaVIt1bJMyBIpES3SS60LbJAAABSBCgCl1oSZEgSQiLqlIhRSCQQpC0UiAIUEklUAQIWkQFpJAIlVaQttW0KCSQmcwiFIW222kkkk1rYEkkltkgAtpJJbQC2pBBbqSW2SAAEka1SgW3OcgUEAKDGcyTp02iSKBQkFtFtki2iipBEtqIC2yS2kEkkEtqFt1bKqSAAUgAABS6sVnIEBJm3RAUgKAEkQtqSFAkloCZlpaUEAEkSFWggkUAiVVtttpVJJIkzMoWktpbaJIC2gBJBJFUBRJLRQBSIi2rRJKQtSEiraEKt1nORQBRZIkVImc73tUggLakgq22BABbSyZFFpZICAokyLaJJbS61qkJIALRURAABQsS2iSAAAJIEKLUgUCpBLbIIgi0oIAJJVAEJISrJBbYWipFtttoEkkzMwXVkUWi2ySkLbnOda0WTJJFtBBItoAICpFFtIiVQqSICii0WTIFWABAJGc53vdoBCki1JFtAAFotkgIW2yQSZkltmcyBvpsqZka1pbbRJAAS2lkgBCgatVJJBAgLQCFIUASSipCRVVbJCAtISS0hWc5tskt0CTMVbZMlotQIltt0W0JJM5kyttskq3VkAACSa1osmZILaAQoAIAESRVurQQFIgALRRJlCpJbohQCDOc71sKEkqxLbItCRbSAFtokyhbaQIiMzMkFt1rQAqqAAKQW6EkJIAoturSRJCIgLRRSIUAiFSAKKKBJktqQCBChnObaUJJItBALdELM5F1q2haiTIIiJbS23Oc0KoACSAAWgUAEKARGZnWt20JIUUiIUWiyQhSICpJbolUZmdWxbSFIhaQqSW0AAtoskSXVSxABJKRLdCSU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";
-}
-
-function getAbsolutePath(href) {
- if (!href) return "";
- const link = document.createElement("a");
- link.href = href;
- return link.href;
-}
-
-// open URL in a new tab or window
-function openURL(url) {
- window.open(url, "_blank");
-}
-
-// open project wiki-page
-function wiki(page) {
- window.open("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/" + page, "_blank");
-}
-
-// wrap URL into html a element
-function link(URL, description) {
- return `${description}`;
-}
-
-function isCtrlClick(event) {
- // meta key is cmd key on MacOs
- return event.ctrlKey || event.metaKey;
-}
-
-function generateDate(from = 100, to = 1000) {
- return new Date(rand(from, to), rand(12), rand(31)).toLocaleDateString("en", {year: "numeric", month: "long", day: "numeric"});
-}
-
-function getQGIScoordinates(x, y) {
- const cx = mapCoordinates.lonW + (x / graphWidth) * mapCoordinates.lonT;
- const cy = mapCoordinates.latN - (y / graphHeight) * mapCoordinates.latT; // this is inverted in QGIS otherwise
- return [cx, cy];
-}
-
-// prompt replacer (prompt does not work in Electron)
-void (function () {
- const prompt = document.getElementById("prompt");
- const form = prompt.querySelector("#promptForm");
-
- window.prompt = function (promptText = "Please provide an input", options = {default: 1, step: 0.01, min: 0, max: 100}, callback) {
- if (options.default === undefined) {
- ERROR && console.error("Prompt: options object does not have default value defined");
- return;
- }
- const input = prompt.querySelector("#promptInput");
- prompt.querySelector("#promptText").innerHTML = promptText;
- const type = typeof options.default === "number" ? "number" : "text";
- input.type = type;
- if (options.step !== undefined) input.step = options.step;
- if (options.min !== undefined) input.min = options.min;
- if (options.max !== undefined) input.max = options.max;
- input.placeholder = "type a " + type;
- input.value = options.default;
- prompt.style.display = "block";
-
- form.addEventListener(
- "submit",
- event => {
- prompt.style.display = "none";
- const v = type === "number" ? +input.value : input.value;
- event.preventDefault();
- if (callback) callback(v);
- },
- {once: true}
- );
- };
-
- const cancel = prompt.querySelector("#promptCancel");
- cancel.addEventListener("click", () => (prompt.style.display = "none"));
-})();
-
-// indexedDB; ldb object
-!(function () {
- function e(t, o) {
- return n
- ? void (n.transaction("s").objectStore("s").get(t).onsuccess = function (e) {
- var t = (e.target.result && e.target.result.v) || null;
- o(t);
- })
- : void setTimeout(function () {
- e(t, o);
- }, 100);
- }
- var t = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
- if (!t) return void ERROR && console.error("indexedDB not supported");
- var n,
- o = {k: "", v: ""},
- r = t.open("d2", 1);
- (r.onsuccess = function (e) {
- n = this.result;
- }),
- (r.onerror = function (e) {
- ERROR && console.error("indexedDB request error"), INFO && console.log(e);
- }),
- (r.onupgradeneeded = function (e) {
- n = null;
- var t = e.target.result.createObjectStore("s", {keyPath: "k"});
- t.transaction.oncomplete = function (e) {
- n = e.target.db;
- };
- }),
- (window.ldb = {
- get: e,
- set: function (e, t) {
- (o.k = e), (o.v = t), n.transaction("s", "readwrite").objectStore("s").put(o);
- }
- });
-})();
diff --git a/utils/arrayUtils.js b/utils/arrayUtils.js
new file mode 100644
index 00000000..854800ce
--- /dev/null
+++ b/utils/arrayUtils.js
@@ -0,0 +1,17 @@
+"use strict";
+// FMG utils related to arrays
+
+// return the last element of array
+function last(array) {
+ return array[array.length - 1];
+}
+
+// return array of values common for both array a and array b
+function common(a, b) {
+ const setB = new Set(b);
+ return [...new Set(a)].filter(a => setB.has(a));
+}
+
+function unique(array) {
+ return [...new Set(array)];
+}
diff --git a/utils/colorUtils.js b/utils/colorUtils.js
new file mode 100644
index 00000000..3a5c6d24
--- /dev/null
+++ b/utils/colorUtils.js
@@ -0,0 +1,33 @@
+"use strict";
+// FMG utils related to colors
+
+// convert RGB color string to HEX without #
+function toHEX(rgb) {
+ if (rgb.charAt(0) === "#") return rgb;
+
+ rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
+ return rgb && rgb.length === 4
+ ? "#" +
+ ("0" + parseInt(rgb[1], 10).toString(16)).slice(-2) +
+ ("0" + parseInt(rgb[2], 10).toString(16)).slice(-2) +
+ ("0" + parseInt(rgb[3], 10).toString(16)).slice(-2)
+ : "";
+}
+
+// return array of standard shuffled colors
+function getColors(number) {
+ const c12 = ["#dababf", "#fb8072", "#80b1d3", "#fdb462", "#b3de69", "#fccde5", "#c6b9c1", "#bc80bd", "#ccebc5", "#ffed6f", "#8dd3c7", "#eb8de7"];
+ const cRB = d3.scaleSequential(d3.interpolateRainbow);
+ const colors = d3.shuffle(d3.range(number).map(i => (i < 12 ? c12[i] : d3.color(cRB((i - 12) / (number - 12))).hex())));
+ return colors;
+}
+
+function getRandomColor() {
+ return d3.color(d3.scaleSequential(d3.interpolateRainbow)(Math.random())).hex();
+}
+
+// mix a color with a random color
+function getMixedColor(color, mix = 0.2, bright = 0.3) {
+ const c = color && color[0] === "#" ? color : getRandomColor(); // if provided color is not hex (e.g. harching), generate random one
+ return d3.color(d3.interpolate(c, getRandomColor())(mix)).brighter(bright).hex();
+}
diff --git a/utils/commonUtils.js b/utils/commonUtils.js
new file mode 100644
index 00000000..5f6c5742
--- /dev/null
+++ b/utils/commonUtils.js
@@ -0,0 +1,225 @@
+"use strict";
+// FMG helper functions
+
+// clip polygon by graph bbox
+function clipPoly(points, secure = 0) {
+ return polygonclip(points, [0, 0, graphWidth, graphHeight], secure);
+}
+
+// get segment of any point on polyline
+function getSegmentId(points, point, step = 10) {
+ if (points.length === 2) return 1;
+ const d2 = (p1, p2) => (p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2;
+
+ let minSegment = 1;
+ let minDist = Infinity;
+
+ for (let i = 0; i < points.length - 1; i++) {
+ const p1 = points[i];
+ const p2 = points[i + 1];
+
+ const length = Math.sqrt(d2(p1, p2));
+ const segments = Math.ceil(length / step);
+ const dx = (p2[0] - p1[0]) / segments;
+ const dy = (p2[1] - p1[1]) / segments;
+
+ for (let s = 0; s < segments; s++) {
+ const x = p1[0] + s * dx;
+ const y = p1[1] + s * dy;
+ const dist2 = d2(point, [x, y]);
+
+ if (dist2 >= minDist) continue;
+ minDist = dist2;
+ minSegment = i + 1;
+ }
+ }
+
+ return minSegment;
+}
+
+// return center point of common edge of 2 pack cells
+function getMiddlePoint(cell1, cell2) {
+ const {cells, vertices} = pack;
+
+ const commonVertices = cells.v[cell1].filter(vertex => vertices.c[vertex].some(cell => cell === cell2));
+ const [x1, y1] = vertices.p[commonVertices[0]];
+ const [x2, y2] = vertices.p[commonVertices[1]];
+
+ const x = (x1 + x2) / 2;
+ const y = (y1 + y2) / 2;
+
+ return [x, y];
+}
+
+function debounce(func, ms) {
+ let isCooldown = false;
+
+ return function () {
+ if (isCooldown) return;
+ func.apply(this, arguments);
+ isCooldown = true;
+ setTimeout(() => (isCooldown = false), ms);
+ };
+}
+
+function throttle(func, ms) {
+ let isThrottled = false;
+ let savedArgs;
+ let savedThis;
+
+ function wrapper() {
+ if (isThrottled) {
+ savedArgs = arguments;
+ savedThis = this;
+ return;
+ }
+
+ func.apply(this, arguments);
+ isThrottled = true;
+
+ setTimeout(function () {
+ isThrottled = false;
+ if (savedArgs) {
+ wrapper.apply(savedThis, savedArgs);
+ savedArgs = savedThis = null;
+ }
+ }, ms);
+ }
+
+ return wrapper;
+}
+
+// parse error to get the readable string in Chrome and Firefox
+function parseError(error) {
+ const isFirefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
+ const errorString = isFirefox ? error.toString() + " " + error.stack : error.stack;
+ const regex = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gi;
+ const errorNoURL = errorString.replace(regex, url => "" + last(url.split("/")) + "");
+ const errorParsed = errorNoURL.replace(/at /gi, "
at ");
+ return errorParsed;
+}
+
+function getBase64(url, callback) {
+ const xhr = new XMLHttpRequest();
+ xhr.onload = function () {
+ const reader = new FileReader();
+ reader.onloadend = function () {
+ callback(reader.result);
+ };
+ reader.readAsDataURL(xhr.response);
+ };
+ xhr.open("GET", url);
+ xhr.responseType = "blob";
+ xhr.send();
+}
+
+// open URL in a new tab or window
+function openURL(url) {
+ window.open(url, "_blank");
+}
+
+// open project wiki-page
+function wiki(page) {
+ window.open("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/" + page, "_blank");
+}
+
+// wrap URL into html a element
+function link(URL, description) {
+ return `${description}`;
+}
+
+function isCtrlClick(event) {
+ // meta key is cmd key on MacOs
+ return event.ctrlKey || event.metaKey;
+}
+
+function generateDate(from = 100, to = 1000) {
+ return new Date(rand(from, to), rand(12), rand(31)).toLocaleDateString("en", {year: "numeric", month: "long", day: "numeric"});
+}
+
+function getLongitude(x, decimals = 2) {
+ return rn(mapCoordinates.lonW + (x / graphWidth) * mapCoordinates.lonT, decimals);
+}
+
+function getLatitude(y, decimals = 2) {
+ return rn(mapCoordinates.latN - (y / graphHeight) * mapCoordinates.latT, decimals);
+}
+
+function getCoordinates(x, y, decimals = 2) {
+ return [getLongitude(x, decimals), getLatitude(y, decimals)];
+}
+
+// prompt replacer (prompt does not work in Electron)
+void (function () {
+ const prompt = document.getElementById("prompt");
+ const form = prompt.querySelector("#promptForm");
+
+ window.prompt = function (promptText = "Please provide an input", options = {default: 1, step: 0.01, min: 0, max: 100}, callback) {
+ if (options.default === undefined) {
+ ERROR && console.error("Prompt: options object does not have default value defined");
+ return;
+ }
+ const input = prompt.querySelector("#promptInput");
+ prompt.querySelector("#promptText").innerHTML = promptText;
+ const type = typeof options.default === "number" ? "number" : "text";
+ input.type = type;
+ if (options.step !== undefined) input.step = options.step;
+ if (options.min !== undefined) input.min = options.min;
+ if (options.max !== undefined) input.max = options.max;
+ input.placeholder = "type a " + type;
+ input.value = options.default;
+ prompt.style.display = "block";
+
+ form.addEventListener(
+ "submit",
+ event => {
+ prompt.style.display = "none";
+ const v = type === "number" ? +input.value : input.value;
+ event.preventDefault();
+ if (callback) callback(v);
+ },
+ {once: true}
+ );
+ };
+
+ const cancel = prompt.querySelector("#promptCancel");
+ cancel.addEventListener("click", () => (prompt.style.display = "none"));
+})();
+
+// indexedDB; ldb object
+void (function () {
+ function e(t, o) {
+ return n
+ ? void (n.transaction("s").objectStore("s").get(t).onsuccess = function (e) {
+ var t = (e.target.result && e.target.result.v) || null;
+ o(t);
+ })
+ : void setTimeout(function () {
+ e(t, o);
+ }, 100);
+ }
+ var t = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
+ if (!t) return void ERROR && console.error("indexedDB not supported");
+ var n,
+ o = {k: "", v: ""},
+ r = t.open("d2", 1);
+ (r.onsuccess = function (e) {
+ n = this.result;
+ }),
+ (r.onerror = function (e) {
+ ERROR && console.error("indexedDB request error"), INFO && console.log(e);
+ }),
+ (r.onupgradeneeded = function (e) {
+ n = null;
+ var t = e.target.result.createObjectStore("s", {keyPath: "k"});
+ t.transaction.oncomplete = function (e) {
+ n = e.target.db;
+ };
+ }),
+ (window.ldb = {
+ get: e,
+ set: function (e, t) {
+ (o.k = e), (o.v = t), n.transaction("s", "readwrite").objectStore("s").put(o);
+ }
+ });
+})();
diff --git a/utils/graphUtils.js b/utils/graphUtils.js
new file mode 100644
index 00000000..3814241f
--- /dev/null
+++ b/utils/graphUtils.js
@@ -0,0 +1,276 @@
+"use strict";
+// FMG utils related to graph
+
+// add boundary points to pseudo-clip voronoi cells
+function getBoundaryPoints(width, height, spacing) {
+ const offset = rn(-1 * spacing);
+ const bSpacing = spacing * 2;
+ const w = width - offset * 2;
+ const h = height - offset * 2;
+ const numberX = Math.ceil(w / bSpacing) - 1;
+ const numberY = Math.ceil(h / bSpacing) - 1;
+ let points = [];
+ for (let i = 0.5; i < numberX; i++) {
+ let x = Math.ceil((w * i) / numberX + offset);
+ points.push([x, offset], [x, h + offset]);
+ }
+ for (let i = 0.5; i < numberY; i++) {
+ let y = Math.ceil((h * i) / numberY + offset);
+ points.push([offset, y], [w + offset, y]);
+ }
+ return points;
+}
+
+// get points on a regular square grid and jitter them a bit
+function getJitteredGrid(width, height, spacing) {
+ const radius = spacing / 2; // square radius
+ const jittering = radius * 0.9; // max deviation
+ const doubleJittering = jittering * 2;
+ const jitter = () => Math.random() * doubleJittering - jittering;
+
+ let points = [];
+ for (let y = radius; y < height; y += spacing) {
+ for (let x = radius; x < width; x += spacing) {
+ const xj = Math.min(rn(x + jitter(), 2), width);
+ const yj = Math.min(rn(y + jitter(), 2), height);
+ points.push([xj, yj]);
+ }
+ }
+ return points;
+}
+
+// return cell index on a regular square grid
+function findGridCell(x, y) {
+ return Math.floor(Math.min(y / grid.spacing, grid.cellsY - 1)) * grid.cellsX + Math.floor(Math.min(x / grid.spacing, grid.cellsX - 1));
+}
+
+// return array of cell indexes in radius on a regular square grid
+function findGridAll(x, y, radius) {
+ const c = grid.cells.c;
+ let r = Math.floor(radius / grid.spacing);
+ let found = [findGridCell(x, y)];
+ if (!r || radius === 1) return found;
+ if (r > 0) found = found.concat(c[found[0]]);
+ if (r > 1) {
+ let frontier = c[found[0]];
+ while (r > 1) {
+ let cycle = frontier.slice();
+ frontier = [];
+ cycle.forEach(function (s) {
+ c[s].forEach(function (e) {
+ if (found.indexOf(e) !== -1) return;
+ found.push(e);
+ frontier.push(e);
+ });
+ });
+ r--;
+ }
+ }
+
+ return found;
+}
+
+// return closest pack points quadtree datum
+function find(x, y, radius = Infinity) {
+ return pack.cells.q.find(x, y, radius);
+}
+
+// return closest cell index
+function findCell(x, y, radius = Infinity) {
+ const found = pack.cells.q.find(x, y, radius);
+ return found ? found[2] : undefined;
+}
+
+// return array of cell indexes in radius
+function findAll(x, y, radius) {
+ const found = pack.cells.q.findAll(x, y, radius);
+ return found.map(r => r[2]);
+}
+
+// get polygon points for packed cells knowing cell id
+function getPackPolygon(i) {
+ return pack.cells.v[i].map(v => pack.vertices.p[v]);
+}
+
+// get polygon points for initial cells knowing cell id
+function getGridPolygon(i) {
+ return grid.cells.v[i].map(v => grid.vertices.p[v]);
+}
+
+// mbostock's poissonDiscSampler
+function* poissonDiscSampler(x0, y0, x1, y1, r, k = 3) {
+ if (!(x1 >= x0) || !(y1 >= y0) || !(r > 0)) throw new Error();
+
+ const width = x1 - x0;
+ const height = y1 - y0;
+ const r2 = r * r;
+ const r2_3 = 3 * r2;
+ const cellSize = r * Math.SQRT1_2;
+ const gridWidth = Math.ceil(width / cellSize);
+ const gridHeight = Math.ceil(height / cellSize);
+ const grid = new Array(gridWidth * gridHeight);
+ const queue = [];
+
+ function far(x, y) {
+ const i = (x / cellSize) | 0;
+ const j = (y / cellSize) | 0;
+ const i0 = Math.max(i - 2, 0);
+ const j0 = Math.max(j - 2, 0);
+ const i1 = Math.min(i + 3, gridWidth);
+ const j1 = Math.min(j + 3, gridHeight);
+ for (let j = j0; j < j1; ++j) {
+ const o = j * gridWidth;
+ for (let i = i0; i < i1; ++i) {
+ const s = grid[o + i];
+ if (s) {
+ const dx = s[0] - x;
+ const dy = s[1] - y;
+ if (dx * dx + dy * dy < r2) return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ function sample(x, y) {
+ queue.push((grid[gridWidth * ((y / cellSize) | 0) + ((x / cellSize) | 0)] = [x, y]));
+ return [x + x0, y + y0];
+ }
+
+ yield sample(width / 2, height / 2);
+
+ pick: while (queue.length) {
+ const i = (Math.random() * queue.length) | 0;
+ const parent = queue[i];
+
+ for (let j = 0; j < k; ++j) {
+ const a = 2 * Math.PI * Math.random();
+ const r = Math.sqrt(Math.random() * r2_3 + r2);
+ const x = parent[0] + r * Math.cos(a);
+ const y = parent[1] + r * Math.sin(a);
+ if (0 <= x && x < width && 0 <= y && y < height && far(x, y)) {
+ yield sample(x, y);
+ continue pick;
+ }
+ }
+
+ const r = queue.pop();
+ if (i < queue.length) queue[i] = r;
+ }
+}
+
+// filter land cells
+function isLand(i) {
+ return pack.cells.h[i] >= 20;
+}
+
+// filter water cells
+function isWater(i) {
+ return pack.cells.h[i] < 20;
+}
+
+// findAll d3.quandtree search from https://bl.ocks.org/lwthatcher/b41479725e0ff2277c7ac90df2de2b5e
+void (function addFindAll() {
+ const Quad = function (node, x0, y0, x1, y1) {
+ this.node = node;
+ this.x0 = x0;
+ this.y0 = y0;
+ this.x1 = x1;
+ this.y1 = y1;
+ };
+
+ const tree_filter = function (x, y, radius) {
+ var t = {x, y, x0: this._x0, y0: this._y0, x3: this._x1, y3: this._y1, quads: [], node: this._root};
+ if (t.node) {
+ t.quads.push(new Quad(t.node, t.x0, t.y0, t.x3, t.y3));
+ }
+ radiusSearchInit(t, radius);
+
+ var i = 0;
+ while ((t.q = t.quads.pop())) {
+ i++;
+
+ // Stop searching if this quadrant can’t contain a closer node.
+ if (!(t.node = t.q.node) || (t.x1 = t.q.x0) > t.x3 || (t.y1 = t.q.y0) > t.y3 || (t.x2 = t.q.x1) < t.x0 || (t.y2 = t.q.y1) < t.y0) continue;
+
+ // Bisect the current quadrant.
+ if (t.node.length) {
+ t.node.explored = true;
+ var xm = (t.x1 + t.x2) / 2,
+ ym = (t.y1 + t.y2) / 2;
+
+ t.quads.push(
+ new Quad(t.node[3], xm, ym, t.x2, t.y2),
+ new Quad(t.node[2], t.x1, ym, xm, t.y2),
+ new Quad(t.node[1], xm, t.y1, t.x2, ym),
+ new Quad(t.node[0], t.x1, t.y1, xm, ym)
+ );
+
+ // Visit the closest quadrant first.
+ if ((t.i = ((y >= ym) << 1) | (x >= xm))) {
+ t.q = t.quads[t.quads.length - 1];
+ t.quads[t.quads.length - 1] = t.quads[t.quads.length - 1 - t.i];
+ t.quads[t.quads.length - 1 - t.i] = t.q;
+ }
+ }
+
+ // Visit this point. (Visiting coincident points isn’t necessary!)
+ else {
+ var dx = x - +this._x.call(null, t.node.data),
+ dy = y - +this._y.call(null, t.node.data),
+ d2 = dx * dx + dy * dy;
+ radiusSearchVisit(t, d2);
+ }
+ }
+ return t.result;
+ };
+ d3.quadtree.prototype.findAll = tree_filter;
+
+ var radiusSearchInit = function (t, radius) {
+ t.result = [];
+ (t.x0 = t.x - radius), (t.y0 = t.y - radius);
+ (t.x3 = t.x + radius), (t.y3 = t.y + radius);
+ t.radius = radius * radius;
+ };
+
+ var radiusSearchVisit = function (t, d2) {
+ t.node.data.scanned = true;
+ if (d2 < t.radius) {
+ do {
+ t.result.push(t.node.data);
+ t.node.data.selected = true;
+ } while ((t.node = t.node.next));
+ }
+ };
+})();
+
+// helper function non-used for the generation
+function drawCellsValue(data) {
+ debug.selectAll("text").remove();
+ debug
+ .selectAll("text")
+ .data(data)
+ .enter()
+ .append("text")
+ .attr("x", (d, i) => pack.cells.p[i][0])
+ .attr("y", (d, i) => pack.cells.p[i][1])
+ .text(d => d);
+}
+
+// helper function non-used for the generation
+function drawPolygons(data) {
+ const max = d3.max(data),
+ min = d3.min(data),
+ scheme = getColorScheme();
+ data = data.map(d => 1 - normalize(d, min, max));
+
+ debug.selectAll("polygon").remove();
+ debug
+ .selectAll("polygon")
+ .data(data)
+ .enter()
+ .append("polygon")
+ .attr("points", (d, i) => getPackPolygon(i))
+ .attr("fill", d => scheme(d))
+ .attr("stroke", d => scheme(d));
+}
diff --git a/utils/nodeUtils.js b/utils/nodeUtils.js
new file mode 100644
index 00000000..0010f3d8
--- /dev/null
+++ b/utils/nodeUtils.js
@@ -0,0 +1,30 @@
+"use strict";
+// FMG utils related to nodes
+
+// remove parent element (usually if child is clicked)
+function removeParent() {
+ this.parentNode.parentNode.removeChild(this.parentNode);
+}
+
+// polyfill for composedPath
+function getComposedPath(node) {
+ let parent;
+ if (node.parentNode) parent = node.parentNode;
+ else if (node.host) parent = node.host;
+ else if (node.defaultView) parent = node.defaultView;
+ if (parent !== undefined) return [node].concat(getComposedPath(parent));
+ return [node];
+}
+
+// get next unused id
+function getNextId(core, i = 1) {
+ while (document.getElementById(core + i)) i++;
+ return core + i;
+}
+
+function getAbsolutePath(href) {
+ if (!href) return "";
+ const link = document.createElement("a");
+ link.href = href;
+ return link.href;
+}
diff --git a/utils/numberUtils.js b/utils/numberUtils.js
new file mode 100644
index 00000000..e3f143a5
--- /dev/null
+++ b/utils/numberUtils.js
@@ -0,0 +1,22 @@
+"use strict";
+// FMG utils related to numbers
+
+// round value to d decimals
+function rn(v, d = 0) {
+ const m = Math.pow(10, d);
+ return Math.round(v * m) / m;
+}
+
+function minmax(value, min, max) {
+ return Math.min(Math.max(value, min), max);
+}
+
+// return value in range [0, 100]
+function lim(v) {
+ return minmax(v, 0, 100);
+}
+
+// normalization function
+function normalize(val, min, max) {
+ return minmax((val - min) / (max - min), 0, 1);
+}
diff --git a/utils/polyfills.js b/utils/polyfills.js
new file mode 100644
index 00000000..369e647f
--- /dev/null
+++ b/utils/polyfills.js
@@ -0,0 +1,16 @@
+"use strict";
+
+// replaceAll
+if (String.prototype.replaceAll === undefined) {
+ String.prototype.replaceAll = function (str, newStr) {
+ if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") return this.replace(str, newStr);
+ return this.replace(new RegExp(str, "g"), newStr);
+ };
+}
+
+// flat
+if (Array.prototype.flat === undefined) {
+ Array.prototype.flat = function () {
+ return this.reduce((acc, val) => (Array.isArray(val) ? acc.concat(val.flat()) : acc.concat(val)), []);
+ };
+}
diff --git a/utils/probabilityUtils.js b/utils/probabilityUtils.js
new file mode 100644
index 00000000..454b659c
--- /dev/null
+++ b/utils/probabilityUtils.js
@@ -0,0 +1,76 @@
+"use strict";
+// FMG utils related to randomness
+
+// random number in a range
+function rand(min, max) {
+ if (min === undefined && max === undefined) return Math.random();
+ if (max === undefined) {
+ max = min;
+ min = 0;
+ }
+ return Math.floor(Math.random() * (max - min + 1)) + min;
+}
+
+// probability shorthand
+function P(probability) {
+ if (probability >= 1) return true;
+ if (probability <= 0) return false;
+ return Math.random() < probability;
+}
+
+function each(n) {
+ return i => i % n === 0;
+}
+
+// random number (normal or gaussian distribution)
+function gauss(expected = 100, deviation = 30, min = 0, max = 300, round = 0) {
+ return rn(minmax(d3.randomNormal(expected, deviation)(), min, max), round);
+}
+
+// probability shorthand for floats
+function Pint(float) {
+ return ~~float + +P(float % 1);
+}
+
+// return random value from the array
+function ra(array) {
+ return array[Math.floor(Math.random() * array.length)];
+}
+
+// return random value from weighted array {"key1":weight1, "key2":weight2}
+function rw(object) {
+ const array = [];
+ for (const key in object) {
+ for (let i = 0; i < object[key]; i++) {
+ array.push(key);
+ }
+ }
+ return array[Math.floor(Math.random() * array.length)];
+}
+
+// return a random integer from min to max biased towards one end based on exponent distribution (the bigger ex the higher bias towards min)
+function biased(min, max, ex) {
+ return Math.round(min + (max - min) * Math.pow(Math.random(), ex));
+}
+
+// get number from string in format "1-3" or "2" or "0.5"
+function getNumberInRange(r) {
+ if (typeof r !== "string") {
+ ERROR && console.error("The value should be a string", r);
+ return 0;
+ }
+ if (!isNaN(+r)) return ~~r + +P(r - ~~r);
+ const sign = r[0] === "-" ? -1 : 1;
+ if (isNaN(+r[0])) r = r.slice(1);
+ const range = r.includes("-") ? r.split("-") : null;
+ if (!range) {
+ ERROR && console.error("Cannot parse the number. Check the format", r);
+ return 0;
+ }
+ const count = rand(range[0] * sign, +range[1]);
+ if (isNaN(count) || count < 0) {
+ ERROR && console.error("Cannot parse number. Check the format", r);
+ return 0;
+ }
+ return count;
+}
diff --git a/utils/stringUtils.js b/utils/stringUtils.js
new file mode 100644
index 00000000..eddc88f6
--- /dev/null
+++ b/utils/stringUtils.js
@@ -0,0 +1,116 @@
+"use strict";
+// FMG utils related to strings
+
+// round numbers in string to d decimals
+function round(s, d = 1) {
+ return s.replace(/[\d\.-][\d\.e-]*/g, function (n) {
+ return rn(n, d);
+ });
+}
+
+// return string with 1st char capitalized
+function capitalize(string) {
+ return string.charAt(0).toUpperCase() + string.slice(1);
+}
+
+// check if char is vowel or can serve as vowel
+function vowel(c) {
+ return `aeiouyɑ'əøɛœæɶɒɨɪɔɐʊɤɯаоиеёэыуюяàèìòùỳẁȁȅȉȍȕáéíóúýẃőűâêîôûŷŵäëïöüÿẅãẽĩõũỹąęįǫųāēīōūȳăĕĭŏŭǎěǐǒǔȧėȯẏẇạẹịọụỵẉḛḭṵṳ`.includes(c);
+}
+
+// remove vowels from the end of the string
+function trimVowels(string) {
+ while (string.length > 3 && vowel(last(string))) {
+ string = string.slice(0, -1);
+ }
+ return string;
+}
+
+// get adjective form from noun
+function getAdjective(string) {
+ // special cases for some suffixes
+ if (string.length > 8 && string.slice(-6) === "orszag") return string.slice(0, -6);
+ if (string.length > 6 && string.slice(-4) === "stan") return string.slice(0, -4);
+ if (P(0.5) && string.slice(-4) === "land") return string + "ic";
+ if (string.slice(-4) === " Guo") string = string.slice(0, -4);
+
+ // don't change is name ends on suffix
+ if (string.slice(-2) === "an") return string;
+ if (string.slice(-3) === "ese") return string;
+ if (string.slice(-1) === "i") return string;
+
+ const end = string.slice(-1); // last letter of string
+ if (end === "a") return (string += "n");
+ if (end === "o") return (string = trimVowels(string) + "an");
+ if (vowel(end) || end === "c") return (string += "an"); // ceiuy
+ if (end === "m" || end === "n") return (string += "ese");
+ if (end === "q") return (string += "i");
+ return trimVowels(string) + "ian";
+}
+
+// get ordinal out of integer: 1 => 1st
+const nth = n => n + (["st", "nd", "rd"][((((n + 90) % 100) - 10) % 10) - 1] || "th");
+
+// get two-letters code (abbreviation) from string
+function abbreviate(name, restricted = []) {
+ const parsed = name.replace("Old ", "O ").replace(/[()]/g, ""); // remove Old prefix and parentheses
+ const words = parsed.split(" ");
+ const letters = words.join("");
+
+ let code = words.length === 2 ? words[0][0] + words[1][0] : letters.slice(0, 2);
+ for (let i = 1; i < letters.length - 1 && restricted.includes(code); i++) {
+ code = letters[0] + letters[i].toUpperCase();
+ }
+ return code;
+}
+
+// conjunct array: [A,B,C] => "A, B and C"
+function list(array) {
+ if (!Intl.ListFormat) return array.join(", ");
+ const conjunction = new Intl.ListFormat(window.lang || "en", {style: "long", type: "conjunction"});
+ return conjunction.format(array);
+}
+
+// split string into 2 almost equal parts not breaking words
+function splitInTwo(str) {
+ const half = str.length / 2;
+ const ar = str.split(" ");
+ if (ar.length < 2) return ar; // only one word
+ let first = "",
+ last = "",
+ middle = "",
+ rest = "";
+
+ ar.forEach((w, d) => {
+ if (d + 1 !== ar.length) w += " ";
+ rest += w;
+ if (!first || rest.length < half) first += w;
+ else if (!middle) middle = w;
+ else last += w;
+ });
+
+ if (!last) return [first, middle];
+ if (first.length < last.length) return [first + middle, last];
+ return [first, middle + last];
+}
+
+// transform string to array [translateX,translateY,rotateDeg,rotateX,rotateY,scale]
+function parseTransform(string) {
+ if (!string) return [0, 0, 0, 0, 0, 1];
+
+ const a = string
+ .replace(/[a-z()]/g, "")
+ .replace(/[ ]/g, ",")
+ .split(",");
+ return [a[0] || 0, a[1] || 0, a[2] || 0, a[3] || 0, a[4] || 0, a[5] || 1];
+}
+
+// check if string is a valid for JSON parse
+JSON.isValid = str => {
+ try {
+ JSON.parse(str);
+ } catch (e) {
+ return false;
+ }
+ return true;
+};
diff --git a/utils/unitUtils.js b/utils/unitUtils.js
new file mode 100644
index 00000000..609b5eb9
--- /dev/null
+++ b/utils/unitUtils.js
@@ -0,0 +1,45 @@
+"use strict";
+// FMG utils related to units
+
+// conver temperature from °C to other scales
+function convertTemperature(temp) {
+ switch (temperatureScale.value) {
+ case "°C":
+ return temp + "°C";
+ case "°F":
+ return rn((temp * 9) / 5 + 32) + "°F";
+ case "K":
+ return rn(temp + 273.15) + "K";
+ case "°R":
+ return rn(((temp + 273.15) * 9) / 5) + "°R";
+ case "°De":
+ return rn(((100 - temp) * 3) / 2) + "°De";
+ case "°N":
+ return rn((temp * 33) / 100) + "°N";
+ case "°Ré":
+ return rn((temp * 4) / 5) + "°Ré";
+ case "°Rø":
+ return rn((temp * 21) / 40 + 7.5) + "°Rø";
+ default:
+ return temp + "°C";
+ }
+}
+
+// corvent number to short string with SI postfix
+function si(n) {
+ if (n >= 1e9) return rn(n / 1e9, 1) + "B";
+ if (n >= 1e8) return rn(n / 1e6) + "M";
+ if (n >= 1e6) return rn(n / 1e6, 1) + "M";
+ if (n >= 1e4) return rn(n / 1e3) + "K";
+ if (n >= 1e3) return rn(n / 1e3, 1) + "K";
+ return rn(n);
+}
+
+// getInteger number from user input data
+function getInteger(value) {
+ const metric = value.slice(-1);
+ if (metric === "K") return parseInt(value.slice(0, -1) * 1e3);
+ if (metric === "M") return parseInt(value.slice(0, -1) * 1e6);
+ if (metric === "B") return parseInt(value.slice(0, -1) * 1e9);
+ return parseInt(value);
+}