diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b254d0ad..acedeb18 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -14,8 +14,8 @@ # Versioning - diff --git a/index.html b/index.html index 635ba6b2..5c14f769 100644 --- a/index.html +++ b/index.html @@ -1191,6 +1191,15 @@ + + + Letter spacing + + + + + + Stroke dash @@ -2688,6 +2697,24 @@ /> + + + - + - - - + + + +

GeoJSON format is used in GIS tools such as QGIS. Check out @@ -8016,13 +8047,14 @@ + - + @@ -8034,26 +8066,27 @@ + - + - + - + - - - + + + - + - + - + @@ -8065,18 +8098,18 @@ - + - + - - - + + + @@ -8087,13 +8120,13 @@ - + - - + + - + diff --git a/main.js b/main.js index 12f75328..4263318e 100644 --- a/main.js +++ b/main.js @@ -62,7 +62,7 @@ let regions = viewbox.append("g").attr("id", "regions"); let statesBody = regions.append("g").attr("id", "statesBody"); let statesHalo = regions.append("g").attr("id", "statesHalo"); let provs = viewbox.append("g").attr("id", "provs"); -let zones = viewbox.append("g").attr("id", "zones").style("display", "none"); +let zones = viewbox.append("g").attr("id", "zones"); let borders = viewbox.append("g").attr("id", "borders"); let stateBorders = borders.append("g").attr("id", "stateBorders"); let provinceBorders = borders.append("g").attr("id", "provinceBorders"); @@ -662,7 +662,7 @@ async function generate(options) { Military.generate(); Markers.generate(); - addZones(); + Zones.generate(); drawScaleBar(scaleBar, scale); Names.getMapName(); @@ -684,7 +684,7 @@ async function generate(options) { buttons: { "Clear data": function () { localStorage.clear(); - localStorage.setItem("version", version); + localStorage.setItem("version", VERSION); }, Regenerate: function () { regenerateMap("generation error"); @@ -1484,442 +1484,6 @@ function rankCells() { TIME && console.timeEnd("rankCells"); } -// generate zones -function addZones(number = 1) { - TIME && console.time("addZones"); - const {cells, states, burgs} = pack; - const used = new Uint8Array(cells.i.length); // to store used cells - const zonesData = []; - - for (let i = 0; i < rn(Math.random() * 1.8 * number); i++) addInvasion(); // invasion of enemy lands - for (let i = 0; i < rn(Math.random() * 1.6 * number); i++) addRebels(); // rebels along a state border - for (let i = 0; i < rn(Math.random() * 1.6 * number); i++) addProselytism(); // proselitism of organized religion - for (let i = 0; i < rn(Math.random() * 1.6 * number); i++) addCrusade(); // crusade on heresy lands - for (let i = 0; i < rn(Math.random() * 1.8 * number); i++) addDisease(); // disease starting in a random city - for (let i = 0; i < rn(Math.random() * 1.4 * number); i++) addDisaster(); // disaster starting in a random city - for (let i = 0; i < rn(Math.random() * 1.4 * number); i++) addEruption(); // volcanic eruption aroung volcano - for (let i = 0; i < rn(Math.random() * 1.0 * number); i++) addAvalanche(); // avalanche impacting highland road - for (let i = 0; i < rn(Math.random() * 1.4 * number); i++) addFault(); // fault line in elevated areas - for (let i = 0; i < rn(Math.random() * 1.4 * number); i++) addFlood(); // flood on river banks - for (let i = 0; i < rn(Math.random() * 1.2 * number); i++) addTsunami(); // tsunami starting near coast - - drawZones(); - - function addInvasion() { - const atWar = states.filter(s => s.diplomacy && s.diplomacy.some(d => d === "Enemy")); - if (!atWar.length) return; - - const invader = ra(atWar); - const target = invader.diplomacy.findIndex(d => d === "Enemy"); - - const cell = ra( - cells.i.filter(i => cells.state[i] === target && cells.c[i].some(c => cells.state[c] === invader.i)) - ); - if (!cell) return; - - const cellsArray = [], - queue = [cell], - power = rand(5, 30); - - while (queue.length) { - const q = P(0.4) ? queue.shift() : queue.pop(); - cellsArray.push(q); - if (cellsArray.length > power) break; - - cells.c[q].forEach(e => { - if (used[e]) return; - if (cells.state[e] !== target) return; - used[e] = 1; - queue.push(e); - }); - } - - const invasion = rw({ - Invasion: 4, - Occupation: 3, - Raid: 2, - Conquest: 2, - Subjugation: 1, - Foray: 1, - Skirmishes: 1, - Incursion: 2, - Pillaging: 1, - Intervention: 1 - }); - const name = getAdjective(invader.name) + " " + invasion; - zonesData.push({name, type: "Invasion", cells: cellsArray, fill: "url(#hatch1)"}); - } - - function addRebels() { - const state = ra(states.filter(s => s.i && !s.removed && s.neighbors.some(n => n))); - if (!state) return; - - const neib = ra(state.neighbors.filter(n => n && !states[n].removed)); - if (!neib) return; - const cell = cells.i.find( - i => cells.state[i] === state.i && !state.removed && cells.c[i].some(c => cells.state[c] === neib) - ); - const cellsArray = []; - const queue = []; - if (cell) queue.push(cell); - - const power = rand(10, 30); - - while (queue.length) { - const q = queue.shift(); - cellsArray.push(q); - if (cellsArray.length > power) break; - - cells.c[q].forEach(e => { - if (used[e]) return; - if (cells.state[e] !== state.i) return; - used[e] = 1; - if (e % 4 !== 0 && !cells.c[e].some(c => cells.state[c] === neib)) return; - queue.push(e); - }); - } - - const rebels = rw({ - Rebels: 5, - Insurgents: 2, - Mutineers: 1, - Rioters: 1, - Separatists: 1, - Secessionists: 1, - Insurrection: 2, - Rebellion: 1, - Conspiracy: 2 - }); - const name = getAdjective(states[neib].name) + " " + rebels; - zonesData.push({name, type: "Rebels", cells: cellsArray, fill: "url(#hatch3)"}); - } - - function addProselytism() { - const organized = ra(pack.religions.filter(r => r.type === "Organized")); - if (!organized) return; - - const cell = ra( - cells.i.filter( - i => - cells.religion[i] && - cells.religion[i] !== organized.i && - cells.c[i].some(c => cells.religion[c] === organized.i) - ) - ); - if (!cell) return; - const target = cells.religion[cell]; - const cellsArray = [], - queue = [cell], - power = rand(10, 30); - - while (queue.length) { - const q = queue.shift(); - cellsArray.push(q); - if (cellsArray.length > power) break; - - cells.c[q].forEach(e => { - if (used[e]) return; - if (cells.religion[e] !== target) return; - if (cells.h[e] < 20) return; - used[e] = 1; - //if (e%2 !== 0 && !cells.c[e].some(c => cells.state[c] === neib)) return; - queue.push(e); - }); - } - - const name = getAdjective(organized.name.split(" ")[0]) + " Proselytism"; - zonesData.push({name, type: "Proselytism", cells: cellsArray, fill: "url(#hatch6)"}); - } - - function addCrusade() { - const heresy = ra(pack.religions.filter(r => r.type === "Heresy")); - if (!heresy) return; - - const cellsArray = cells.i.filter(i => !used[i] && cells.religion[i] === heresy.i); - if (!cellsArray.length) return; - cellsArray.forEach(i => (used[i] = 1)); - - const name = getAdjective(heresy.name.split(" ")[0]) + " Crusade"; - zonesData.push({name, type: "Crusade", cells: cellsArray, fill: "url(#hatch6)"}); - } - - function addDisease() { - const burg = ra(burgs.filter(b => !used[b.cell] && b.i && !b.removed)); // random burg - if (!burg) return; - - const cellsArray = []; - const cost = []; - const power = rand(20, 37); - - const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); - queue.queue({e: burg.cell, p: 0}); - - while (queue.length) { - const next = queue.dequeue(); - if (cells.burg[next.e] || cells.pop[next.e]) cellsArray.push(next.e); - used[next.e] = 1; - - cells.c[next.e].forEach(nextCellId => { - const c = Routes.getRoute(next.e, nextCellId) ? 5 : 100; - const p = next.p + c; - if (p > power) return; - - if (!cost[nextCellId] || p < cost[nextCellId]) { - cost[nextCellId] = p; - queue.queue({e: nextCellId, p}); - } - }); - } - - const adjective = () => - ra(["Great", "Silent", "Severe", "Blind", "Unknown", "Loud", "Deadly", "Burning", "Bloody", "Brutal", "Fatal"]); - const animal = () => - ra([ - "Ape", - "Bear", - "Boar", - "Cat", - "Cow", - "Dog", - "Pig", - "Fox", - "Bird", - "Horse", - "Rat", - "Raven", - "Sheep", - "Spider", - "Wolf" - ]); - const color = () => - ra([ - "Golden", - "White", - "Black", - "Red", - "Pink", - "Purple", - "Blue", - "Green", - "Yellow", - "Amber", - "Orange", - "Brown", - "Grey" - ]); - - const type = rw({ - Fever: 5, - Pestilence: 2, - Flu: 2, - Pox: 2, - Smallpox: 2, - Plague: 4, - Cholera: 2, - Dropsy: 1, - Leprosy: 2 - }); - const name = rw({[color()]: 4, [animal()]: 2, [adjective()]: 1}) + " " + type; - zonesData.push({name, type: "Disease", cells: cellsArray, fill: "url(#hatch12)"}); - } - - function addDisaster() { - const burg = ra(burgs.filter(b => !used[b.cell] && b.i && !b.removed)); // random burg - if (!burg) return; - - const cellsArray = [], - cost = [], - power = rand(5, 25); - const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); - queue.queue({e: burg.cell, p: 0}); - - while (queue.length) { - const next = queue.dequeue(); - if (cells.burg[next.e] || cells.pop[next.e]) cellsArray.push(next.e); - used[next.e] = 1; - - cells.c[next.e].forEach(function (e) { - const c = rand(1, 10); - const p = next.p + c; - if (p > power) return; - - if (!cost[e] || p < cost[e]) { - cost[e] = p; - queue.queue({e, p}); - } - }); - } - - const type = rw({Famine: 5, Dearth: 1, Drought: 3, Earthquake: 3, Tornadoes: 1, Wildfires: 1}); - const name = getAdjective(burg.name) + " " + type; - zonesData.push({name, type: "Disaster", cells: cellsArray, fill: "url(#hatch5)"}); - } - - function addEruption() { - const volcano = byId("markers").querySelector("use[data-id='#marker_volcano']"); - if (!volcano) return; - - const x = +volcano.dataset.x, - y = +volcano.dataset.y, - cell = findCell(x, y); - const id = volcano.id; - const note = notes.filter(n => n.id === id); - - if (note[0]) note[0].legend = note[0].legend.replace("Active volcano", "Erupting volcano"); - const name = note[0] ? note[0].name.replace(" Volcano", "") + " Eruption" : "Volcano Eruption"; - - const cellsArray = [], - queue = [cell], - power = rand(10, 30); - - while (queue.length) { - const q = P(0.5) ? queue.shift() : queue.pop(); - cellsArray.push(q); - if (cellsArray.length > power) break; - cells.c[q].forEach(e => { - if (used[e] || cells.h[e] < 20) return; - used[e] = 1; - queue.push(e); - }); - } - - zonesData.push({name, type: "Disaster", cells: cellsArray, fill: "url(#hatch7)"}); - } - - function addAvalanche() { - const routes = cells.i.filter(i => !used[i] && Routes.isConnected(i) && cells.h[i] >= 70); - if (!routes.length) return; - - const cell = +ra(routes); - const cellsArray = [], - queue = [cell], - power = rand(3, 15); - - while (queue.length) { - const q = P(0.3) ? queue.shift() : queue.pop(); - cellsArray.push(q); - if (cellsArray.length > power) break; - cells.c[q].forEach(e => { - if (used[e] || cells.h[e] < 65) return; - used[e] = 1; - queue.push(e); - }); - } - - const proper = getAdjective(Names.getCultureShort(cells.culture[cell])); - const name = proper + " Avalanche"; - zonesData.push({name, type: "Disaster", cells: cellsArray, fill: "url(#hatch5)"}); - } - - function addFault() { - const elevated = cells.i.filter(i => !used[i] && cells.h[i] > 50 && cells.h[i] < 70); - if (!elevated.length) return; - - const cell = ra(elevated); - const cellsArray = [], - queue = [cell], - power = rand(3, 15); - - while (queue.length) { - const q = queue.pop(); - if (cells.h[q] >= 20) cellsArray.push(q); - if (cellsArray.length > power) break; - cells.c[q].forEach(e => { - if (used[e] || cells.r[e]) return; - used[e] = 1; - queue.push(e); - }); - } - - const proper = getAdjective(Names.getCultureShort(cells.culture[cell])); - const name = proper + " Fault"; - zonesData.push({name, type: "Disaster", cells: cellsArray, fill: "url(#hatch2)"}); - } - - function addFlood() { - const fl = cells.fl.filter(fl => fl), - meanFlux = d3.mean(fl), - maxFlux = d3.max(fl), - flux = (maxFlux - meanFlux) / 2 + meanFlux; - const rivers = cells.i.filter( - i => !used[i] && cells.h[i] < 50 && cells.r[i] && cells.fl[i] > flux && cells.burg[i] - ); - if (!rivers.length) return; - - const cell = +ra(rivers), - river = cells.r[cell]; - const cellsArray = [], - queue = [cell], - power = rand(5, 30); - - while (queue.length) { - const q = queue.pop(); - cellsArray.push(q); - if (cellsArray.length > power) break; - - cells.c[q].forEach(e => { - if (used[e] || cells.h[e] < 20 || cells.r[e] !== river || cells.h[e] > 50 || cells.fl[e] < meanFlux) return; - used[e] = 1; - queue.push(e); - }); - } - - const name = getAdjective(burgs[cells.burg[cell]].name) + " Flood"; - zonesData.push({name, type: "Disaster", cells: cellsArray, fill: "url(#hatch13)"}); - } - - function addTsunami() { - const coastal = cells.i.filter(i => !used[i] && cells.t[i] === -1 && pack.features[cells.f[i]].type !== "lake"); - if (!coastal.length) return; - - const cell = +ra(coastal); - const cellsArray = [], - queue = [cell], - power = rand(10, 30); - - while (queue.length) { - const q = queue.shift(); - if (cells.t[q] === 1) cellsArray.push(q); - if (cellsArray.length > power) break; - - cells.c[q].forEach(e => { - if (used[e]) return; - if (cells.t[e] > 2) return; - if (pack.features[cells.f[e]].type === "lake") return; - used[e] = 1; - queue.push(e); - }); - } - - const proper = getAdjective(Names.getCultureShort(cells.culture[cell])); - const name = proper + " Tsunami"; - zonesData.push({name, type: "Disaster", cells: cellsArray, fill: "url(#hatch13)"}); - } - - function drawZones() { - zones - .selectAll("g") - .data(zonesData) - .enter() - .append("g") - .attr("id", (d, i) => "zone" + i) - .attr("data-description", d => d.name) - .attr("data-type", d => d.type) - .attr("data-cells", d => d.cells.join(",")) - .attr("fill", d => d.fill) - .selectAll("polygon") - .data(d => d.cells) - .enter() - .append("polygon") - .attr("points", d => getPackPolygon(d)) - .attr("id", function (d) { - return this.parentNode.id + "_" + d; - }); - } - - TIME && console.timeEnd("addZones"); -} - // show map stats on generation complete function showStatistics() { const heightmap = byId("templateInput").value; @@ -1971,8 +1535,7 @@ function undraw() { viewbox .selectAll("path, circle, polygon, line, text, use, #texture > image, #zones > g, #armies > g, #ruler > g") .remove(); - document - .getElementById("deftemp") + byId("deftemp") .querySelectorAll("path, clipPath, svg") .forEach(el => el.remove()); byId("coas").innerHTML = ""; // remove auto-generated emblems diff --git a/modules/burgs-and-states.js b/modules/burgs-and-states.js index 8441929c..5d6d7a9e 100644 --- a/modules/burgs-and-states.js +++ b/modules/burgs-and-states.js @@ -611,8 +611,7 @@ window.BurgsAndStates = (() => { // generate Diplomatic Relationships const generateDiplomacy = () => { TIME && console.time("generateDiplomacy"); - const cells = pack.cells, - states = pack.states; + const {cells, states} = pack; const chronicle = (states[0].diplomacy = []); const valid = states.filter(s => s.i && !states.removed); @@ -696,21 +695,23 @@ window.BurgsAndStates = (() => { const defender = ra( ad.map((r, d) => (r === "Rival" && !states[d].diplomacy.includes("Vassal") ? d : 0)).filter(d => d) ); - let ap = states[attacker].area * states[attacker].expansionism, - dp = states[defender].area * states[defender].expansionism; + let ap = states[attacker].area * states[attacker].expansionism; + let dp = states[defender].area * states[defender].expansionism; if (ap < dp * gauss(1.6, 0.8, 0, 10, 2)) continue; // defender is too strong - const an = states[attacker].name, - dn = states[defender].name; // names - const attackers = [attacker], - defenders = [defender]; // attackers and defenders array + + const an = states[attacker].name; + const dn = states[defender].name; // names + const attackers = [attacker]; + const defenders = [defender]; // attackers and defenders array const dd = states[defender].diplomacy; // defender relations; - // start a war - const war = [`${an}-${trimVowels(dn)}ian War`, `${an} declared a war on its rival ${dn}`]; - const end = options.year; - const start = end - gauss(2, 2, 0, 5); - states[attacker].campaigns.push({name: `${trimVowels(dn)}ian War`, start, end}); - states[defender].campaigns.push({name: `${trimVowels(an)}ian War`, start, end}); + // start an ongoing war + const name = `${an}-${trimVowels(dn)}ian War`; + const start = options.year - gauss(2, 3, 0, 10); + const war = [name, `${an} declared a war on its rival ${dn}`]; + const campaign = {name, start, attacker, defender}; + states[attacker].campaigns.push(campaign); + states[defender].campaigns.push(campaign); // attacker vassals join the war ad.forEach((r, d) => { @@ -790,7 +791,6 @@ window.BurgsAndStates = (() => { } TIME && console.timeEnd("generateDiplomacy"); - //console.table(states.map(s => s.diplomacy)); }; // select a forms for listed or all valid states diff --git a/modules/dynamic/auto-update.js b/modules/dynamic/auto-update.js index 4bbb778d..a11faa31 100644 --- a/modules/dynamic/auto-update.js +++ b/modules/dynamic/auto-update.js @@ -1,8 +1,10 @@ "use strict"; // update old map file to the current version -export function resolveVersionConflicts(version) { - if (version < 1) { +export function resolveVersionConflicts(mapVersion) { + const isOlderThan = tagVersion => compareVersions(mapVersion, tagVersion).isOlder; + + if (isOlderThan("1.0.0")) { // v1.0 added a new religions layer relig = viewbox.insert("g", "#terrain").attr("id", "relig"); Religions.generate(); @@ -63,7 +65,7 @@ export function resolveVersionConflicts(version) { .attr("stroke-width", 0) .attr("stroke-dasharray", null) .attr("stroke-linecap", "butt"); - addZones(); + Zones.generate(); if (!markers.selectAll("*").size()) { Markers.generate(); turnButtonOn("toggleMarkers"); @@ -107,11 +109,11 @@ export function resolveVersionConflicts(version) { biomesData.habitability.push(12); } - if (version < 1.1) { - // v1.0 initial code had a bug with religion layer id + if (isOlderThan("1.1.0")) { + // v1.0 code had a bug with religion layer id if (!relig.size()) relig = viewbox.insert("g", "#terrain").attr("id", "relig"); - // v1.0 initially has Sympathy status then relaced with Friendly + // v1.0 had Sympathy status then relaced with Friendly for (const s of pack.states) { if (!s.diplomacy) continue; s.diplomacy = s.diplomacy.map(r => (r === "Sympathy" ? "Friendly" : r)); @@ -203,7 +205,7 @@ export function resolveVersionConflicts(version) { drawCoastline(); } - if (version < 1.11) { + if (isOlderThan("1.11.0")) { // v1.11 added new attributes terrs.attr("scheme", "bright").attr("terracing", 0).attr("skip", 5).attr("relax", 0).attr("curve", 0); svg.select("#oceanic > *").attr("id", "oceanicPattern"); @@ -229,7 +231,7 @@ export function resolveVersionConflicts(version) { if (!terrain.attr("density")) terrain.attr("density", 0.4); } - if (version < 1.21) { + if (isOlderThan("1.21.0")) { // v1.11 replaced "display" attribute by "display" style viewbox.selectAll("g").each(function () { if (this.hasAttribute("display")) { @@ -255,12 +257,12 @@ export function resolveVersionConflicts(version) { }); } - if (version < 1.22) { + if (isOlderThan("1.22.0")) { // v1.22 changed state neighbors from Set object to array BurgsAndStates.collectStatistics(); } - if (version < 1.3) { + if (isOlderThan("1.3.0")) { // v1.3 added global options object const winds = options.slice(); // previostly wind was saved in settings[19] const year = rand(100, 2000); @@ -285,7 +287,7 @@ export function resolveVersionConflicts(version) { Military.generate(); } - if (version < 1.4) { + if (isOlderThan("1.4.0")) { // v1.35 added dry lakes if (!lakes.select("#dry").size()) { lakes.append("g").attr("id", "dry"); @@ -329,7 +331,7 @@ export function resolveVersionConflicts(version) { pack.states.filter(s => s.military).forEach(s => s.military.forEach(r => (r.state = s.i))); } - if (version < 1.5) { + if (isOlderThan("1.5.0")) { // not need to store default styles from v 1.5 localStorage.removeItem("styleClean"); localStorage.removeItem("styleGloom"); @@ -367,7 +369,7 @@ export function resolveVersionConflicts(version) { }); } - if (version < 1.6) { + if (isOlderThan("1.6.0")) { // v1.6 changed rivers data for (const river of pack.rivers) { const el = document.getElementById("river" + river.i); @@ -399,7 +401,7 @@ export function resolveVersionConflicts(version) { } } - if (version < 1.61) { + if (isOlderThan("1.61.0")) { // v1.61 changed rulers data ruler.style("display", null); rulers = new Rulers(); @@ -453,12 +455,12 @@ export function resolveVersionConflicts(version) { pattern.innerHTML = /* html */ ``; } - if (version < 1.62) { + if (isOlderThan("1.62.0")) { // v1.62 changed grid data gridOverlay.attr("size", null); } - if (version < 1.63) { + if (isOlderThan("1.63.0")) { // v1.63 changed ocean pattern opacity element const oceanPattern = document.getElementById("oceanPattern"); if (oceanPattern) oceanPattern.removeAttribute("opacity"); @@ -472,7 +474,7 @@ export function resolveVersionConflicts(version) { labels.select("#addedLabels").style("text-shadow", "white 0 0 4px"); } - if (version < 1.64) { + if (isOlderThan("1.64.0")) { // v1.64 change states style const opacity = regions.attr("opacity"); const filter = regions.attr("filter"); @@ -481,7 +483,7 @@ export function resolveVersionConflicts(version) { regions.attr("opacity", null).attr("filter", null); } - if (version < 1.65) { + if (isOlderThan("1.65.0")) { // v1.65 changed rivers data d3.select("#rivers").attr("style", null); // remove style to unhide layer const {cells, rivers} = pack; @@ -523,13 +525,13 @@ export function resolveVersionConflicts(version) { } } - if (version < 1.652) { + if (isOlderThan("1.652.0")) { // remove style to unhide layers rivers.attr("style", null); borders.attr("style", null); } - if (version < 1.7) { + if (isOlderThan("1.7.0")) { // v1.7 changed markers data const defs = document.getElementById("defs-markers"); const markersGroup = document.getElementById("markers"); @@ -587,7 +589,7 @@ export function resolveVersionConflicts(version) { } } - if (version < 1.72) { + if (isOlderThan("1.72.0")) { // v1.72 renamed custom style presets const storedStyles = Object.keys(localStorage).filter(key => key.startsWith("style")); storedStyles.forEach(styleName => { @@ -598,7 +600,7 @@ export function resolveVersionConflicts(version) { }); } - if (version < 1.73) { + if (isOlderThan("1.73.0")) { // v1.73 moved the hatching patterns out of the user's SVG document.getElementById("hatching")?.remove(); @@ -609,17 +611,17 @@ export function resolveVersionConflicts(version) { }); } - if (version < 1.84) { + if (isOlderThan("1.84.0")) { // v1.84.0 added grid.cellsDesired to stored data if (!grid.cellsDesired) grid.cellsDesired = rn((graphWidth * graphHeight) / grid.spacing ** 2, -3); } - if (version < 1.85) { + if (isOlderThan("1.85.0")) { // v1.84.0 moved intial screen out of maon svg svg.select("#initial").remove(); } - if (version < 1.86) { + if (isOlderThan("1.86.0")) { // v1.86.0 added multi-origin culture and religion hierarchy trees for (const culture of pack.cultures) { culture.origins = [culture.origin]; @@ -632,14 +634,14 @@ export function resolveVersionConflicts(version) { } } - if (version < 1.88) { + if (isOlderThan("1.88.0")) { // v1.87 may have incorrect shield for some reason pack.states.forEach(({coa}) => { if (coa?.shield === "state") delete coa.shield; }); } - if (version < 1.91) { + if (isOlderThan("1.91.0")) { // from 1.91.00 custom coa is moved to coa object pack.states.forEach(state => { if (state.coa === "custom") state.coa = {custom: true}; @@ -688,14 +690,14 @@ export function resolveVersionConflicts(version) { }); } - if (version < 1.92) { + if (isOlderThan("1.92.0")) { // v1.92 change labels text-anchor from 'start' to 'middle' labels.selectAll("tspan").each(function () { this.setAttribute("x", 0); }); } - if (version < 1.94) { + if (isOlderThan("1.94.0")) { // from v1.94.00 texture image is removed when layer is off texture.style("display", null); @@ -713,7 +715,7 @@ export function resolveVersionConflicts(version) { } } - if (version < 1.95) { + if (isOlderThan("1.95.0")) { // v1.95.00 added vignette visual layer const mask = defs.append("mask").attr("id", "vignette-mask"); mask.append("rect").attr("fill", "white").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%"); @@ -739,7 +741,7 @@ export function resolveVersionConflicts(version) { vignette.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%"); } - if (version < 1.96) { + if (isOlderThan("1.96.0")) { // v1.96 added ocean rendering for heightmap terrs.selectAll("*").remove(); @@ -833,7 +835,7 @@ export function resolveVersionConflicts(version) { }); } - if (version < 1.97) { + if (isOlderThan("1.97.0")) { // v1.97.00 changed MFCG link to an arbitrary preview URL options.villageMaxPopulation = 2000; options.showBurgPreview = options.showMFCGMap; @@ -849,7 +851,7 @@ export function resolveVersionConflicts(version) { }); } - if (version < 1.98) { + if (isOlderThan("1.98.0")) { // v1.98.00 changed compass layer and rose element id const rose = compass.select("use"); rose.attr("xlink:href", "#defs-compass-rose"); @@ -861,7 +863,7 @@ export function resolveVersionConflicts(version) { } } - if (version < 1.99) { + if (isOlderThan("1.99.0")) { // v1.99.00 changed routes generation algorithm and data format routes.attr("display", null).attr("style", null); @@ -923,4 +925,19 @@ export function resolveVersionConflicts(version) { } } } + + if (isOlderThan("1.100.0")) { + // v1.100.00 added zones to pack data + pack.zones = []; + zones.selectAll("g").each(function () { + const i = pack.zones.length; + const name = this.dataset.description; + const type = this.dataset.type; + const color = this.getAttribute("fill"); + const cells = this.dataset.cells.split(",").map(Number); + pack.zones.push({i, name, type, cells, color}); + }); + zones.style("display", null).selectAll("*").remove(); + if (layerIsOn("toggleZones")) drawZones(); + } } diff --git a/modules/dynamic/export-json.js b/modules/dynamic/export-json.js index df0d6815..cc895b7a 100644 --- a/modules/dynamic/export-json.js +++ b/modules/dynamic/export-json.js @@ -53,7 +53,8 @@ function getMinimalDataJson() { religions: pack.religions, rivers: pack.rivers, markers: pack.markers, - routes: pack.routes + routes: pack.routes, + zones: pack.zones }; return JSON.stringify({info, settings, mapCoordinates, pack: packData, biomesData, notes, nameBases}); } @@ -72,7 +73,7 @@ function getGridDataJson() { function getMapInfo() { return { - version, + version: VERSION, description: "Azgaar's Fantasy Map Generator output: azgaar.github.io/Fantasy-map-generator", exportedAt: new Date().toISOString(), mapName: mapName.value, @@ -172,7 +173,8 @@ function getPackCellsData() { religions: pack.religions, rivers: pack.rivers, markers: pack.markers, - routes: pack.routes + routes: pack.routes, + zones: pack.zones }; } diff --git a/modules/io/export.js b/modules/io/export.js index 0d1286b9..15e2b020 100644 --- a/modules/io/export.js +++ b/modules/io/export.js @@ -440,14 +440,24 @@ function inlineStyle(clone) { emptyG.remove(); } -function saveGeoJSON_Cells() { +function saveGeoJsonCells() { + const {cells, vertices} = pack; const json = {type: "FeatureCollection", features: []}; - const cells = pack.cells; + const getPopulation = i => { const [r, u] = getCellPopulation(i); return rn(r + u); }; - const getHeight = i => parseInt(getFriendlyHeight([cells.p[i][0], cells.p[i][1]])); + + const getHeight = i => parseInt(getFriendlyHeight([...cells.p[i]])); + + function getCellCoordinates(cellVertices) { + const coordinates = cellVertices.map(vertex => { + const [x, y] = vertices.p[vertex]; + return getCoordinates(x, y, 4); + }); + return [[...coordinates, coordinates[0]]]; + } cells.i.forEach(i => { const coordinates = getCellCoordinates(cells.v[i]); @@ -470,20 +480,14 @@ function saveGeoJSON_Cells() { downloadFile(JSON.stringify(json), fileName, "application/json"); } -function saveGeoJSON_Routes() { - const {cells, burgs} = pack; - let points = cells.p.map(([x, y], cellId) => { - const burgId = cells.burg[cellId]; - if (burgId) return [burgs[burgId].x, burgs[burgId].y]; - return [x, y]; - }); - - const features = pack.routes.map(route => { - const coordinates = route.points || getRoutePoints(route, points); +function saveGeoJsonRoutes() { + const features = pack.routes.map(({i, points, group, name = null}) => { + const coordinates = points.map(([x, y]) => getCoordinates(x, y, 4)); + const id = `route${i}`; return { type: "Feature", geometry: {type: "LineString", coordinates}, - properties: {id: route.id, group: route.group} + properties: {id, group, name} }; }); const json = {type: "FeatureCollection", features}; @@ -492,24 +496,27 @@ function saveGeoJSON_Routes() { downloadFile(JSON.stringify(json), fileName, "application/json"); } -function saveGeoJSON_Rivers() { - const json = {type: "FeatureCollection", features: []}; - - rivers.selectAll("path").each(function () { - const river = pack.rivers.find(r => r.i === +this.id.slice(5)); - if (!river) return; - - const coordinates = getRiverPoints(this); - const properties = {...river, id: this.id}; - const feature = {type: "Feature", geometry: {type: "LineString", coordinates}, properties}; - json.features.push(feature); - }); +function saveGeoJsonRivers() { + const features = pack.rivers.map( + ({i, cells, points, source, mouth, parent, basin, widthFactor, sourceWidth, discharge, name, type}) => { + if (!cells || cells.length < 2) return; + const meanderedPoints = Rivers.addMeandering(cells, points); + const coordinates = meanderedPoints.map(([x, y]) => getCoordinates(x, y, 4)); + const id = `river${i}`; + return { + type: "Feature", + geometry: {type: "LineString", coordinates}, + properties: {id, source, mouth, parent, basin, widthFactor, sourceWidth, discharge, name, type} + }; + } + ); + const json = {type: "FeatureCollection", features}; const fileName = getFileName("Rivers") + ".geojson"; downloadFile(JSON.stringify(json), fileName, "application/json"); } -function saveGeoJSON_Markers() { +function saveGeoJsonMarkers() { const features = pack.markers.map(marker => { const {i, type, icon, x, y, size, fill, stroke} = marker; const coordinates = getCoordinates(x, y, 4); @@ -524,22 +531,3 @@ function saveGeoJSON_Markers() { 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 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/io/load.js b/modules/io/load.js index 81786374..6462b35f 100644 --- a/modules/io/load.js +++ b/modules/io/load.js @@ -104,8 +104,6 @@ function showUploadErrorMessage(error, URL, random) { function uploadMap(file, callback) { uploadMap.timeStart = performance.now(); - const OLDEST_SUPPORTED_VERSION = 0.7; - const currentVersion = parseFloat(version); const fileReader = new FileReader(); fileReader.onloadend = async function (fileLoadedEvent) { @@ -114,14 +112,14 @@ function uploadMap(file, callback) { const result = fileLoadedEvent.target.result; const [mapData, mapVersion] = await parseLoadedResult(result); - const isInvalid = !mapData || isNaN(mapVersion) || mapData.length < 26 || !mapData[5]; - const isUpdated = mapVersion === currentVersion; - const isAncient = mapVersion < OLDEST_SUPPORTED_VERSION; - const isNewer = mapVersion > currentVersion; - const isOutdated = mapVersion < currentVersion; + const isInvalid = !mapData || !isValidVersion(mapVersion) || mapData.length < 26 || !mapData[5]; + const isUpdated = compareVersions(mapVersion, VERSION).isEqual; + const isAncient = compareVersions(mapVersion, "0.7.0").isOlder; + const isNewer = compareVersions(mapVersion, VERSION).isNewer; + const isOutdated = compareVersions(mapVersion, VERSION).isOlder; if (isInvalid) return showUploadMessage("invalid", mapData, mapVersion); - if (isUpdated) return parseLoadedData(mapData); + if (isUpdated) return showUploadMessage("updated", mapData, mapVersion); if (isAncient) return showUploadMessage("ancient", mapData, mapVersion); if (isNewer) return showUploadMessage("newer", mapData, mapVersion); if (isOutdated) return showUploadMessage("outdated", mapData, mapVersion); @@ -154,8 +152,8 @@ async function parseLoadedResult(result) { const decoded = isDelimited ? resultAsString : decodeURIComponent(atob(resultAsString)); const mapData = decoded.split("\r\n"); - const mapVersion = parseFloat(mapData[0].split("|")[0] || mapData[0]); - return [mapData, mapVersion]; + const mapVersionString = mapData[0].split("|")[0] || mapData[0] || ""; + return [mapData, mapVersionString]; } catch (error) { // map file can be compressed with gzip const uncompressedData = await uncompress(result); @@ -167,35 +165,36 @@ async function parseLoadedResult(result) { } function showUploadMessage(type, mapData, mapVersion) { - const archive = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "archived version"); - let message, title, canBeLoaded; + let message, title; if (type === "invalid") { - message = `The file does not look like a valid save file.
Please check the data format`; + message = "The file does not look like a valid save file.
Please check the data format"; title = "Invalid file"; - canBeLoaded = false; + } else if (type === "updated") { + parseLoadedData(mapData, mapVersion); + return; } else if (type === "ancient") { + const archive = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "archived version"); message = `The map version you are trying to load (${mapVersion}) is too old and cannot be updated to the current version.
Please keep using an ${archive}`; title = "Ancient file"; - canBeLoaded = false; } else if (type === "newer") { message = `The map version you are trying to load (${mapVersion}) is newer than the current version.
Please load the file in the appropriate version`; title = "Newer file"; - canBeLoaded = false; } else if (type === "outdated") { - INFO && console.info(`Loading map. Auto-update from ${mapVersion} to ${version}`); + INFO && console.info(`Loading map. Auto-updating from ${mapVersion} to ${VERSION}`); parseLoadedData(mapData, mapVersion); return; } alertMessage.innerHTML = message; - const buttons = { - OK: function () { - $(this).dialog("close"); - if (canBeLoaded) parseLoadedData(mapData, mapVersion); + $("#alert").dialog({ + title, + buttons: { + OK: function () { + $(this).dialog("close"); + } } - }; - $("#alert").dialog({title, buttons}); + }); } async function parseLoadedData(data, mapVersion) { @@ -380,6 +379,7 @@ async function parseLoadedData(data, mapVersion) { pack.rivers = data[32] ? JSON.parse(data[32]) : []; pack.markers = data[35] ? JSON.parse(data[35]) : []; pack.routes = data[37] ? JSON.parse(data[37]) : []; + pack.zones = data[38] ? JSON.parse(data[38]) : []; const cells = pack.cells; cells.biome = Uint8Array.from(data[16].split(",")); @@ -462,9 +462,8 @@ async function parseLoadedData(data, mapVersion) { { // dynamically import and run auto-update script - const versionNumber = parseFloat(params[0]); - const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.99.01"); - resolveVersionConflicts(versionNumber); + const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.100.00"); + resolveVersionConflicts(mapVersion); } // add custom heightmap color scheme if any @@ -709,7 +708,7 @@ async function parseLoadedData(data, mapVersion) { ERROR && console.error(error); clearMainTip(); - alertMessage.innerHTML = /* html */ `An error is occured on map loading. Select a different file to load,
generate a new random map or cancel the loading.
Map version: ${mapVersion}. Generator version: ${version}. + alertMessage.innerHTML = /* html */ `An error is occured on map loading. Select a different file to load,
generate a new random map or cancel the loading.
Map version: ${mapVersion}. Generator version: ${VERSION}.

${parseError(error)}

`; $("#alert").dialog({ diff --git a/modules/io/save.js b/modules/io/save.js index 73e6845f..96d1d981 100644 --- a/modules/io/save.js +++ b/modules/io/save.js @@ -41,7 +41,7 @@ function prepareMapData() { const date = new Date(); const dateString = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate(); const license = "File can be loaded in azgaar.github.io/Fantasy-Map-Generator"; - const params = [version, license, dateString, seed, graphWidth, graphHeight, mapId].join("|"); + const params = [VERSION, license, dateString, seed, graphWidth, graphHeight, mapId].join("|"); const settings = [ distanceUnitInput.value, distanceScale, @@ -100,6 +100,7 @@ function prepareMapData() { const markers = JSON.stringify(pack.markers); const cellRoutes = JSON.stringify(pack.cells.routes); const routes = JSON.stringify(pack.routes); + const zones = JSON.stringify(pack.zones); // store name array only if not the same as default const defaultNB = Names.getNameBases(); @@ -152,7 +153,8 @@ function prepareMapData() { fonts, markers, cellRoutes, - routes + routes, + zones ].join("\r\n"); return mapData; } diff --git a/modules/military-generator.js b/modules/military-generator.js index df2f2892..e5aacb4d 100644 --- a/modules/military-generator.js +++ b/modules/military-generator.js @@ -503,7 +503,9 @@ window.Military = (function () { : ""; const campaign = s.campaigns ? ra(s.campaigns) : null; - const year = campaign ? rand(campaign.start, campaign.end) : gauss(options.year - 100, 150, 1, options.year - 6); + const year = campaign + ? rand(campaign.start, campaign.end || options.year) + : gauss(options.year - 100, 150, 1, options.year - 6); const conflict = campaign ? ` during the ${campaign.name}` : ""; const legend = `Regiment was formed in ${year} ${options.era}${conflict}. ${station}${troops}`; notes.push({id: `regiment${s.i}-${r.i}`, name: `${r.icon} ${r.name}`, legend}); diff --git a/modules/submap.js b/modules/submap.js index 6a9593ca..8804db9d 100644 --- a/modules/submap.js +++ b/modules/submap.js @@ -310,7 +310,7 @@ window.Submap = (function () { stage("Redraw emblems."); drawEmblems(); stage("Regenerating Zones."); - addZones(); + Zones.generate(); Names.getMapName(); stage("Restoring Notes."); notes = parentMap.notes; diff --git a/modules/ui/burg-editor.js b/modules/ui/burg-editor.js index a90a11d8..94145e13 100644 --- a/modules/ui/burg-editor.js +++ b/modules/ui/burg-editor.js @@ -228,34 +228,26 @@ function editBurg(id) { const burgsToRemove = burgsInGroup.filter(b => !(pack.burgs[b].capital || pack.burgs[b].lock)); const capital = burgsToRemove.length < burgsInGroup.length; - alertMessage.innerHTML = /* html */ `Are you sure you want to remove ${ - 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, + confirmationDialog({ title: "Remove burg group", - buttons: { - Remove: function () { - $(this).dialog("close"); - $("#burgEditor").dialog("close"); - hideGroupSection(); - burgsToRemove.forEach(b => removeBurg(b)); + message: `Are you sure you want to remove ${ + 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 + }. This action cannot be reverted`, + confirm: "Remove", + onConfirm: () => { + $("#burgEditor").dialog("close"); + hideGroupSection(); + burgsToRemove.forEach(b => removeBurg(b)); - if (!basic && !capital) { - // entirely remove group - const labelG = document.querySelector("#burgLabels > #" + group.id); - const iconG = document.querySelector("#burgIcons > #" + group.id); - const anchorG = document.querySelector("#anchors > #" + group.id); - if (labelG) labelG.remove(); - if (iconG) iconG.remove(); - if (anchorG) anchorG.remove(); - } - }, - Cancel: function () { - $(this).dialog("close"); + if (!basic && !capital) { + const labelG = document.querySelector("#burgLabels > #" + group.id); + const iconG = document.querySelector("#burgIcons > #" + group.id); + const anchorG = document.querySelector("#anchors > #" + group.id); + if (labelG) labelG.remove(); + if (iconG) iconG.remove(); + if (anchorG) anchorG.remove(); } } }); @@ -509,19 +501,13 @@ function editBurg(id) { } }); } else { - 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(id); // see Editors module - $("#burgEditor").dialog("close"); - }, - Cancel: function () { - $(this).dialog("close"); - } + message: "Are you sure you want to remove the burg?
This action cannot be reverted", + confirm: "Remove", + onConfirm: () => { + removeBurg(id); // see Editors module + $("#burgEditor").dialog("close"); } }); } diff --git a/modules/ui/burgs-overview.js b/modules/ui/burgs-overview.js index f6b09a31..ae4d09dc 100644 --- a/modules/ui/burgs-overview.js +++ b/modules/ui/burgs-overview.js @@ -245,7 +245,7 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) { confirmationDialog({ title: "Remove burg", - message: "Are you sure you want to remove the burg? This actiove cannot be reverted", + message: "Are you sure you want to remove the burg?
This action cannot be reverted", confirm: "Remove", onConfirm: () => { removeBurg(burg); diff --git a/modules/ui/general.js b/modules/ui/general.js index f665902d..7fd7e4d5 100644 --- a/modules/ui/general.js +++ b/modules/ui/general.js @@ -168,6 +168,7 @@ function showMapTooltip(point, e, i, g) { if (burgsOverview?.offsetParent) highlightEditorLine(burgsOverview, burg, 5000); return; } + if (group === "labels") return tip("Click to edit the Label"); if (group === "markers") return tip("Click to edit the Marker. Hold Shift to not close the assosiated note"); @@ -199,9 +200,11 @@ function showMapTooltip(point, e, i, g) { if (group === "coastline") return tip("Click to edit the coastline"); if (group === "zones") { - const zone = path[path.length - 8]; - tip(zone.dataset.description); - if (zonesEditor?.offsetParent) highlightEditorLine(zonesEditor, zone.id, 5000); + const element = path[path.length - 8]; + const zoneId = +element.dataset.id; + const zone = pack.zones.find(zone => zone.i === zoneId); + tip(zone.name); + if (zonesEditor?.offsetParent) highlightEditorLine(zonesEditor, zoneId, 5000); return; } diff --git a/modules/ui/heightmap-editor.js b/modules/ui/heightmap-editor.js index ac270299..a88c698c 100644 --- a/modules/ui/heightmap-editor.js +++ b/modules/ui/heightmap-editor.js @@ -261,7 +261,7 @@ function editHeightmap(options) { Military.generate(); Markers.generate(); - addZones(); + Zones.generate(); TIME && console.timeEnd("regenerateErasedData"); INFO && console.groupEnd("Edit Heightmap"); } diff --git a/modules/ui/hotkeys.js b/modules/ui/hotkeys.js index 3367dbb5..1b563d52 100644 --- a/modules/ui/hotkeys.js +++ b/modules/ui/hotkeys.js @@ -18,10 +18,9 @@ function handleKeyup(event) { event.stopPropagation(); - const {code, key, ctrlKey, metaKey, shiftKey, altKey} = event; + const {code, key, ctrlKey, metaKey, shiftKey} = 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(); @@ -60,11 +59,6 @@ function handleKeyup(event) { else if (key === "#") toggleAddRiver(); else if (key === "$") createRoute(); 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(); diff --git a/modules/ui/labels-editor.js b/modules/ui/labels-editor.js index d19de7ae..809be210 100644 --- a/modules/ui/labels-editor.js +++ b/modules/ui/labels-editor.js @@ -26,28 +26,32 @@ function editLabel() { modules.editLabel = true; // add listeners - document.getElementById("labelGroupShow").addEventListener("click", showGroupSection); - document.getElementById("labelGroupHide").addEventListener("click", hideGroupSection); - document.getElementById("labelGroupSelect").addEventListener("click", changeGroup); - document.getElementById("labelGroupInput").addEventListener("change", createNewGroup); - document.getElementById("labelGroupNew").addEventListener("click", toggleNewGroupInput); - document.getElementById("labelGroupRemove").addEventListener("click", removeLabelsGroup); + byId("labelGroupShow").on("click", showGroupSection); + byId("labelGroupHide").on("click", hideGroupSection); + byId("labelGroupSelect").on("click", changeGroup); + byId("labelGroupInput").on("change", createNewGroup); + byId("labelGroupNew").on("click", toggleNewGroupInput); + byId("labelGroupRemove").on("click", removeLabelsGroup); - document.getElementById("labelTextShow").addEventListener("click", showTextSection); - document.getElementById("labelTextHide").addEventListener("click", hideTextSection); - document.getElementById("labelText").addEventListener("input", changeText); - document.getElementById("labelTextRandom").addEventListener("click", generateRandomName); + byId("labelTextShow").on("click", showTextSection); + byId("labelTextHide").on("click", hideTextSection); + byId("labelText").on("input", changeText); + byId("labelTextRandom").on("click", generateRandomName); - document.getElementById("labelEditStyle").addEventListener("click", editGroupStyle); + byId("labelEditStyle").on("click", editGroupStyle); - document.getElementById("labelSizeShow").addEventListener("click", showSizeSection); - document.getElementById("labelSizeHide").addEventListener("click", hideSizeSection); - document.getElementById("labelStartOffset").addEventListener("input", changeStartOffset); - document.getElementById("labelRelativeSize").addEventListener("input", changeRelativeSize); + byId("labelSizeShow").on("click", showSizeSection); + byId("labelSizeHide").on("click", hideSizeSection); + byId("labelStartOffset").on("input", changeStartOffset); + byId("labelRelativeSize").on("input", changeRelativeSize); - document.getElementById("labelAlign").addEventListener("click", editLabelAlign); - document.getElementById("labelLegend").addEventListener("click", editLabelLegend); - document.getElementById("labelRemoveSingle").addEventListener("click", removeLabel); + byId("labelLetterSpacingShow").on("click", showLetterSpacingSection); + byId("labelLetterSpacingHide").on("click", hideLetterSpacingSection); + byId("labelLetterSpacingSize").on("input", changeLetterSpacingSize); + + byId("labelAlign").on("click", editLabelAlign); + byId("labelLegend").on("click", editLabelLegend); + byId("labelRemoveSingle").on("click", removeLabel); function showEditorTips() { showMainTip(); @@ -62,12 +66,12 @@ function editLabel() { const group = text.parentNode.id; if (group === "states" || group === "burgLabels") { - document.getElementById("labelGroupShow").style.display = "none"; + byId("labelGroupShow").style.display = "none"; return; } hideGroupSection(); - const select = document.getElementById("labelGroupSelect"); + const select = byId("labelGroupSelect"); select.options.length = 0; // remove all options labels.selectAll(":scope > g").each(function () { @@ -78,17 +82,17 @@ function editLabel() { } function updateValues(textPath) { - document.getElementById("labelText").value = [...textPath.querySelectorAll("tspan")] - .map(tspan => tspan.textContent) - .join("|"); - document.getElementById("labelStartOffset").value = parseFloat(textPath.getAttribute("startOffset")); - document.getElementById("labelRelativeSize").value = parseFloat(textPath.getAttribute("font-size")); + byId("labelText").value = [...textPath.querySelectorAll("tspan")].map(tspan => tspan.textContent).join("|"); + byId("labelStartOffset").value = parseFloat(textPath.getAttribute("startOffset")); + byId("labelRelativeSize").value = parseFloat(textPath.getAttribute("font-size")); + let letterSpacingSize = textPath.getAttribute("letter-spacing") ? textPath.getAttribute("letter-spacing") : 0; + byId("labelLetterSpacingSize").value = parseFloat(letterSpacingSize); } function drawControlPointsAndLine() { debug.select("#controlPoints").remove(); debug.append("g").attr("id", "controlPoints").attr("transform", elSelected.attr("transform")); - const path = document.getElementById("textPath_" + elSelected.attr("id")); + const path = byId("textPath_" + elSelected.attr("id")); debug.select("#controlPoints").append("path").attr("d", path.getAttribute("d")).on("click", addInterimControlPoint); const l = path.getTotalLength(); if (!l) return; @@ -117,7 +121,7 @@ function editLabel() { } function redrawLabelPath() { - const path = document.getElementById("textPath_" + elSelected.attr("id")); + const path = byId("textPath_" + elSelected.attr("id")); lineGen.curve(d3.curveBundle.beta(1)); const points = []; debug @@ -188,19 +192,19 @@ function editLabel() { function showGroupSection() { document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "none")); - document.getElementById("labelGroupSection").style.display = "inline-block"; + byId("labelGroupSection").style.display = "inline-block"; } function hideGroupSection() { 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"; + byId("labelGroupSection").style.display = "none"; + byId("labelGroupInput").style.display = "none"; + byId("labelGroupInput").value = ""; + byId("labelGroupSelect").style.display = "inline-block"; } function changeGroup() { - document.getElementById(this.value).appendChild(elSelected.node()); + byId(this.value).appendChild(elSelected.node()); } function toggleNewGroupInput() { @@ -224,7 +228,7 @@ function editLabel() { .replace(/ /g, "_") .replace(/[^\w\s]/gi, ""); - if (document.getElementById(group)) { + if (byId(group)) { tip("Element with this id already exists. Please provide a unique name", false, "error"); return; } @@ -237,22 +241,22 @@ function editLabel() { // just rename if only 1 element left const oldGroup = elSelected.node().parentNode; if (oldGroup !== "states" && oldGroup !== "addedLabels" && oldGroup.childElementCount === 1) { - document.getElementById("labelGroupSelect").selectedOptions[0].remove(); - document.getElementById("labelGroupSelect").options.add(new Option(group, group, false, true)); + byId("labelGroupSelect").selectedOptions[0].remove(); + byId("labelGroupSelect").options.add(new Option(group, group, false, true)); oldGroup.id = group; toggleNewGroupInput(); - document.getElementById("labelGroupInput").value = ""; + byId("labelGroupInput").value = ""; return; } const newGroup = elSelected.node().parentNode.cloneNode(false); - document.getElementById("labels").appendChild(newGroup); + byId("labels").appendChild(newGroup); newGroup.id = group; - document.getElementById("labelGroupSelect").options.add(new Option(group, group, false, true)); - document.getElementById(group).appendChild(elSelected.node()); + byId("labelGroupSelect").options.add(new Option(group, group, false, true)); + byId(group).appendChild(elSelected.node()); toggleNewGroupInput(); - document.getElementById("labelGroupInput").value = ""; + byId("labelGroupInput").value = ""; } function removeLabelsGroup() { @@ -275,7 +279,7 @@ function editLabel() { .select("#" + group) .selectAll("text") .each(function () { - document.getElementById("textPath_" + this.id).remove(); + byId("textPath_" + this.id).remove(); this.remove(); }); if (!basic) labels.select("#" + group).remove(); @@ -289,16 +293,16 @@ function editLabel() { function showTextSection() { document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "none")); - document.getElementById("labelTextSection").style.display = "inline-block"; + byId("labelTextSection").style.display = "inline-block"; } function hideTextSection() { document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "inline-block")); - document.getElementById("labelTextSection").style.display = "none"; + byId("labelTextSection").style.display = "none"; } function changeText() { - const input = document.getElementById("labelText").value; + const input = byId("labelText").value; const el = elSelected.select("textPath").node(); const lines = input.split("|"); @@ -323,7 +327,7 @@ function editLabel() { const culture = pack.cells.culture[cell]; name = Names.getCulture(culture); } - document.getElementById("labelText").value = name; + byId("labelText").value = name; changeText(); } @@ -334,12 +338,22 @@ function editLabel() { function showSizeSection() { document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "none")); - document.getElementById("labelSizeSection").style.display = "inline-block"; + byId("labelSizeSection").style.display = "inline-block"; } function hideSizeSection() { document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "inline-block")); - document.getElementById("labelSizeSection").style.display = "none"; + byId("labelSizeSection").style.display = "none"; + } + + function showLetterSpacingSection() { + document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "none")); + byId("labelLetterSpacingSection").style.display = "inline-block"; + } + + function hideLetterSpacingSection() { + document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "inline-block")); + byId("labelLetterSpacingSection").style.display = "none"; } function changeStartOffset() { @@ -353,6 +367,12 @@ function editLabel() { changeText(); } + function changeLetterSpacingSize() { + elSelected.select("textPath").attr("letter-spacing", this.value + "px"); + tip("Label letter-spacing size: " + this.value + "px"); + changeText(); + } + function editLabelAlign() { const bbox = elSelected.node().getBBox(); const c = [bbox.x + bbox.width / 2, bbox.y + bbox.height / 2]; diff --git a/modules/ui/layers.js b/modules/ui/layers.js index 5572ef78..d717792c 100644 --- a/modules/ui/layers.js +++ b/modules/ui/layers.js @@ -181,6 +181,7 @@ function restoreLayers() { if (layerIsOn("toggleIce")) drawIce(); if (layerIsOn("toggleEmblems")) drawEmblems(); if (layerIsOn("toggleMarkers")) drawMarkers(); + if (layerIsOn("toggleZones")) drawZones(); // some layers are rendered each time, remove them if they are not on if (!layerIsOn("toggleBorders")) borders.selectAll("path").remove(); @@ -1872,18 +1873,29 @@ function fitScaleBar(scaleBar, fullWidth, fullHeight) { function toggleZones(event) { if (!layerIsOn("toggleZones")) { turnButtonOn("toggleZones"); - $("#zones").fadeIn(); + drawZones(); if (event && isCtrlClick(event)) editStyle("zones"); } else { - if (event && isCtrlClick(event)) { - editStyle("zones"); - return; - } + if (event && isCtrlClick(event)) return editStyle("zones"); turnButtonOff("toggleZones"); - $("#zones").fadeOut(); + zones.selectAll("*").remove(); } } +function drawZones() { + const filterBy = byId("zonesFilterType").value; + const isFiltered = filterBy && filterBy !== "all"; + const visibleZones = pack.zones.filter( + ({hidden, cells, type}) => !hidden && cells.length && (!isFiltered || type === filterBy) + ); + zones.html(visibleZones.map(drawZone).join("")); +} + +function drawZone({i, cells, type, color}) { + const path = getVertexPath(cells); + return ``; +} + function toggleEmblems(event) { if (!layerIsOn("toggleEmblems")) { turnButtonOn("toggleEmblems"); diff --git a/modules/ui/options.js b/modules/ui/options.js index 6d0a9fef..b7b643a1 100644 --- a/modules/ui/options.js +++ b/modules/ui/options.js @@ -782,7 +782,7 @@ function showExportPane() { } async function exportToJson(type) { - const {exportToJson} = await import("../dynamic/export-json.js?v=1.97.08"); + const {exportToJson} = await import("../dynamic/export-json.js?v=1.100.00"); exportToJson(type); } diff --git a/modules/ui/routes-editor.js b/modules/ui/routes-editor.js index 82f13e44..4ea119fb 100644 --- a/modules/ui/routes-editor.js +++ b/modules/ui/routes-editor.js @@ -389,20 +389,13 @@ function editRoute(id) { } function removeRoute() { - alertMessage.innerHTML = "Are you sure you want to remove the route"; - $("#alert").dialog({ - resizable: false, - width: "22em", + confirmationDialog({ title: "Remove route", - buttons: { - Remove: function () { - Routes.remove(getRoute()); - $(this).dialog("close"); - $("#routeEditor").dialog("close"); - }, - Cancel: function () { - $(this).dialog("close"); - } + message: "Are you sure you want to remove the route?
This action cannot be reverted", + confirm: "Remove", + onConfirm: () => { + Routes.remove(getRoute()); + $("#routeEditor").dialog("close"); } }); } diff --git a/modules/ui/routes-overview.js b/modules/ui/routes-overview.js index 3a8cc5fa..252b2275 100644 --- a/modules/ui/routes-overview.js +++ b/modules/ui/routes-overview.js @@ -144,22 +144,14 @@ function overviewRoutes() { function triggerRouteRemove() { const routeId = +this.parentNode.dataset.id; - - alertMessage.innerHTML = `Are you sure you want to remove the route?`; - $("#alert").dialog({ - resizable: false, - width: "22em", + confirmationDialog({ title: "Remove route", - buttons: { - Remove: function () { - const route = pack.routes.find(r => r.i === routeId); - Routes.remove(route); - routesOverviewAddLines(); - $(this).dialog("close"); - }, - Cancel: function () { - $(this).dialog("close"); - } + message: "Are you sure you want to remove the route?
This action cannot be reverted", + confirm: "Remove", + onConfirm: () => { + const route = pack.routes.find(r => r.i === routeId); + Routes.remove(route); + routesOverviewAddLines(); } }); } diff --git a/modules/ui/stylePresets.js b/modules/ui/style-presets.js similarity index 96% rename from modules/ui/stylePresets.js rename to modules/ui/style-presets.js index 4ce4b2a5..01626205 100644 --- a/modules/ui/stylePresets.js +++ b/modules/ui/style-presets.js @@ -64,7 +64,7 @@ async function getStylePreset(desiredPreset) { async function fetchSystemPreset(preset) { try { - const res = await fetch(`./styles/${preset}.json?v=${version}`); + const res = await fetch(`./styles/${preset}.json?v=${VERSION}`); return await res.json(); } catch (err) { throw new Error("Cannot fetch style preset", preset); @@ -271,7 +271,15 @@ function addStylePreset() { "data-columns" ], "#legendBox": ["fill", "fill-opacity"], - "#burgLabels > #cities": ["opacity", "fill", "text-shadow", "data-size", "font-size", "font-family"], + "#burgLabels > #cities": [ + "opacity", + "fill", + "text-shadow", + "letter-spacing", + "data-size", + "font-size", + "font-family" + ], "#burgIcons > #cities": [ "opacity", "fill", @@ -283,7 +291,15 @@ function addStylePreset() { "stroke-linecap" ], "#anchors > #cities": ["opacity", "fill", "size", "stroke", "stroke-width"], - "#burgLabels > #towns": ["opacity", "fill", "text-shadow", "data-size", "font-size", "font-family"], + "#burgLabels > #towns": [ + "opacity", + "fill", + "text-shadow", + "letter-spacing", + "data-size", + "font-size", + "font-family" + ], "#burgIcons > #towns": [ "opacity", "fill", @@ -301,6 +317,7 @@ function addStylePreset() { "stroke", "stroke-width", "text-shadow", + "letter-spacing", "data-size", "font-size", "font-family", @@ -312,6 +329,7 @@ function addStylePreset() { "stroke", "stroke-width", "text-shadow", + "letter-spacing", "data-size", "font-size", "font-family", diff --git a/modules/ui/style.js b/modules/ui/style.js index aa481ad1..4f003bb1 100644 --- a/modules/ui/style.js +++ b/modules/ui/style.js @@ -18,7 +18,7 @@ } // store some style inputs as options -styleElements.addEventListener("change", function (ev) { +styleElements.on("change", function (ev) { if (ev.target.dataset.stored) lock(ev.target.dataset.stored); }); @@ -71,7 +71,7 @@ function getColorScheme(scheme = "bright") { } // Toggle style sections on element select -styleElementSelect.addEventListener("change", selectStyleElement); +styleElementSelect.on("change", selectStyleElement); function selectStyleElement() { const styleElement = styleElementSelect.value; @@ -147,15 +147,17 @@ function selectStyleElement() { // clipping if ( [ - "cells", - "gridOverlay", - "coordinates", - "compass", - "terrain", - "temperature", - "routes", - "texture", "biomes", + "cells", + "compass", + "coordinates", + "gridOverlay", + "population", + "prec", + "routes", + "temperature", + "terrain", + "texture", "zones" ].includes(styleElement) ) { @@ -237,6 +239,7 @@ function selectStyleElement() { styleFill.style.display = "block"; styleStroke.style.display = "block"; styleStrokeWidth.style.display = "block"; + styleLetterSpacing.style.display = "block"; styleShadow.style.display = "block"; styleSize.style.display = "block"; @@ -244,6 +247,7 @@ function selectStyleElement() { styleFillInput.value = styleFillOutput.value = el.attr("fill") || "#3e3e4b"; styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke") || "#3a3a3a"; styleStrokeWidthInput.value = el.attr("stroke-width") || 0; + styleLetterSpacingInput.value = el.attr("letter-spacing") || 0; styleShadowInput.value = el.style("text-shadow") || "white 0 0 4px"; styleFont.style.display = "block"; @@ -405,7 +409,7 @@ function selectStyleElement() { } // Handle style inputs change -styleGroupSelect.addEventListener("change", selectStyleElement); +styleGroupSelect.on("change", selectStyleElement); function getEl() { const el = styleElementSelect.value; @@ -414,42 +418,46 @@ function getEl() { else return svg.select("#" + el).select("#" + g); } -styleFillInput.addEventListener("input", function () { +styleFillInput.on("input", function () { styleFillOutput.value = this.value; getEl().attr("fill", this.value); }); -styleStrokeInput.addEventListener("input", function () { +styleStrokeInput.on("input", function () { styleStrokeOutput.value = this.value; getEl().attr("stroke", this.value); if (styleElementSelect.value === "gridOverlay" && layerIsOn("toggleGrid")) drawGrid(); }); -styleStrokeWidthInput.addEventListener("input", e => { +styleStrokeWidthInput.on("input", e => { getEl().attr("stroke-width", e.target.value); if (styleElementSelect.value === "gridOverlay" && layerIsOn("toggleGrid")) drawGrid(); }); -styleStrokeDasharrayInput.addEventListener("input", function () { +styleLetterSpacingInput.on("input", e => { + getEl().attr("letter-spacing", e.target.value); +}); + +styleStrokeDasharrayInput.on("input", function () { getEl().attr("stroke-dasharray", this.value); if (styleElementSelect.value === "gridOverlay" && layerIsOn("toggleGrid")) drawGrid(); }); -styleStrokeLinecapInput.addEventListener("change", function () { +styleStrokeLinecapInput.on("change", function () { getEl().attr("stroke-linecap", this.value); if (styleElementSelect.value === "gridOverlay" && layerIsOn("toggleGrid")) drawGrid(); }); -styleOpacityInput.addEventListener("input", e => { +styleOpacityInput.on("input", e => { getEl().attr("opacity", e.target.value); }); -styleFilterInput.addEventListener("change", function () { +styleFilterInput.on("change", function () { if (styleGroupSelect.value === "ocean") return oceanLayers.attr("filter", this.value); getEl().attr("filter", this.value); }); -styleTextureInput.addEventListener("change", function () { +styleTextureInput.on("change", function () { changeTexture(this.value); }); @@ -468,7 +476,7 @@ function updateTextureSelectValue(href) { } } -styleTextureShiftX.addEventListener("input", function () { +styleTextureShiftX.on("input", function () { texture.attr("data-x", this.value); texture .select("image") @@ -476,7 +484,7 @@ styleTextureShiftX.addEventListener("input", function () { .attr("width", graphWidth - this.valueAsNumber); }); -styleTextureShiftY.addEventListener("input", function () { +styleTextureShiftY.on("input", function () { texture.attr("data-y", this.value); texture .select("image") @@ -484,17 +492,17 @@ styleTextureShiftY.addEventListener("input", function () { .attr("height", graphHeight - this.valueAsNumber); }); -styleClippingInput.addEventListener("change", function () { +styleClippingInput.on("change", function () { getEl().attr("mask", this.value); }); -styleGridType.addEventListener("change", function () { +styleGridType.on("change", function () { getEl().attr("type", this.value); if (layerIsOn("toggleGrid")) drawGrid(); calculateFriendlyGridSize(); }); -styleGridScale.addEventListener("input", function () { +styleGridScale.on("input", function () { getEl().attr("scale", this.value); if (layerIsOn("toggleGrid")) drawGrid(); calculateFriendlyGridSize(); @@ -506,52 +514,52 @@ function calculateFriendlyGridSize() { styleGridSizeFriendly.value = friendly; } -styleGridShiftX.addEventListener("input", function () { +styleGridShiftX.on("input", function () { getEl().attr("dx", this.value); if (layerIsOn("toggleGrid")) drawGrid(); }); -styleGridShiftY.addEventListener("input", function () { +styleGridShiftY.on("input", function () { getEl().attr("dy", this.value); if (layerIsOn("toggleGrid")) drawGrid(); }); -styleRescaleMarkers.addEventListener("change", function () { +styleRescaleMarkers.on("change", function () { markers.attr("rescale", +this.checked); invokeActiveZooming(); }); -styleCoastlineAuto.addEventListener("change", function () { +styleCoastlineAuto.on("change", function () { coastline.select("#sea_island").attr("auto-filter", +this.checked); styleFilter.style.display = this.checked ? "none" : "block"; invokeActiveZooming(); }); -styleOceanFill.addEventListener("input", function () { +styleOceanFill.on("input", function () { oceanLayers.select("rect").attr("fill", this.value); styleOceanFillOutput.value = this.value; }); -styleOceanPattern.addEventListener("change", function () { +styleOceanPattern.on("change", function () { byId("oceanicPattern")?.setAttribute("href", this.value); }); -styleOceanPatternOpacity.addEventListener("input", e => { +styleOceanPatternOpacity.on("input", e => { byId("oceanicPattern").setAttribute("opacity", e.target.value); }); -outlineLayers.addEventListener("change", function () { +outlineLayers.on("change", function () { oceanLayers.selectAll("path").remove(); oceanLayers.attr("layers", this.value); OceanLayers(); }); -styleHeightmapScheme.addEventListener("change", function () { +styleHeightmapScheme.on("change", function () { getEl().attr("scheme", this.value); drawHeightmap(); }); -openCreateHeightmapSchemeButton.addEventListener("click", function () { +openCreateHeightmapSchemeButton.on("click", function () { // start with current scheme const scheme = getEl().attr("scheme"); this.dataset.stops = scheme.startsWith("#") @@ -670,97 +678,97 @@ openCreateHeightmapSchemeButton.addEventListener("click", function () { }); }); -styleHeightmapRenderOcean.addEventListener("change", e => { +styleHeightmapRenderOcean.on("change", e => { const checked = +e.target.checked; getEl().attr("data-render", checked); drawHeightmap(); }); -styleHeightmapTerracing.addEventListener("input", e => { +styleHeightmapTerracing.on("input", e => { getEl().attr("terracing", e.target.value); drawHeightmap(); }); -styleHeightmapSkip.addEventListener("input", e => { +styleHeightmapSkip.on("input", e => { getEl().attr("skip", e.target.value); drawHeightmap(); }); -styleHeightmapSimplification.addEventListener("input", e => { +styleHeightmapSimplification.on("input", e => { getEl().attr("relax", e.target.value); drawHeightmap(); }); -styleHeightmapCurve.addEventListener("change", e => { +styleHeightmapCurve.on("change", e => { getEl().attr("curve", e.target.value); drawHeightmap(); }); -styleReliefSet.addEventListener("change", e => { +styleReliefSet.on("change", e => { terrain.attr("set", e.target.value); ReliefIcons.draw(); if (!layerIsOn("toggleRelief")) toggleRelief(); }); -styleReliefSize.addEventListener("change", e => { +styleReliefSize.on("change", e => { terrain.attr("size", e.target.value); ReliefIcons.draw(); if (!layerIsOn("toggleRelief")) toggleRelief(); }); -styleReliefDensity.addEventListener("change", e => { +styleReliefDensity.on("change", e => { terrain.attr("density", e.target.value); ReliefIcons.draw(); if (!layerIsOn("toggleRelief")) toggleRelief(); }); -styleTemperatureFillOpacityInput.addEventListener("input", e => { +styleTemperatureFillOpacityInput.on("input", e => { temperature.attr("fill-opacity", e.target.value); }); -styleTemperatureFontSizeInput.addEventListener("input", e => { +styleTemperatureFontSizeInput.on("input", e => { temperature.attr("font-size", e.target.value + "px"); }); -styleTemperatureFillInput.addEventListener("input", e => { +styleTemperatureFillInput.on("input", e => { temperature.attr("fill", e.target.value); styleTemperatureFillOutput.value = e.target.value; }); -stylePopulationRuralStrokeInput.addEventListener("input", e => { +stylePopulationRuralStrokeInput.on("input", e => { population.select("#rural").attr("stroke", e.target.value); stylePopulationRuralStrokeOutput.value = e.target.value; }); -stylePopulationUrbanStrokeInput.addEventListener("input", e => { +stylePopulationUrbanStrokeInput.on("input", e => { population.select("#urban").attr("stroke", e.target.value); stylePopulationUrbanStrokeOutput.value = e.target.value; }); -styleCompassSizeInput.addEventListener("input", shiftCompass); -styleCompassShiftX.addEventListener("input", shiftCompass); -styleCompassShiftY.addEventListener("input", shiftCompass); +styleCompassSizeInput.on("input", shiftCompass); +styleCompassShiftX.on("input", shiftCompass); +styleCompassShiftY.on("input", shiftCompass); function shiftCompass() { const tr = `translate(${styleCompassShiftX.value} ${styleCompassShiftY.value}) scale(${styleCompassSizeInput.value})`; compass.select("use").attr("transform", tr); } -styleLegendColItems.addEventListener("input", e => { +styleLegendColItems.on("input", e => { legend.select("#legendBox").attr("data-columns", e.target.value); redrawLegend(); }); -styleLegendBack.addEventListener("input", e => { +styleLegendBack.on("input", e => { styleLegendBackOutput.value = e.target.value; legend.select("#legendBox").attr("fill", e.target.value); }); -styleLegendOpacity.addEventListener("input", e => { +styleLegendOpacity.on("input", e => { legend.select("#legendBox").attr("fill-opacity", e.target.value); }); -styleSelectFont.addEventListener("change", changeFont); +styleSelectFont.on("change", changeFont); function changeFont() { const family = styleSelectFont.value; getEl().attr("font-family", family); @@ -768,11 +776,11 @@ function changeFont() { if (styleElementSelect.value === "legend") redrawLegend(); } -styleShadowInput.addEventListener("input", function () { +styleShadowInput.on("input", function () { getEl().style("text-shadow", this.value); }); -styleFontAdd.addEventListener("click", function () { +styleFontAdd.on("click", function () { addFontNameInput.value = ""; addFontURLInput.value = ""; @@ -809,20 +817,20 @@ styleFontAdd.addEventListener("click", function () { }); }); -addFontMethod.addEventListener("change", function () { +addFontMethod.on("change", function () { addFontURLInput.style.display = this.value === "fontURL" ? "inline" : "none"; }); -styleFontSize.addEventListener("change", function () { +styleFontSize.on("change", function () { changeFontSize(getEl(), +this.value); }); -styleFontPlus.addEventListener("click", function () { +styleFontPlus.on("click", function () { const current = +styleFontSize.value || 12; changeFontSize(getEl(), Math.min(current + 1, 999)); }); -styleFontMinus.addEventListener("click", function () { +styleFontMinus.on("click", function () { const current = +styleFontSize.value || 12; changeFontSize(getEl(), Math.max(current - 1, 1)); }); @@ -845,16 +853,16 @@ function changeFontSize(el, size) { if (styleElementSelect.value === "legend") redrawLegend(); } -styleRadiusInput.addEventListener("change", function () { +styleRadiusInput.on("change", function () { changeRadius(+this.value); }); -styleRadiusPlus.addEventListener("click", function () { +styleRadiusPlus.on("click", function () { const size = Math.max(rn(getEl().attr("size") * 1.1, 2), 0.2); changeRadius(size); }); -styleRadiusMinus.addEventListener("click", function () { +styleRadiusMinus.on("click", function () { const size = Math.max(rn(getEl().attr("size") * 0.9, 2), 0.2); changeRadius(size); }); @@ -876,16 +884,16 @@ function changeRadius(size, group) { changeIconSize(size * 2, g); // change also anchor icons } -styleIconSizeInput.addEventListener("change", function () { +styleIconSizeInput.on("change", function () { changeIconSize(+this.value); }); -styleIconSizePlus.addEventListener("click", function () { +styleIconSizePlus.on("click", function () { const size = Math.max(rn(getEl().attr("size") * 1.1, 2), 0.2); changeIconSize(size); }); -styleIconSizeMinus.addEventListener("click", function () { +styleIconSizeMinus.on("click", function () { const size = Math.max(rn(getEl().attr("size") * 0.9, 2), 0.2); changeIconSize(size); }); @@ -910,34 +918,34 @@ function changeIconSize(size, group) { styleIconSizeInput.value = size; } -styleStatesBodyOpacity.addEventListener("input", e => { +styleStatesBodyOpacity.on("input", e => { statesBody.attr("opacity", e.target.value); }); -styleStatesBodyFilter.addEventListener("change", function () { +styleStatesBodyFilter.on("change", function () { statesBody.attr("filter", this.value); }); -styleStatesHaloWidth.addEventListener("input", e => { +styleStatesHaloWidth.on("input", e => { const value = e.target.value; statesHalo.attr("data-width", value).attr("stroke-width", value); }); -styleStatesHaloOpacity.addEventListener("input", e => { +styleStatesHaloOpacity.on("input", e => { statesHalo.attr("opacity", e.target.value); }); -styleStatesHaloBlur.addEventListener("input", e => { +styleStatesHaloBlur.on("input", e => { const value = Number(e.target.value); const blur = value > 0 ? `blur(${value}px)` : null; statesHalo.attr("filter", blur); }); -styleArmiesFillOpacity.addEventListener("input", e => { +styleArmiesFillOpacity.on("input", e => { armies.attr("fill-opacity", e.target.value); }); -styleArmiesSize.addEventListener("input", e => { +styleArmiesSize.on("input", e => { const value = Number(e.target.value); armies.attr("box-size", value).attr("font-size", value * 2); @@ -948,17 +956,17 @@ styleArmiesSize.addEventListener("input", e => { }); }); -emblemsStateSizeInput.addEventListener("change", e => { +emblemsStateSizeInput.on("change", e => { emblems.select("#stateEmblems").attr("data-size", e.target.value); drawEmblems(); }); -emblemsProvinceSizeInput.addEventListener("change", e => { +emblemsProvinceSizeInput.on("change", e => { emblems.select("#provinceEmblems").attr("data-size", e.target.value); drawEmblems(); }); -emblemsBurgSizeInput.addEventListener("change", e => { +emblemsBurgSizeInput.on("change", e => { emblems.select("#burgEmblems").attr("data-size", e.target.value); drawEmblems(); }); @@ -1013,7 +1021,7 @@ Object.keys(vignettePresets).forEach(preset => { styleVignettePreset.options.add(new Option(preset, preset, false, false)); }); -styleVignettePreset.addEventListener("change", function () { +styleVignettePreset.on("change", function () { const attributes = JSON.parse(vignettePresets[this.value]); for (const selector in attributes) { @@ -1045,35 +1053,35 @@ styleVignettePreset.addEventListener("change", function () { } }); -styleVignetteX.addEventListener("input", e => { +styleVignetteX.on("input", e => { byId("vignette-rect")?.setAttribute("x", `${e.target.value}%`); }); -styleVignetteWidth.addEventListener("input", e => { +styleVignetteWidth.on("input", e => { byId("vignette-rect")?.setAttribute("width", `${e.target.value}%`); }); -styleVignetteY.addEventListener("input", e => { +styleVignetteY.on("input", e => { byId("vignette-rect")?.setAttribute("y", `${e.target.value}%`); }); -styleVignetteHeight.addEventListener("input", e => { +styleVignetteHeight.on("input", e => { byId("vignette-rect")?.setAttribute("height", `${e.target.value}%`); }); -styleVignetteRx.addEventListener("input", e => { +styleVignetteRx.on("input", e => { byId("vignette-rect")?.setAttribute("rx", `${e.target.value}%`); }); -styleVignetteRy.addEventListener("input", e => { +styleVignetteRy.on("input", e => { byId("vignette-rect")?.setAttribute("ry", `${e.target.value}%`); }); -styleVignetteBlur.addEventListener("input", e => { +styleVignetteBlur.on("input", e => { byId("vignette-rect")?.setAttribute("filter", `blur(${e.target.value}px)`); }); -styleScaleBar.addEventListener("input", function (event) { +styleScaleBar.on("input", function (event) { const scaleBarBack = scaleBar.select("#scaleBarBack"); if (!scaleBarBack.size()) return; @@ -1153,7 +1161,7 @@ function updateElements() { } // GLOBAL FILTERS -mapFilters.addEventListener("click", applyMapFilter); +mapFilters.on("click", applyMapFilter); function applyMapFilter(event) { if (event.target.tagName !== "BUTTON") return; const button = event.target; diff --git a/modules/ui/submap.js b/modules/ui/submap.js index d5e36fcc..7b9dab06 100644 --- a/modules/ui/submap.js +++ b/modules/ui/submap.js @@ -159,18 +159,6 @@ window.UISubmap = (function () { return canvas; } - // currently unused alternative to PNG version - async function loadPreviewSVG($container, w, h) { - $container.innerHTML = /*html*/ ` - - - - - - `; - return byId("submapPreviewSVG"); - } - // Resample the whole map to different cell resolution or shape const resampleCurrentMap = debounce(function () { WARN && console.warn("Resampling current map"); diff --git a/modules/ui/tools.js b/modules/ui/tools.js index 9c75ac20..9dce15c5 100644 --- a/modules/ui/tools.js +++ b/modules/ui/tools.js @@ -167,9 +167,9 @@ function regenerateStates() { Military.generate(); if (layerIsOn("toggleEmblems")) drawEmblems(); - if (document.getElementById("burgsOverviewRefresh")?.offsetParent) burgsOverviewRefresh.click(); - if (document.getElementById("statesEditorRefresh")?.offsetParent) statesEditorRefresh.click(); - if (document.getElementById("militaryOverviewRefresh")?.offsetParent) militaryOverviewRefresh.click(); + if (byId("burgsOverviewRefresh")?.offsetParent) burgsOverviewRefresh.click(); + if (byId("statesEditorRefresh")?.offsetParent) statesEditorRefresh.click(); + if (byId("militaryOverviewRefresh")?.offsetParent) militaryOverviewRefresh.click(); } function recreateStates() { @@ -445,8 +445,8 @@ function regenerateBurgs() { emblems.selectAll("use").remove(); if (layerIsOn("toggleEmblems")) drawEmblems(); - if (document.getElementById("burgsOverviewRefresh")?.offsetParent) burgsOverviewRefresh.click(); - if (document.getElementById("statesEditorRefresh")?.offsetParent) statesEditorRefresh.click(); + if (byId("burgsOverviewRefresh")?.offsetParent) burgsOverviewRefresh.click(); + if (byId("statesEditorRefresh")?.offsetParent) statesEditorRefresh.click(); } function regenerateEmblems() { @@ -521,7 +521,7 @@ function regenerateCultures() { function regenerateMilitary() { Military.generate(); if (!layerIsOn("toggleMilitary")) toggleMilitary(); - if (document.getElementById("militaryOverviewRefresh").offsetParent) militaryOverviewRefresh.click(); + if (byId("militaryOverviewRefresh").offsetParent) militaryOverviewRefresh.click(); } function regenerateIce() { @@ -534,7 +534,7 @@ function regenerateMarkers() { Markers.regenerate(); turnButtonOn("toggleMarkers"); drawMarkers(); - if (document.getElementById("markersOverviewRefresh").offsetParent) markersOverviewRefresh.click(); + if (byId("markersOverviewRefresh").offsetParent) markersOverviewRefresh.click(); } function regenerateZones(event) { @@ -545,10 +545,9 @@ function regenerateZones(event) { else addNumberOfZones(gauss(1, 0.5, 0.6, 5, 2)); function addNumberOfZones(number) { - zones.selectAll("g").remove(); // remove existing zones - addZones(number); - if (document.getElementById("zonesEditorRefresh").offsetParent) zonesEditorRefresh.click(); - if (!layerIsOn("toggleZones")) toggleZones(); + Zones.generate(number); + if (byId("zonesEditorRefresh").offsetParent) zonesEditorRefresh.click(); + if (layerIsOn("toggleZones")) drawZones(); } } @@ -559,7 +558,7 @@ function unpressClickToAddButton() { } function toggleAddLabel() { - const pressed = document.getElementById("addLabel").classList.contains("pressed"); + const pressed = byId("addLabel").classList.contains("pressed"); if (pressed) { unpressClickToAddButton(); return; @@ -627,22 +626,22 @@ function addLabelOnClick() { function toggleAddBurg() { unpressClickToAddButton(); - document.getElementById("addBurgTool").classList.add("pressed"); + byId("addBurgTool").classList.add("pressed"); overviewBurgs(); - document.getElementById("addNewBurg").click(); + byId("addNewBurg").click(); } function toggleAddRiver() { - const pressed = document.getElementById("addRiver").classList.contains("pressed"); + const pressed = byId("addRiver").classList.contains("pressed"); if (pressed) { unpressClickToAddButton(); - document.getElementById("addNewRiver").classList.remove("pressed"); + byId("addNewRiver").classList.remove("pressed"); return; } addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed")); addRiver.classList.add("pressed"); - document.getElementById("addNewRiver").classList.add("pressed"); + byId("addNewRiver").classList.add("pressed"); closeDialogs(".stable"); viewbox.style("cursor", "crosshair").on("click", addRiverOnClick); tip("Click on map to place new river or extend an existing one. Hold Shift to place multiple rivers", true, "warn"); @@ -728,7 +727,7 @@ function addRiverOnClick() { } // continue old river - document.getElementById("river" + oldRiverId)?.remove(); + byId("river" + oldRiverId)?.remove(); riverCells.forEach(i => (cells.r[i] = oldRiverId)); oldRiverCells.forEach(cell => { if (h[cell] > h[min]) { @@ -796,13 +795,13 @@ function addRiverOnClick() { if (d3.event.shiftKey === false) { Lakes.cleanupLakeData(); unpressClickToAddButton(); - document.getElementById("addNewRiver").classList.remove("pressed"); + byId("addNewRiver").classList.remove("pressed"); if (addNewRiver.offsetParent) riversOverviewRefresh.click(); } } function toggleAddMarker() { - const pressed = document.getElementById("addMarker")?.classList.contains("pressed"); + const pressed = byId("addMarker")?.classList.contains("pressed"); if (pressed) { unpressClickToAddButton(); return; @@ -830,7 +829,7 @@ function addMarkerOnClick() { const isMarkerSelected = markers.length && elSelected?.node()?.parentElement?.id === "markers"; const selectedMarker = isMarkerSelected ? markers.find(marker => marker.i === +elSelected.attr("id").slice(6)) : null; - const selectedType = document.getElementById("addedMarkerType").value; + const selectedType = byId("addedMarkerType").value; const selectedConfig = Markers.getConfig().find(({type}) => type === selectedType); const baseMarker = selectedMarker || selectedConfig || {icon: "❓"}; @@ -840,13 +839,13 @@ function addMarkerOnClick() { selectedConfig.add("marker" + marker.i, cell); } - const markersElement = document.getElementById("markers"); + const markersElement = byId("markers"); const rescale = +markersElement.getAttribute("rescale"); markersElement.insertAdjacentHTML("beforeend", drawMarker(marker, rescale)); if (d3.event.shiftKey === false) { - document.getElementById("markerAdd").classList.remove("pressed"); - document.getElementById("markersAddFromOverview").classList.remove("pressed"); + byId("markerAdd").classList.remove("pressed"); + byId("markersAddFromOverview").classList.remove("pressed"); unpressClickToAddButton(); } } diff --git a/modules/ui/zones-editor.js b/modules/ui/zones-editor.js index 0e0bc6b6..a7ac8835 100644 --- a/modules/ui/zones-editor.js +++ b/modules/ui/zones-editor.js @@ -14,7 +14,6 @@ function editZones() { $("#zonesEditor").dialog({ title: "Zones Editor", resizable: false, - width: fitContent(), close: () => exitZonesManualAssignment("close"), position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"} }); @@ -31,34 +30,40 @@ function editZones() { byId("zonesManuallyCancel").on("click", cancelZonesManualAssignent); byId("zonesAdd").on("click", addZonesLayer); byId("zonesExport").on("click", downloadZonesData); - byId("zonesRemove").on("click", toggleEraseMode); + byId("zonesRemove").on("click", e => e.target.classList.toggle("pressed")); body.on("click", function (ev) { - const el = ev.target, - cl = el.classList, - zone = el.parentNode.dataset.id; - if (el.tagName === "FILL-BOX") changeFill(el); - else if (cl.contains("culturePopulation")) changePopulation(zone); - else if (cl.contains("icon-trash-empty")) zoneRemove(zone); - else if (cl.contains("icon-eye")) toggleVisibility(el); - else if (cl.contains("icon-pin")) toggleFog(zone, cl); - if (customization) selectZone(el); + const line = ev.target.closest("div.states"); + const zone = pack.zones.find(z => z.i === +line.dataset.id); + if (!zone) return; + + if (customization) { + if (zone.hidden) return; + body.querySelector("div.selected").classList.remove("selected"); + line.classList.add("selected"); + return; + } + + if (ev.target.closest("fill-box")) changeFill(ev.target.closest("fill-box").getAttribute("fill"), zone); + else if (ev.target.classList.contains("zonePopulation")) changePopulation(zone); + else if (ev.target.classList.contains("zoneRemove")) zoneRemove(zone); + else if (ev.target.classList.contains("zoneHide")) toggleVisibility(zone); + else if (ev.target.classList.contains("zoneFog")) toggleFog(zone, ev.target.classList); }); body.on("input", function (ev) { - const el = ev.target; - const zone = zones.select("#" + el.parentNode.dataset.id); + const line = ev.target.closest("div.states"); + const zone = pack.zones.find(z => z.i === +line.dataset.id); + if (!zone) return; - if (el.classList.contains("zoneName")) zone.attr("data-description", el.value); - else if (el.classList.contains("zoneType")) zone.attr("data-type", el.value); + if (ev.target.classList.contains("zoneName")) changeDescription(zone, ev.target.value); + else if (ev.target.classList.contains("zoneType")) changeType(zone, ev.target.value); }); // update type filter with a list of used types function updateFilters() { - const zones = Array.from(document.querySelectorAll("#zones > g")); - const types = unique(zones.map(zone => zone.dataset.type)); - const filterSelect = byId("zonesFilterType"); + const types = unique(pack.zones.map(zone => zone.type)); const typeToFilterBy = types.includes(zonesFilterType.value) ? zonesFilterType.value : "all"; filterSelect.innerHTML = @@ -68,47 +73,42 @@ function editZones() { // add line for each zone function zonesEditorAddLines() { - const unit = " " + getAreaUnit(); - const typeToFilterBy = byId("zonesFilterType").value; - const zones = Array.from(document.querySelectorAll("#zones > g")); - const filteredZones = typeToFilterBy === "all" ? zones : zones.filter(zone => zone.dataset.type === typeToFilterBy); + const filteredZones = + typeToFilterBy === "all" ? pack.zones : pack.zones.filter(zone => zone.type === typeToFilterBy); - const lines = filteredZones.map(zoneEl => { - const c = zoneEl.dataset.cells ? zoneEl.dataset.cells.split(",").map(c => +c) : []; - const description = zoneEl.dataset.description; - const type = zoneEl.dataset.type; - const fill = zoneEl.getAttribute("fill"); - const area = getArea(d3.sum(c.map(i => pack.cells.area[i]))); - const rural = d3.sum(c.map(i => pack.cells.pop[i])) * populationRate; + const lines = filteredZones.map(({i, name, type, cells, color, hidden}) => { + const area = getArea(d3.sum(cells.map(i => pack.cells.area[i]))); + const rural = d3.sum(cells.map(i => pack.cells.pop[i])) * populationRate; const urban = - d3.sum(c.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate * urbanization; - const population = rural + urban; + d3.sum(cells.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate * urbanization; + const population = rn(rural + urban); const populationTip = `Total population: ${si(population)}; Rural population: ${si( rural )}; Urban population: ${si(urban)}. Click to change`; - const inactive = zoneEl.style.display === "none"; - const focused = defs.select("#fog #focus" + zoneEl.id).size(); + const focused = defs.select("#fog #focusZone" + i).size(); - return `
- - + return /* html */ `
+ + -
${c.length}
+
${cells.length}
-
${si(area) + unit}
+
${si(area) + " " + getAreaUnit()}
-
${si(population)}
+
${si(population)}
- - - + +
`; }); @@ -121,14 +121,13 @@ function editZones() { (d3.sum(pack.cells.pop) + d3.sum(pack.burgs.filter(b => !b.removed).map(b => b.population)) * urbanization) * populationRate; zonesFooterPopulation.dataset.population = totalPop; - zonesFooterNumber.innerHTML = /* html */ `${filteredZones.length} of ${zones.length}`; + zonesFooterNumber.innerHTML = `${filteredZones.length} of ${pack.zones.length}`; zonesFooterCells.innerHTML = pack.cells.i.length; - zonesFooterArea.innerHTML = si(totalArea) + unit; + zonesFooterArea.innerHTML = si(totalArea) + " " + getAreaUnit(); zonesFooterPopulation.innerHTML = si(totalPop); - // add listeners - body.querySelectorAll("div.states").forEach(el => el.on("mouseenter", ev => zoneHighlightOn(ev))); - body.querySelectorAll("div.states").forEach(el => el.on("mouseleave", ev => zoneHighlightOff(ev))); + body.querySelectorAll("div.states").forEach(el => el.on("mouseenter", zoneHighlightOn)); + body.querySelectorAll("div.states").forEach(el => el.on("mouseleave", zoneHighlightOff)); if (body.dataset.type === "percentage") { body.dataset.type = "absolute"; @@ -138,25 +137,17 @@ function editZones() { } function zoneHighlightOn(event) { - const zone = event.target.dataset.id; - zones.select("#" + zone).style("outline", "1px solid red"); + const zoneId = event.target.dataset.id; + zones.select("#zone" + zoneId).style("outline", "1px solid red"); } function zoneHighlightOff(event) { - const zone = event.target.dataset.id; - zones.select("#" + zone).style("outline", null); + const zoneId = event.target.dataset.id; + zones.select("#zone" + zoneId).style("outline", null); } function filterZonesByType() { - const typeToFilterBy = this.value; - const zones = Array.from(document.querySelectorAll("#zones > g")); - - for (const zone of zones) { - const type = zone.dataset.type; - const visible = typeToFilterBy === "all" || type === typeToFilterBy; - zone.style.display = visible ? "block" : "none"; - } - + drawZones(); zonesEditorAddLines(); } @@ -167,23 +158,24 @@ function editZones() { axis: "y", update: movezone }); - function movezone(ev, ui) { - const zone = $("#" + ui.item.attr("data-id")); - const prev = $("#" + ui.item.prev().attr("data-id")); - if (prev) { - zone.insertAfter(prev); - return; - } - const next = $("#" + ui.item.next().attr("data-id")); - if (next) zone.insertBefore(next); + + function movezone(_ev, ui) { + const zone = pack.zones.find(z => z.i === +ui.item[0].dataset.id); + const oldIndex = pack.zones.indexOf(zone); + const newIndex = ui.item.index(); + if (oldIndex === newIndex) return; + + pack.zones.splice(oldIndex, 1); + pack.zones.splice(newIndex, 0, zone); + drawZones(); } function enterZonesManualAssignent() { if (!layerIsOn("toggleZones")) toggleZones(); customization = 10; + document.querySelectorAll("#zonesBottom > *").forEach(el => (el.style.display = "none")); byId("zonesManuallyButtons").style.display = "inline-block"; - zonesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden")); zonesFooter.style.display = "none"; body.querySelectorAll("div > input, select, svg").forEach(e => (e.style.pointerEvents = "none")); @@ -197,21 +189,32 @@ function editZones() { .on("touchmove mousemove", moveZoneBrush); body.querySelector("div").classList.add("selected"); - zones.selectAll("g").each(function () { - this.setAttribute("data-init", this.getAttribute("data-cells")); - }); - } - function selectZone(el) { - body.querySelector("div.selected").classList.remove("selected"); - el.classList.add("selected"); + // draw zones as individual cells + zones.selectAll("*").remove(); + + const filterBy = byId("zonesFilterType").value; + const isFiltered = filterBy && filterBy !== "all"; + const visibleZones = pack.zones.filter(zone => !zone.hidden && (!isFiltered || zone.type === filterBy)); + const data = visibleZones.map(({i, cells, color}) => cells.map(cell => ({cell, zoneId: i, fill: color}))).flat(); + zones + .selectAll("polygon") + .data(data, d => `${d.zoneId}-${d.cell}`) + .enter() + .append("polygon") + .attr("points", d => getPackPolygon(d.cell)) + .attr("fill", d => d.fill) + .attr("data-zone", d => d.zoneId) + .attr("data-cell", d => d.cell); } function selectZoneOnMapClick() { - if (d3.event.target.parentElement.parentElement.id !== "zones") return; - const zone = d3.event.target.parentElement.id; - const el = body.querySelector("div[data-id='" + zone + "']"); - selectZone(el); + if (d3.event.target.parentElement.id !== "zones") return; + const zoneId = d3.event.target.dataset.zone; + const el = body.querySelector("div[data-id='" + zoneId + "']"); + + body.querySelector("div.selected").classList.remove("selected"); + el.classList.add("selected"); } function dragZoneBrush() { @@ -219,43 +222,41 @@ function editZones() { const eraseMode = byId("zonesRemove").classList.contains("pressed"); const landOnly = byId("zonesBrushLandOnly").checked; - const selected = body.querySelector("div.selected"); - const zone = zones.select("#" + selected.dataset.id); - const base = zone.attr("id") + "_"; // id generic part - d3.event.on("drag", () => { if (!d3.event.dx && !d3.event.dy) return; const [x, y] = d3.mouse(this); moveCircle(x, y, radius); - let selection = radius > 5 ? findAll(x, y, radius) : [findCell(x, y, radius)]; + let selection = radius > 5 ? findAll(x, y, radius) : [findCell(x, y)]; if (landOnly) selection = selection.filter(i => pack.cells.h[i] >= 20); - if (!selection) return; + if (!selection.length) return; - const dataCells = zone.attr("data-cells"); - let cells = dataCells ? dataCells.split(",").map(i => +i) : []; + const zoneId = +body.querySelector("div.selected")?.dataset.id; + const zone = pack.zones.find(z => z.i === zoneId); + if (!zone) return; if (eraseMode) { - // remove - selection.forEach(i => { - const index = cells.indexOf(i); - if (index === -1) return; - zone.select("polygon#" + base + i).remove(); - cells.splice(index, 1); - }); + const data = zones + .selectAll("polygon") + .data() + .filter(d => !(d.zoneId === zoneId && selection.includes(d.cell))); + zones + .selectAll("polygon") + .data(data, d => `${d.zoneId}-${d.cell}`) + .exit() + .remove(); } else { - // add - selection.forEach(i => { - if (cells.includes(i)) return; - cells.push(i); - zone - .append("polygon") - .attr("points", getPackPolygon(i)) - .attr("id", base + i); - }); + const data = selection.map(cell => ({cell, zoneId, fill: zone.color})); + zones + .selectAll("polygon") + .data(data, d => `${d.zoneId}-${d.cell}`) + .enter() + .append("polygon") + .attr("points", d => getPackPolygon(d.cell)) + .attr("fill", d => d.fill) + .attr("data-zone", d => d.zoneId) + .attr("data-cell", d => d.cell); } - - zone.attr("data-cells", cells); }); } @@ -263,39 +264,29 @@ function editZones() { showMainTip(); const point = d3.mouse(this); const radius = +zonesBrush.value; - moveCircle(point[0], point[1], radius); + moveCircle(...point, radius); } function applyZonesManualAssignent() { - zones.selectAll("g").each(function () { - if (this.dataset.cells) return; - // all zone cells are removed - unfog("focusZone" + this.id); - this.style.display = "block"; - }); + const data = zones.selectAll("polygon").data(); + const zoneCells = data.reduce((acc, d) => { + if (!acc[d.zoneId]) acc[d.zoneId] = []; + acc[d.zoneId].push(d.cell); + return acc; + }, {}); + const filterBy = byId("zonesFilterType").value; + const isFiltered = filterBy && filterBy !== "all"; + const visibleZones = pack.zones.filter(zone => !zone.hidden && (!isFiltered || zone.type === filterBy)); + visibleZones.forEach(zone => (zone.cells = zoneCells[zone.i] || [])); + + drawZones(); zonesEditorAddLines(); exitZonesManualAssignment(); } - // restore initial zone cells function cancelZonesManualAssignent() { - zones.selectAll("g").each(function () { - const zone = d3.select(this); - const dataCells = zone.attr("data-init"); - const cells = dataCells ? dataCells.split(",").map(i => +i) : []; - zone.attr("data-cells", cells); - zone.selectAll("*").remove(); - const base = zone.attr("id") + "_"; // id generic part - zone - .selectAll("*") - .data(cells) - .enter() - .append("polygon") - .attr("points", d => getPackPolygon(d)) - .attr("id", d => base + d); - }); - + drawZones(); exitZonesManualAssignment(); } @@ -313,60 +304,47 @@ function editZones() { restoreDefaultEvents(); clearMainTip(); - zones.selectAll("g").each(function () { - this.removeAttribute("data-init"); - }); + const selected = body.querySelector("div.selected"); if (selected) selected.classList.remove("selected"); } - function changeFill(el) { - const fill = el.getAttribute("fill"); + function changeFill(fill, zone) { const callback = newFill => { - el.fill = newFill; - byId(el.parentNode.dataset.id).setAttribute("fill", newFill); + zone.color = newFill; + drawZones(); + zonesEditorAddLines(); }; openPicker(fill, callback); } - function toggleVisibility(el) { - const zone = zones.select("#" + el.parentNode.dataset.id); - const inactive = zone.style("display") === "none"; - inactive ? zone.style("display", "block") : zone.style("display", "none"); - el.classList.toggle("inactive"); + function toggleVisibility(zone) { + const isHidden = Boolean(zone.hidden); + if (isHidden) delete zone.hidden; + else zone.hidden = true; + + drawZones(); + zonesEditorAddLines(); } - function toggleFog(z, cl) { - const dataCells = zones.select("#" + z).attr("data-cells"); - if (!dataCells) return; - - const path = - "M" + - dataCells - .split(",") - .map(c => getPackPolygon(+c)) - .join("M") + - "Z", - id = "focusZone" + z; - cl.contains("inactive") ? fog(id, path) : unfog(id); + function toggleFog(zone, cl) { + const inactive = cl.contains("inactive"); cl.toggle("inactive"); + + if (inactive) { + const path = zones.select("#zone" + zone.i).attr("d"); + fog("focusZone" + zone.i, path); + } else { + unfog("focusZone" + zone.i); + } } function toggleLegend() { - if (legend.selectAll("*").size()) { - clearLegend(); - return; - } // hide legend - const data = []; - - zones.selectAll("g").each(function () { - const id = this.dataset.id; - const description = this.dataset.description; - const fill = this.getAttribute("fill"); - data.push([id, fill, description]); - }); - + const filterBy = byId("zonesFilterType").value; + const isFiltered = filterBy && filterBy !== "all"; + const visibleZones = pack.zones.filter(zone => !zone.hidden && (!isFiltered || zone.type === filterBy)); + const data = visibleZones.map(({i, name, color}) => ["zone" + i, color, name]); drawLegend("Zones", data); } @@ -380,8 +358,7 @@ function editZones() { body.querySelectorAll(":scope > div").forEach(function (el) { el.querySelector(".stateCells").innerHTML = rn((+el.dataset.cells / totalCells) * 100, 2) + "%"; el.querySelector(".biomeArea").innerHTML = rn((+el.dataset.area / totalArea) * 100, 2) + "%"; - el.querySelector(".culturePopulation").innerHTML = - rn((+el.dataset.population / totalPopulation) * 100, 2) + "%"; + el.querySelector(".zonePopulation").innerHTML = rn((+el.dataset.population / totalPopulation) * 100, 2) + "%"; }); } else { body.dataset.type = "absolute"; @@ -390,28 +367,23 @@ function editZones() { } function addZonesLayer() { - const id = getNextId("zone"); - const description = "Unknown zone"; + const zoneId = pack.zones.length ? Math.max(...pack.zones.map(z => z.i)) + 1 : 0; + const name = "Unknown zone"; const type = "Unknown"; - const fill = "url(#hatch" + (id.slice(4) % 42) + ")"; - zones - .append("g") - .attr("id", id) - .attr("data-description", description) - .attr("data-type", type) - .attr("data-cells", "") - .attr("fill", fill); + const color = "url(#hatch" + (zoneId % 42) + ")"; + pack.zones.push({i: zoneId, name, type, color, cells: []}); zonesEditorAddLines(); + drawZones(); } function downloadZonesData() { const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value; - let data = "Id,Fill,Description,Type,Cells,Area " + unit + ",Population\n"; // headers + let data = "Id,Color,Description,Type,Cells,Area " + unit + ",Population\n"; // headers body.querySelectorAll(":scope > div").forEach(function (el) { data += el.dataset.id + ","; - data += el.dataset.fill + ","; + data += el.dataset.color + ","; data += el.dataset.description + ","; data += el.dataset.type + ","; data += el.dataset.cells + ","; @@ -423,27 +395,24 @@ function editZones() { downloadFile(data, name); } - function toggleEraseMode() { - this.classList.toggle("pressed"); + function changeDescription(zone, value) { + zone.name = value; + zones.select("#zone" + zone.i).attr("data-description", value); + } + + function changeType(zone, value) { + zone.type = value; + zones.select("#zone" + zone.i).attr("data-type", value); } function changePopulation(zone) { - const dataCells = zones.select("#" + zone).attr("data-cells"); - const cells = dataCells - ? dataCells - .split(",") - .map(i => +i) - .filter(i => pack.cells.h[i] >= 20) - : []; - if (!cells.length) { - tip("Zone does not have any land cells, cannot change population", false, "error"); - return; - } - const burgs = pack.burgs.filter(b => !b.removed && cells.includes(b.cell)); + const landCells = zone.cells.filter(i => pack.cells.h[i] >= 20); + if (!landCells.length) return tip("Zone does not have any land cells, cannot change population", false, "error"); - const rural = rn(d3.sum(cells.map(i => pack.cells.pop[i])) * populationRate); + const burgs = pack.burgs.filter(b => !b.removed && landCells.includes(b.cell)); + const rural = rn(d3.sum(landCells.map(i => pack.cells.pop[i])) * populationRate); const urban = rn( - d3.sum(cells.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate * urbanization + d3.sum(landCells.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate * urbanization ); const total = rural + urban; const l = n => Number(n).toLocaleString(); @@ -485,12 +454,12 @@ function editZones() { function applyPopulationChange() { const ruralChange = ruralPop.value / rural; if (isFinite(ruralChange) && ruralChange !== 1) { - cells.forEach(i => (pack.cells.pop[i] *= ruralChange)); + landCells.forEach(i => (pack.cells.pop[i] *= ruralChange)); } if (!isFinite(ruralChange) && +ruralPop.value > 0) { const points = ruralPop.value / populationRate; - const pop = rn(points / cells.length); - cells.forEach(i => (pack.cells.pop[i] = pop)); + const pop = rn(points / landCells.length); + landCells.forEach(i => (pack.cells.pop[i] = pop)); } const urbanChange = urbanPop.value / urban; @@ -508,8 +477,16 @@ function editZones() { } function zoneRemove(zone) { - zones.select("#" + zone).remove(); - unfog("focusZone" + zone); - zonesEditorAddLines(); + confirmationDialog({ + title: "Remove zone", + message: "Are you sure you want to remove the zone?
This action cannot be reverted", + confirm: "Remove", + onConfirm: () => { + pack.zones = pack.zones.filter(z => z.i !== zone.i); + zones.select("#zone" + zone.i).remove(); + unfog("focusZone" + zone.i); + zonesEditorAddLines(); + } + }); } } diff --git a/modules/zones-generator.js b/modules/zones-generator.js new file mode 100644 index 00000000..e0923615 --- /dev/null +++ b/modules/zones-generator.js @@ -0,0 +1,447 @@ +"use strict"; + +window.Zones = (function () { + const config = { + invasion: {quantity: 2, generate: addInvasion}, // invasion of enemy lands + rebels: {quantity: 1.5, generate: addRebels}, // rebels along a state border + proselytism: {quantity: 1.6, generate: addProselytism}, // proselitism of organized religion + crusade: {quantity: 1.6, generate: addCrusade}, // crusade on heresy lands + disease: {quantity: 1.4, generate: addDisease}, // disease starting in a random city + disaster: {quantity: 1, generate: addDisaster}, // disaster starting in a random city + eruption: {quantity: 1, generate: addEruption}, // eruption aroung volcano + avalanche: {quantity: 0.8, generate: addAvalanche}, // avalanche impacting highland road + fault: {quantity: 1, generate: addFault}, // fault line in elevated areas + flood: {quantity: 1, generate: addFlood}, // flood on river banks + tsunami: {quantity: 1, generate: addTsunami} // tsunami starting near coast + }; + + const generate = function (globalModifier = 1) { + TIME && console.time("generateZones"); + + const usedCells = new Uint8Array(pack.cells.i.length); + pack.zones = []; + + Object.values(config).forEach(type => { + const expectedNumber = type.quantity * globalModifier; + let number = gauss(expectedNumber, expectedNumber / 2, 0, 100); + while (number--) type.generate(usedCells); + }); + + TIME && console.timeEnd("generateZones"); + }; + + function addInvasion(usedCells) { + const {cells, states} = pack; + + const ongoingConflicts = states + .filter(s => s.i && !s.removed && s.campaigns) + .map(s => s.campaigns) + .flat() + .filter(c => !c.end); + if (!ongoingConflicts.length) return; + const {defender, attacker} = ra(ongoingConflicts); + + const borderCells = cells.i.filter(cellId => { + if (usedCells[cellId]) return false; + if (cells.state[cellId] !== defender) return false; + return cells.c[cellId].some(c => cells.state[c] === attacker); + }); + + const startCell = ra(borderCells); + if (startCell === undefined) return; + + const invationCells = []; + const queue = [startCell]; + const maxCells = rand(5, 30); + + while (queue.length) { + const cellId = P(0.4) ? queue.shift() : queue.pop(); + invationCells.push(cellId); + if (invationCells.length >= maxCells) break; + + cells.c[cellId].forEach(neibCellId => { + if (usedCells[neibCellId]) return; + if (cells.state[neibCellId] !== defender) return; + usedCells[neibCellId] = 1; + queue.push(neibCellId); + }); + } + + const subtype = rw({ + Invasion: 5, + Occupation: 4, + Conquest: 3, + Incursion: 2, + Intervention: 2, + Subjugation: 1, + Foray: 1, + Skirmishes: 1, + Pillaging: 1, + Raid: 1 + }); + const name = getAdjective(states[attacker].name) + " " + subtype; + + pack.zones.push({i: pack.zones.length, name, type: "Invasion", cells: invationCells, color: "url(#hatch1)"}); + } + + function addRebels(usedCells) { + const {cells, states} = pack; + + const state = ra(states.filter(s => s.i && !s.removed && s.neighbors.some(Boolean))); + if (!state) return; + + const neibStateId = ra(state.neighbors.filter(n => n && !states[n].removed)); + if (!neibStateId) return; + + const cellsArray = []; + const queue = []; + const borderCellId = cells.i.find( + i => cells.state[i] === state.i && cells.c[i].some(c => cells.state[c] === neibStateId) + ); + if (borderCellId) queue.push(borderCellId); + const maxCells = rand(10, 30); + + while (queue.length) { + const cellId = queue.shift(); + cellsArray.push(cellId); + if (cellsArray.length >= maxCells) break; + + cells.c[cellId].forEach(neibCellId => { + if (usedCells[neibCellId]) return; + if (cells.state[neibCellId] !== state.i) return; + usedCells[neibCellId] = 1; + if (neibCellId % 4 !== 0 && !cells.c[neibCellId].some(c => cells.state[c] === neibStateId)) return; + queue.push(neibCellId); + }); + } + + const rebels = rw({ + Rebels: 5, + Insurrection: 2, + Mutineers: 1, + Insurgents: 1, + Rioters: 1, + Separatists: 1, + Secessionists: 1, + Rebellion: 1, + Conspiracy: 1 + }); + + const name = getAdjective(states[neibStateId].name) + " " + rebels; + pack.zones.push({i: pack.zones.length, name, type: "Rebels", cells: cellsArray, color: "url(#hatch3)"}); + } + + function addProselytism(usedCells) { + const {cells, religions} = pack; + + const organizedReligions = religions.filter(r => r.i && !r.removed && r.type === "Organized"); + const religion = ra(organizedReligions); + if (!religion) return; + + const targetBorderCells = cells.i.filter( + i => + cells.h[i] < 20 && + cells.pop[i] && + cells.religion[i] !== religion.i && + cells.c[i].some(c => cells.religion[c] === religion.i) + ); + const startCell = ra(targetBorderCells); + if (!startCell) return; + + const targetReligionId = cells.religion[startCell]; + const proselytismCells = []; + const queue = [startCell]; + const maxCells = rand(10, 30); + + while (queue.length) { + const cellId = queue.shift(); + proselytismCells.push(cellId); + if (proselytismCells.length >= maxCells) break; + + cells.c[cellId].forEach(neibCellId => { + if (usedCells[neibCellId]) return; + if (cells.religion[neibCellId] !== targetReligionId) return; + if (cells.h[neibCellId] < 20 || !cells.pop[i]) return; + usedCells[neibCellId] = 1; + queue.push(neibCellId); + }); + } + + const name = `${getAdjective(religion.name.split(" ")[0])} Proselytism`; + pack.zones.push({i: pack.zones.length, name, type: "Proselytism", cells: proselytismCells, color: "url(#hatch6)"}); + } + + function addCrusade(usedCells) { + const {cells, religions} = pack; + + const heresies = religions.filter(r => !r.removed && r.type === "Heresy"); + if (!heresies.length) return; + + const heresy = ra(heresies); + const crusadeCells = cells.i.filter(i => !usedCells[i] && cells.religion[i] === heresy.i); + if (!crusadeCells.length) return; + crusadeCells.forEach(i => (usedCells[i] = 1)); + + const name = getAdjective(heresy.name.split(" ")[0]) + " Crusade"; + pack.zones.push({ + i: pack.zones.length, + name, + type: "Crusade", + cells: Array.from(crusadeCells), + color: "url(#hatch6)" + }); + } + + function addDisease(usedCells) { + const {cells, burgs} = pack; + + const burg = ra(burgs.filter(b => !usedCells[b.cell] && b.i && !b.removed)); // random burg + if (!burg) return; + + const cellsArray = []; + const cost = []; + const maxCells = rand(20, 40); + + const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); + queue.queue({e: burg.cell, p: 0}); + + while (queue.length) { + const next = queue.dequeue(); + if (cells.burg[next.e] || cells.pop[next.e]) cellsArray.push(next.e); + usedCells[next.e] = 1; + + cells.c[next.e].forEach(nextCellId => { + const c = Routes.getRoute(next.e, nextCellId) ? 5 : 100; + const p = next.p + c; + if (p > maxCells) return; + + if (!cost[nextCellId] || p < cost[nextCellId]) { + cost[nextCellId] = p; + queue.queue({e: nextCellId, p}); + } + }); + } + + // prettier-ignore + const name = `${(() => { + const model = rw({color: 2, animal: 1, adjective: 1}); + if (model === "color") return ra(["Amber", "Azure", "Black", "Blue", "Brown", "Crimson", "Emerald", "Golden", "Green", "Grey", "Orange", "Pink", "Purple", "Red", "Ruby", "Scarlet", "Silver", "Violet", "White", "Yellow"]); + if (model === "animal") return ra(["Ape", "Bear", "Bird", "Boar", "Cat", "Cow", "Dog", "Fox", "Horse", "Lion", "Pig", "Rat", "Raven", "Sheep", "Spider", "Tiger", "Viper", "Wolf", "Worm", "Wyrm"]); + if (model === "adjective") return ra(["Blind", "Bloody", "Brutal", "Burning", "Deadly", "Fatal", "Furious", "Great", "Grim", "Horrible", "Invisible", "Lethal", "Loud", "Mortal", "Savage", "Severe", "Silent", "Unknown", "Venomous", "Vicious"]); + })()} ${rw({Fever: 5, Plague: 3, Cough: 3, Flu: 2, Pox: 2, Cholera: 2, Typhoid: 2, Leprosy: 1, Smallpox: 1, Pestilence: 1, Consumption: 1, Malaria: 1, Dropsy: 1})}`; + + pack.zones.push({i: pack.zones.length, name, type: "Disease", cells: cellsArray, color: "url(#hatch12)"}); + } + + function addDisaster(usedCells) { + const {cells, burgs} = pack; + + const burg = ra(burgs.filter(b => !usedCells[b.cell] && b.i && !b.removed)); + if (!burg) return; + usedCells[burg.cell] = 1; + + const cellsArray = []; + const cost = []; + const maxCells = rand(5, 25); + + const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); + queue.queue({e: burg.cell, p: 0}); + + while (queue.length) { + const next = queue.dequeue(); + if (cells.burg[next.e] || cells.pop[next.e]) cellsArray.push(next.e); + usedCells[next.e] = 1; + + cells.c[next.e].forEach(function (e) { + const c = rand(1, 10); + const p = next.p + c; + if (p > maxCells) return; + + if (!cost[e] || p < cost[e]) { + cost[e] = p; + queue.queue({e, p}); + } + }); + } + + const type = rw({ + Famine: 5, + Drought: 3, + Earthquake: 3, + Dearth: 1, + Tornadoes: 1, + Wildfires: 1, + Storms: 1, + Blight: 1 + }); + const name = getAdjective(burg.name) + " " + type; + pack.zones.push({i: pack.zones.length, name, type: "Disaster", cells: cellsArray, color: "url(#hatch5)"}); + } + + function addEruption(usedCells) { + const {cells, markers} = pack; + + const volcanoe = markers.find(m => m.type === "volcanoes" && !usedCells[m.cell]); + if (!volcanoe) return; + usedCells[volcanoe.cell] = 1; + + const note = notes.find(n => n.id === "marker" + volcanoe.i); + if (note) note.legend = note.legend.replace("Active volcano", "Erupting volcano"); + const name = note ? note.name.replace(" Volcano", "") + " Eruption" : "Volcano Eruption"; + + const cellsArray = []; + const queue = [volcanoe.cell]; + const maxCells = rand(10, 30); + + while (queue.length) { + const cellId = P(0.5) ? queue.shift() : queue.pop(); + cellsArray.push(cellId); + if (cellsArray.length >= maxCells) break; + + cells.c[cellId].forEach(neibCellId => { + if (usedCells[neibCellId] || cells.h[neibCellId] < 20) return; + usedCells[neibCellId] = 1; + queue.push(neibCellId); + }); + } + + pack.zones.push({i: pack.zones.length, name, type: "Eruption", cells: cellsArray, color: "url(#hatch7)"}); + } + + function addAvalanche(usedCells) { + const {cells} = pack; + + const routeCells = cells.i.filter(i => !usedCells[i] && Routes.isConnected(i) && cells.h[i] >= 70); + if (!routeCells.length) return; + + const startCell = ra(routeCells); + usedCells[startCell] = 1; + + const cellsArray = []; + const queue = [startCell]; + const maxCells = rand(3, 15); + + while (queue.length) { + const cellId = P(0.3) ? queue.shift() : queue.pop(); + cellsArray.push(cellId); + if (cellsArray.length >= maxCells) break; + + cells.c[cellId].forEach(neibCellId => { + if (usedCells[neibCellId] || cells.h[neibCellId] < 65) return; + usedCells[neibCellId] = 1; + queue.push(neibCellId); + }); + } + + const name = getAdjective(Names.getCultureShort(cells.culture[startCell])) + " Avalanche"; + pack.zones.push({i: pack.zones.length, name, type: "Avalanche", cells: cellsArray, color: "url(#hatch5)"}); + } + + function addFault(usedCells) { + const cells = pack.cells; + + const elevatedCells = cells.i.filter(i => !usedCells[i] && cells.h[i] > 50 && cells.h[i] < 70); + if (!elevatedCells.length) return; + + const startCell = ra(elevatedCells); + usedCells[startCell] = 1; + + const cellsArray = []; + const queue = [startCell]; + const maxCells = rand(3, 15); + + while (queue.length) { + const cellId = queue.pop(); + if (cells.h[cellId] >= 20) cellsArray.push(cellId); + if (cellsArray.length >= maxCells) break; + + cells.c[cellId].forEach(neibCellId => { + if (usedCells[neibCellId] || cells.r[neibCellId]) return; + usedCells[neibCellId] = 1; + queue.push(neibCellId); + }); + } + + const name = getAdjective(Names.getCultureShort(cells.culture[startCell])) + " Fault"; + pack.zones.push({i: pack.zones.length, name, type: "Fault", cells: cellsArray, color: "url(#hatch2)"}); + } + + function addFlood(usedCells) { + const cells = pack.cells; + + const fl = cells.fl.filter(Boolean); + const meanFlux = d3.mean(fl); + const maxFlux = d3.max(fl); + const fluxThreshold = (maxFlux - meanFlux) / 2 + meanFlux; + + const bigRiverCells = cells.i.filter( + i => !usedCells[i] && cells.h[i] < 50 && cells.r[i] && cells.fl[i] > fluxThreshold && cells.burg[i] + ); + if (!bigRiverCells.length) return; + + const startCell = ra(bigRiverCells); + usedCells[startCell] = 1; + + const riverId = cells.r[startCell]; + const cellsArray = []; + const queue = [startCell]; + const maxCells = rand(5, 30); + + while (queue.length) { + const cellId = queue.pop(); + cellsArray.push(cellId); + if (cellsArray.length >= maxCells) break; + + cells.c[cellId].forEach(neibCellId => { + if ( + usedCells[neibCellId] || + cells.h[neibCellId] < 20 || + cells.r[neibCellId] !== riverId || + cells.h[neibCellId] > 50 || + cells.fl[neibCellId] < meanFlux + ) + return; + usedCells[neibCellId] = 1; + queue.push(neibCellId); + }); + } + + const name = getAdjective(pack.burgs[cells.burg[startCell]].name) + " Flood"; + pack.zones.push({i: pack.zones.length, name, type: "Flood", cells: cellsArray, color: "url(#hatch13)"}); + } + + function addTsunami(usedCells) { + const {cells, features} = pack; + + const coastalCells = cells.i.filter( + i => !usedCells[i] && cells.t[i] === -1 && features[cells.f[i]].type !== "lake" + ); + if (!coastalCells.length) return; + + const startCell = ra(coastalCells); + usedCells[startCell] = 1; + + const cellsArray = []; + const queue = [startCell]; + const maxCells = rand(10, 30); + + while (queue.length) { + const cellId = queue.shift(); + if (cells.t[cellId] === 1) cellsArray.push(cellId); + if (cellsArray.length >= maxCells) break; + + cells.c[cellId].forEach(neibCellId => { + if (usedCells[neibCellId]) return; + if (cells.t[neibCellId] > 2) return; + if (pack.features[cells.f[neibCellId]].type === "lake") return; + usedCells[neibCellId] = 1; + queue.push(neibCellId); + }); + } + + const name = getAdjective(Names.getCultureShort(cells.culture[startCell])) + " Tsunami"; + pack.zones.push({i: pack.zones.length, name, type: "Tsunami", cells: cellsArray, color: "url(#hatch13)"}); + } + + return {generate}; +})(); diff --git a/styles/ancient.json b/styles/ancient.json index 8bc05ec2..d57aa524 100644 --- a/styles/ancient.json +++ b/styles/ancient.json @@ -332,6 +332,7 @@ "opacity": 1, "fill": "#3e3e4b", "text-shadow": "white 0px 0px 4px", + "letter-spacing": 0, "data-size": 12, "font-size": 12, "font-family": "Great Vibes" @@ -357,6 +358,7 @@ "opacity": 1, "fill": "#3e3e4b", "text-shadow": "white 0px 0px 4px", + "letter-spacing": 0, "data-size": 5, "font-size": 5, "font-family": "Great Vibes" @@ -384,6 +386,7 @@ "stroke": "#3a3a3a", "stroke-width": 0, "text-shadow": "white 0px 0px 4px", + "letter-spacing": 0, "data-size": 22, "font-size": 22, "font-family": "Great Vibes", @@ -395,6 +398,7 @@ "stroke": "#3a3a3a", "stroke-width": 0, "text-shadow": "white 0px 0px 4px", + "letter-spacing": 0, "data-size": 18, "font-size": 18, "font-family": "Times New Roman", diff --git a/styles/atlas.json b/styles/atlas.json index d7990935..ef7d7f8a 100644 --- a/styles/atlas.json +++ b/styles/atlas.json @@ -332,6 +332,7 @@ "opacity": 1, "fill": "#000000", "text-shadow": "white 0px 0px 4px", + "letter-spacing": 0, "data-size": 5, "font-size": 5, "font-family": "Amarante" @@ -357,6 +358,7 @@ "opacity": 1, "fill": "#000000", "text-shadow": "white 0px 0px 4px", + "letter-spacing": 0, "data-size": 4, "font-size": 4, "font-family": "Amarante" @@ -384,6 +386,7 @@ "stroke": "#000000", "stroke-width": 0, "text-shadow": "white 0px 0px 4px", + "letter-spacing": 0, "data-size": 21, "font-size": 21, "font-family": "Amarante", @@ -395,6 +398,7 @@ "stroke": "#000000", "stroke-width": 0, "text-shadow": "white 0px 0px 4px", + "letter-spacing": 0, "data-size": 18, "font-size": 18, "font-family": "Amarante", diff --git a/styles/clean.json b/styles/clean.json index 82681e28..c5aad094 100644 --- a/styles/clean.json +++ b/styles/clean.json @@ -319,22 +319,22 @@ "mask": "url(#land)" }, "#legend": { - "data-size": 12.74, - "font-size": 12.74, + "data-size": 12, + "font-size": 12, "font-family": "Arial", "stroke": "#909090", - "stroke-width": 1.13, + "stroke-width": 1, "stroke-dasharray": 0, "stroke-linecap": "round", - "data-x": 98.39, - "data-y": 12.67, - "data-columns": null + "data-x": 99, + "data-y": 93, + "data-columns": 8 }, - "#legendBox": {}, "#burgLabels > #cities": { "opacity": 1, "fill": "#414141", "text-shadow": "white 0 0 4px", + "letter-spacing": 0, "data-size": 7, "font-size": 7, "font-family": "Arial" @@ -359,6 +359,8 @@ "#burgLabels > #towns": { "opacity": 1, "fill": "#414141", + "text-shadow": "none", + "letter-spacing": 0, "data-size": 3, "font-size": 3, "font-family": "Arial" @@ -386,6 +388,7 @@ "stroke": "#303030", "stroke-width": 0, "text-shadow": "white 0 0 2px", + "letter-spacing": 0, "data-size": 10, "font-size": 10, "font-family": "Arial", @@ -397,6 +400,7 @@ "stroke": "#3a3a3a", "stroke-width": 0, "text-shadow": "white 0 0 4px", + "letter-spacing": 0, "data-size": 18, "font-size": 18, "font-family": "Arial", diff --git a/styles/cyberpunk.json b/styles/cyberpunk.json index 2821eb7f..93f22284 100644 --- a/styles/cyberpunk.json +++ b/styles/cyberpunk.json @@ -332,6 +332,7 @@ "opacity": 1, "fill": "#ffffff", "text-shadow": "white 0px 0px 4px", + "letter-spacing": 0, "data-size": 8, "font-size": 8, "font-family": "Orbitron" @@ -357,6 +358,7 @@ "opacity": 1, "fill": "#ffffff", "text-shadow": "white 0px 0px 4px", + "letter-spacing": 0, "data-size": 3, "font-size": 3, "font-family": "Orbitron" @@ -384,6 +386,7 @@ "stroke": "#000000", "stroke-width": 0, "text-shadow": "white 0px 0px 4px", + "letter-spacing": 0, "data-size": 18, "font-size": 18, "font-family": "Orbitron", @@ -395,6 +398,7 @@ "stroke": "#000000", "stroke-width": 0, "text-shadow": "white 0px 0px 4px", + "letter-spacing": 0, "data-size": 18, "font-size": 18, "font-family": "Almendra SC", diff --git a/styles/darkSeas.json b/styles/darkSeas.json index 9bed1ef2..2bc90fa6 100644 --- a/styles/darkSeas.json +++ b/styles/darkSeas.json @@ -321,6 +321,7 @@ "opacity": 1, "fill": "#000000", "text-shadow": "white 0px 0px 4px", + "letter-spacing": 0, "data-size": 7, "font-size": 7, "font-family": "Lugrasimo" @@ -345,6 +346,7 @@ "opacity": 1, "fill": "#000000", "text-shadow": "white 0px 0px 4px", + "letter-spacing": 0, "data-size": 5, "font-size": 5, "font-family": "Lugrasimo" @@ -371,6 +373,7 @@ "stroke": "#000000", "stroke-width": 0, "text-shadow": "white 0px 0px 4px", + "letter-spacing": 0, "data-size": 21, "font-size": 21, "font-family": "Eagle Lake", @@ -382,6 +385,7 @@ "stroke": "#000000", "stroke-width": 0, "text-shadow": "white 0px 0px 4px", + "letter-spacing": 0, "data-size": 18, "font-size": 18, "font-family": "Eagle Lake", diff --git a/styles/default.json b/styles/default.json index 623e37f3..9168debc 100644 --- a/styles/default.json +++ b/styles/default.json @@ -332,6 +332,7 @@ "opacity": 1, "fill": "#3e3e4b", "text-shadow": "white 0px 0px 4px", + "letter-spacing": 0, "data-size": 7, "font-size": 7, "font-family": "Almendra SC" @@ -357,6 +358,7 @@ "opacity": 1, "fill": "#3e3e4b", "text-shadow": "white 0px 0px 4px", + "letter-spacing": 0, "data-size": 4, "font-size": 4, "font-family": "Almendra SC" @@ -384,6 +386,7 @@ "stroke": "#3a3a3a", "stroke-width": 0, "text-shadow": "white 0px 0px 4px", + "letter-spacing": 0, "data-size": 22, "font-size": 22, "font-family": "Almendra SC", @@ -395,6 +398,7 @@ "stroke": "#3a3a3a", "stroke-width": 0, "text-shadow": "white 0px 0px 4px", + "letter-spacing": 0, "data-size": 18, "font-size": 18, "font-family": "Almendra SC", diff --git a/styles/gloom.json b/styles/gloom.json index 858c807c..19318882 100644 --- a/styles/gloom.json +++ b/styles/gloom.json @@ -335,6 +335,7 @@ "opacity": 1, "fill": "#3e3e4b", "text-shadow": "white 0 0 2px", + "letter-spacing": 0, "data-size": 8, "font-size": 8, "font-family": "Underdog" @@ -359,6 +360,8 @@ "#burgLabels > #towns": { "opacity": 1, "fill": "#3e3e4b", + "text-shadow": "none", + "letter-spacing": 0, "data-size": 4, "font-size": 4, "font-family": "Underdog" @@ -386,6 +389,7 @@ "stroke": "#b5b5b5", "stroke-width": 0, "text-shadow": "white 0 0 2px", + "letter-spacing": 0, "data-size": 20, "font-size": 20, "font-family": "Underdog", @@ -397,6 +401,7 @@ "stroke": "#3a3a3a", "stroke-width": 0, "text-shadow": "white 0 0 4px", + "letter-spacing": 0, "data-size": 18, "font-size": 18, "font-family": "Underdog", diff --git a/styles/light.json b/styles/light.json index cf846e1b..de539872 100644 --- a/styles/light.json +++ b/styles/light.json @@ -332,6 +332,7 @@ "opacity": 1, "fill": "#3a3a3a", "text-shadow": "white 0px 0px 4px", + "letter-spacing": 0, "data-size": 8, "font-size": 8, "font-family": "IM Fell English" @@ -357,6 +358,7 @@ "opacity": 1, "fill": "#3e3e4b", "text-shadow": "white 0px 0px 4px", + "letter-spacing": 0, "data-size": 4, "font-size": 4, "font-family": "IM Fell English" @@ -384,6 +386,7 @@ "stroke": "#000000", "stroke-width": 0.3, "text-shadow": "white 0px 0px 6px", + "letter-spacing": 0, "data-size": 14, "font-size": 14, "font-family": "IM Fell English", @@ -395,6 +398,7 @@ "stroke": "#701b05", "stroke-width": 0.1, "text-shadow": "white 0px 0px 4px", + "letter-spacing": 0, "data-size": 6, "font-size": 6, "font-family": "IM Fell English", diff --git a/styles/monochrome.json b/styles/monochrome.json index 20d3e588..1ee17c43 100644 --- a/styles/monochrome.json +++ b/styles/monochrome.json @@ -328,6 +328,7 @@ "opacity": 1, "fill": "#000000", "text-shadow": "white 0px 0px 4px", + "letter-spacing": 0, "data-size": 7, "font-size": 7, "font-family": "Courier New" @@ -353,6 +354,7 @@ "opacity": 1, "fill": "#000000", "text-shadow": "white 0px 0px 4px", + "letter-spacing": 0, "data-size": 4, "font-size": 4, "font-family": "Courier New" @@ -380,6 +382,7 @@ "stroke": "#3a3a3a", "stroke-width": 0, "text-shadow": "white 0px 0px 4px", + "letter-spacing": 0, "data-size": 18, "font-size": 18, "font-family": "Courier New", @@ -390,7 +393,8 @@ "fill": "#3e3e4b", "stroke": "#3a3a3a", "stroke-width": 0, - "text-shadow": "white 0 0 4px", + "text-shadow": "white 0px 0px 4px", + "letter-spacing": 0, "data-size": 18, "font-size": 18, "font-family": "Courier New", diff --git a/styles/night.json b/styles/night.json index 90b40e75..67a5e799 100644 --- a/styles/night.json +++ b/styles/night.json @@ -332,6 +332,7 @@ "opacity": 1, "fill": "#dbdbe1", "text-shadow": "black 0px 0px 4px", + "letter-spacing": 0, "data-size": 8, "font-size": 8, "font-family": "Courier New" @@ -357,6 +358,7 @@ "opacity": 1, "fill": "#ffffff", "text-shadow": "black 0px 0px 4px", + "letter-spacing": 0, "data-size": 4.28, "font-size": 4.28, "font-family": "Courier New" @@ -384,6 +386,7 @@ "stroke": "#7a83ae", "stroke-width": 0.3, "text-shadow": "black 0px 0px 0.1px", + "letter-spacing": 0, "data-size": 14, "font-size": 14, "font-family": "Courier New", @@ -394,7 +397,8 @@ "fill": "#3e3e4b", "stroke": "#3a3a3a", "stroke-width": 0, - "text-shadow": "white 0px 0px 4px", + "text-shadow": "black 0px 0px 4px", + "letter-spacing": 0, "data-size": 18, "font-size": 18, "font-family": "Almendra SC", diff --git a/styles/pale.json b/styles/pale.json index 312009b9..8e839600 100644 --- a/styles/pale.json +++ b/styles/pale.json @@ -332,6 +332,7 @@ "opacity": 0.8, "fill": "#3a3a3a", "text-shadow": "white 0px 0px 4px", + "letter-spacing": 0, "data-size": 7, "font-size": 7, "font-family": "Arima Madurai" @@ -357,6 +358,7 @@ "opacity": 0.8, "fill": "#3e3e4b", "text-shadow": "white 0px 0px 4px", + "letter-spacing": 0, "data-size": 4, "font-size": 4, "font-family": "Arima Madurai" @@ -384,6 +386,7 @@ "stroke": "#000000", "stroke-width": 0, "text-shadow": "white 0px 0px 6px", + "letter-spacing": 0, "data-size": 14, "font-size": 14, "font-family": "Arima Madurai", @@ -395,6 +398,7 @@ "stroke": "#701b05", "stroke-width": 0.1, "text-shadow": "white 0px 0px 4px", + "letter-spacing": 0, "data-size": 6, "font-size": 6, "font-family": "Arima Madurai", diff --git a/styles/watercolor.json b/styles/watercolor.json index 79cc9484..982c1b49 100644 --- a/styles/watercolor.json +++ b/styles/watercolor.json @@ -332,6 +332,7 @@ "opacity": 1, "fill": "#043449", "text-shadow": "white 0px 0px 2px", + "letter-spacing": 0, "data-size": 5, "font-size": 5, "font-family": "Comfortaa" @@ -357,6 +358,7 @@ "opacity": 1, "fill": "#3e3e4b", "text-shadow": "white 0px 0px 4px", + "letter-spacing": 0, "data-size": 3, "font-size": 3, "font-family": "Comfortaa" @@ -384,6 +386,7 @@ "stroke": "#000000", "stroke-width": 0.15, "text-shadow": "black 1px 1px 3px", + "letter-spacing": 0, "data-size": 18, "font-size": 18, "font-family": "Gloria Hallelujah", @@ -395,6 +398,7 @@ "stroke": "#3a3a3a", "stroke-width": 0, "text-shadow": "white 0px 0px 4px", + "letter-spacing": 0, "data-size": 16, "font-size": 16, "font-family": "Comfortaa", diff --git a/utils/pathUtils.js b/utils/pathUtils.js new file mode 100644 index 00000000..ff3bbf2a --- /dev/null +++ b/utils/pathUtils.js @@ -0,0 +1,155 @@ +"use strict"; + +// get continuous paths for all cells at once based on getType(cellId) comparison +function getVertexPaths({getType, options}) { + const {cells, vertices} = pack; + const paths = {}; + + const checkedCells = new Uint8Array(cells.c.length); + const addToChecked = cellId => (checkedCells[cellId] = 1); + const isChecked = cellId => checkedCells[cellId] === 1; + + for (let cellId = 0; cellId < cells.c.length; cellId++) { + if (isChecked(cellId) || getType(cellId) === 0) continue; + addToChecked(cellId); + + const type = getType(cellId); + const ofSameType = cellId => getType(cellId) === type; + const ofDifferentType = cellId => getType(cellId) !== type; + + const onborderCell = cells.c[cellId].find(ofDifferentType); + if (onborderCell === undefined) continue; + + const feature = pack.features[cells.f[onborderCell]]; + if (feature.type === "lake" && feature.shoreline.every(ofSameType)) continue; // inner lake + + const startingVertex = cells.v[cellId].find(v => vertices.c[v].some(ofDifferentType)); + if (startingVertex === undefined) throw new Error(`Starting vertex for cell ${cellId} is not found`); + + const vertexChain = connectVertices({startingVertex, ofSameType, addToChecked, closeRing: true}); + if (vertexChain.length < 3) continue; + + addPath(type, vertexChain); + } + + return Object.entries(paths); + + function getBorderPath(vertexChain, discontinue) { + let discontinued = true; + let lastOperation = ""; + const path = vertexChain.map(vertex => { + if (discontinue(vertex)) { + discontinued = true; + return ""; + } + + const operation = discontinued ? "M" : "L"; + const command = operation === lastOperation ? "" : operation; + + discontinued = false; + lastOperation = operation; + + return ` ${command}${getVertexPoint(vertex)}`; + }); + + return path.join("").trim(); + } + + function isBorderVertex(vertex) { + const adjacentCells = vertices.c[vertex]; + return adjacentCells.some(i => cells.b[i]); + } + + function isLandVertex(vertex) { + const adjacentCells = vertices.c[vertex]; + return adjacentCells.every(i => cells.h[i] >= MIN_LAND_HEIGHT); + } + + function addPath(index, vertexChain) { + if (!paths[index]) paths[index] = {fill: "", waterGap: "", halo: ""}; + if (options.fill) paths[index].fill += getFillPath(vertexChain); + if (options.halo) paths[index].halo += getBorderPath(vertexChain, isBorderVertex); + if (options.waterGap) paths[index].waterGap += getBorderPath(vertexChain, isLandVertex); + } +} + +function getVertexPoint(vertexId) { + return pack.vertices.p[vertexId]; +} + +function getFillPath(vertexChain) { + const points = vertexChain.map(getVertexPoint); + const firstPoint = points.shift(); + return `M${firstPoint} L${points.join(" ")}`; +} + +// get single path for an non-continuous array of cells +function getVertexPath(cellsArray) { + const {cells, vertices} = pack; + + const cellsObj = Object.fromEntries(cellsArray.map(cellId => [cellId, true])); + const ofSameType = cellId => cellsObj[cellId]; + const ofDifferentType = cellId => !cellsObj[cellId]; + + const checkedCells = new Uint8Array(cells.c.length); + const addToChecked = cellId => (checkedCells[cellId] = 1); + const isChecked = cellId => checkedCells[cellId] === 1; + + let path = ""; + + for (const cellId of cellsArray) { + if (isChecked(cellId)) continue; + + const onborderCell = cells.c[cellId].find(ofDifferentType); + if (onborderCell === undefined) continue; + + const feature = pack.features[cells.f[onborderCell]]; + if (feature.type === "lake" && feature.shoreline.every(ofSameType)) continue; // inner lake + + const startingVertex = cells.v[cellId].find(v => vertices.c[v].some(ofDifferentType)); + if (startingVertex === undefined) throw new Error(`Starting vertex for cell ${cellId} is not found`); + + const vertexChain = connectVertices({startingVertex, ofSameType, addToChecked, closeRing: true}); + if (vertexChain.length < 3) continue; + + path += getFillPath(vertexChain); + } + + return path; +} + +function connectVertices({startingVertex, ofSameType, addToChecked, closeRing}) { + const vertices = pack.vertices; + const MAX_ITERATIONS = pack.cells.i.length; + const chain = []; // vertices chain to form a path + + let next = startingVertex; + for (let i = 0; i === 0 || next !== startingVertex; i++) { + const previous = chain.at(-1); + const current = next; + chain.push(current); + + const neibCells = vertices.c[current]; + if (addToChecked) neibCells.filter(ofSameType).forEach(addToChecked); + + const [c1, c2, c3] = neibCells.map(ofSameType); + const [v1, v2, v3] = vertices.v[current]; + + if (v1 !== previous && c1 !== c2) next = v1; + else if (v2 !== previous && c2 !== c3) next = v2; + else if (v3 !== previous && c1 !== c3) next = v3; + + if (next === current) { + ERROR && console.error("ConnectVertices: next vertex is not found"); + break; + } + + if (i === MAX_ITERATIONS) { + ERROR && console.error("ConnectVertices: max iterations reached", MAX_ITERATIONS); + break; + } + } + + if (closeRing) chain.push(startingVertex); + return chain; +} diff --git a/versioning.js b/versioning.js index ce4aa000..8a5f9393 100644 --- a/versioning.js +++ b/versioning.js @@ -1,21 +1,26 @@ "use strict"; - -// version and caching control -const version = "1.99.11"; // generator version, update each time +/** + * Version Control Guidelines + * -------------------------- + * We use Semantic Versioning: major.minor.patch. Refer to https://semver.org + * Our .map file format is considered the public API. + * + * Update the version MANUALLY on each merge to main: + * 1. MAJOR version: Incompatible changes that break existing maps + * 2. MINOR version: Backwards-compatible changes requiring old .map files to be updated + * 3. PATCH version: Backwards-compatible bug fixes not affecting .map file format + * + * Example: 1.102.0 -> Major version 1, Minor version 102, Patch version 0 + */ +const VERSION = "1.101.00"; { - document.title += " v" + version; + document.title += " v" + VERSION; const loadingScreenVersion = document.getElementById("versionText"); - if (loadingScreenVersion) loadingScreenVersion.innerText = `v${version}`; + if (loadingScreenVersion) loadingScreenVersion.innerText = `v${VERSION}`; - const versionNumber = parseFloat(version); - const storedVersion = localStorage.getItem("version") ? parseFloat(localStorage.getItem("version")) : 0; - - const isOutdated = storedVersion !== versionNumber; - if (isOutdated) clearCache(); - - const showUpdate = storedVersion < versionNumber; - if (showUpdate) setTimeout(showUpdateWindow, 6000); + const storedVersion = localStorage.getItem("version"); + if (compareVersions(storedVersion, VERSION, {patch: false}).isOlder) setTimeout(showUpdateWindow, 6000); function showUpdateWindow() { const changelog = "https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog"; @@ -23,11 +28,13 @@ const version = "1.99.11"; // generator version, update each time const discord = "https://discordapp.com/invite/X7E84HU"; const patreon = "https://www.patreon.com/azgaar"; - alertMessage.innerHTML = /* html */ `The Fantasy Map Generator is updated up to version ${version}. This version is compatible with previous versions, loaded save files will be auto-updated. - ${storedVersion ? "Reload the page to fetch fresh code." : ""} + alertMessage.innerHTML = /* html */ `The Fantasy Map Generator is updated up to version ${VERSION}. This version is compatible with previous versions, loaded save files will be auto-updated. + ${storedVersion ? "Click on OK and then reload the page to fetch fresh code." : ""}

Join our Discord server and Reddit community to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.

@@ -51,15 +52,15 @@ const version = "1.99.11"; // generator version, update each time const buttons = { Ok: function () { $(this).dialog("close"); - if (storedVersion) localStorage.clear(); - localStorage.setItem("version", version); + localStorage.setItem("version", VERSION); } }; if (storedVersion) { - buttons.Reload = () => { + buttons.Cleanup = () => { + clearCache(); localStorage.clear(); - localStorage.setItem("version", version); + localStorage.setItem("version", VERSION); location.reload(); }; } @@ -75,6 +76,29 @@ const version = "1.99.11"; // generator version, update each time async function clearCache() { const cacheNames = await caches.keys(); - Promise.all(cacheNames.map(cacheName => caches.delete(cacheName))); + return Promise.all(cacheNames.map(cacheName => caches.delete(cacheName))); } } + +function isValidVersion(versionString) { + if (!versionString) return false; + const [major, minor, patch] = versionString.split("."); + return !isNaN(major) && !isNaN(minor) && !isNaN(patch); +} + +function compareVersions(version1, version2, options = {major: true, minor: true, patch: true}) { + if (!isValidVersion(version1) || !isValidVersion(version2)) return {isEqual: false, isNewer: false, isOlder: false}; + + let [major1, minor1, patch1] = version1.split(".").map(Number); + let [major2, minor2, patch2] = version2.split(".").map(Number); + + if (!options.major) major1 = major2 = 0; + if (!options.minor) minor1 = minor2 = 0; + if (!options.patch) patch1 = patch2 = 0; + + const isEqual = major1 === major2 && minor1 === minor2 && patch1 === patch2; + const isNewer = major1 > major2 || (major1 === major2 && (minor1 > minor2 || (minor1 === minor2 && patch1 > patch2))); + const isOlder = major1 < major2 || (major1 === major2 && (minor1 < minor2 || (minor1 === minor2 && patch1 < patch2))); + + return {isEqual, isNewer, isOlder}; +}