diff --git a/index.html b/index.html index f29e9465..5c440e2e 100644 --- a/index.html +++ b/index.html @@ -4504,11 +4504,6 @@ - - - - - @@ -4554,6 +4549,12 @@ + + + + + + diff --git a/modules/io/auto-update.js b/modules/io/auto-update.js new file mode 100644 index 00000000..064af0ad --- /dev/null +++ b/modules/io/auto-update.js @@ -0,0 +1,523 @@ +"use strict"; + +// update old .map version to the current one +export function resolveVersionConflicts(version) { + if (version < 1) { + // v1.0 added a new religions layer + relig = viewbox.insert("g", "#terrain").attr("id", "relig"); + Religions.generate(); + + // v1.0 added a legend box + legend = svg.append("g").attr("id", "legend"); + legend + .attr("font-family", "Almendra SC") + .attr("font-size", 13) + .attr("data-size", 13) + .attr("data-x", 99) + .attr("data-y", 93) + .attr("stroke-width", 2.5) + .attr("stroke", "#812929") + .attr("stroke-dasharray", "0 4 10 4") + .attr("stroke-linecap", "round"); + + // v1.0 separated drawBorders fron drawStates() + stateBorders = borders.append("g").attr("id", "stateBorders"); + provinceBorders = borders.append("g").attr("id", "provinceBorders"); + borders + .attr("opacity", null) + .attr("stroke", null) + .attr("stroke-width", null) + .attr("stroke-dasharray", null) + .attr("stroke-linecap", null) + .attr("filter", null); + stateBorders.attr("opacity", 0.8).attr("stroke", "#56566d").attr("stroke-width", 1).attr("stroke-dasharray", "2").attr("stroke-linecap", "butt"); + provinceBorders.attr("opacity", 0.8).attr("stroke", "#56566d").attr("stroke-width", 0.5).attr("stroke-dasharray", "1").attr("stroke-linecap", "butt"); + + // v1.0 added state relations, provinces, forms and full names + provs = viewbox.insert("g", "#borders").attr("id", "provs").attr("opacity", 0.6); + BurgsAndStates.collectStatistics(); + BurgsAndStates.generateCampaigns(); + BurgsAndStates.generateDiplomacy(); + BurgsAndStates.defineStateForms(); + drawStates(); + BurgsAndStates.generateProvinces(); + drawBorders(); + if (!layerIsOn("toggleBorders")) $("#borders").fadeOut(); + if (!layerIsOn("toggleStates")) regions.attr("display", "none").selectAll("path").remove(); + + // v1.0 added zones layer + zones = viewbox.insert("g", "#borders").attr("id", "zones").attr("display", "none"); + zones.attr("opacity", 0.6).attr("stroke", null).attr("stroke-width", 0).attr("stroke-dasharray", null).attr("stroke-linecap", "butt"); + addZones(); + if (!markers.selectAll("*").size()) { + Markers.generate(); + turnButtonOn("toggleMarkers"); + } + + // v1.0 add fogging layer (state focus) + fogging = viewbox.insert("g", "#ruler").attr("id", "fogging-cont").attr("mask", "url(#fog)").append("g").attr("id", "fogging").style("display", "none"); + fogging.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%"); + defs.append("mask").attr("id", "fog").append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%").attr("fill", "white"); + + // v1.0 changes states opacity bask to regions level + if (statesBody.attr("opacity")) { + regions.attr("opacity", statesBody.attr("opacity")); + statesBody.attr("opacity", null); + } + + // v1.0 changed labels to multi-lined + labels.selectAll("textPath").each(function () { + const text = this.textContent; + const shift = this.getComputedTextLength() / -1.5; + this.innerHTML = `${text}`; + }); + + // v1.0 added new biome - Wetland + biomesData.name.push("Wetland"); + biomesData.color.push("#0b9131"); + biomesData.habitability.push(12); + } + + if (version < 1.1) { + // v1.0 initial 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 + for (const s of pack.states) { + if (!s.diplomacy) continue; + s.diplomacy = s.diplomacy.map(r => (r === "Sympathy" ? "Friendly" : r)); + } + + // labels should be toggled via style attribute, so remove display attribute + labels.attr("display", null); + + // v1.0 added religions heirarchy tree + if (pack.religions[1] && !pack.religions[1].code) { + pack.religions + .filter(r => r.i) + .forEach(r => { + r.origin = 0; + r.code = r.name.slice(0, 2); + }); + } + + if (!document.getElementById("freshwater")) { + lakes.append("g").attr("id", "freshwater"); + lakes.select("#freshwater").attr("opacity", 0.5).attr("fill", "#a6c1fd").attr("stroke", "#5f799d").attr("stroke-width", 0.7).attr("filter", null); + } + + if (!document.getElementById("salt")) { + lakes.append("g").attr("id", "salt"); + lakes.select("#salt").attr("opacity", 0.5).attr("fill", "#409b8a").attr("stroke", "#388985").attr("stroke-width", 0.7).attr("filter", null); + } + + // v1.1 added new lake and coast groups + if (!document.getElementById("sinkhole")) { + lakes.append("g").attr("id", "sinkhole"); + lakes.append("g").attr("id", "frozen"); + lakes.append("g").attr("id", "lava"); + lakes.select("#sinkhole").attr("opacity", 1).attr("fill", "#5bc9fd").attr("stroke", "#53a3b0").attr("stroke-width", 0.7).attr("filter", null); + lakes.select("#frozen").attr("opacity", 0.95).attr("fill", "#cdd4e7").attr("stroke", "#cfe0eb").attr("stroke-width", 0).attr("filter", null); + lakes.select("#lava").attr("opacity", 0.7).attr("fill", "#90270d").attr("stroke", "#f93e0c").attr("stroke-width", 2).attr("filter", "url(#crumpled)"); + + coastline.append("g").attr("id", "sea_island"); + coastline.append("g").attr("id", "lake_island"); + coastline.select("#sea_island").attr("opacity", 0.5).attr("stroke", "#1f3846").attr("stroke-width", 0.7).attr("filter", "url(#dropShadow)"); + coastline.select("#lake_island").attr("opacity", 1).attr("stroke", "#7c8eaf").attr("stroke-width", 0.35).attr("filter", null); + } + + // v1.1 features stores more data + defs.select("#land").selectAll("path").remove(); + defs.select("#water").selectAll("path").remove(); + coastline.selectAll("path").remove(); + lakes.selectAll("path").remove(); + drawCoastline(); + } + + if (version < 1.11) { + // 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"); + oceanLayers.attr("layers", "-6,-3,-1"); + gridOverlay.attr("type", "pointyHex").attr("size", 10); + + // v1.11 added cultures heirarchy tree + if (pack.cultures[1] && !pack.cultures[1].code) { + pack.cultures + .filter(c => c.i) + .forEach(c => { + c.origin = 0; + c.code = c.name.slice(0, 2); + }); + } + + // v1.11 had an issue with fogging being displayed on load + unfog(); + + // v1.2 added new terrain attributes + if (!terrain.attr("set")) terrain.attr("set", "simple"); + if (!terrain.attr("size")) terrain.attr("size", 1); + if (!terrain.attr("density")) terrain.attr("density", 0.4); + } + + if (version < 1.21) { + // v1.11 replaced "display" attribute by "display" style + viewbox.selectAll("g").each(function () { + if (this.hasAttribute("display")) { + this.removeAttribute("display"); + this.style.display = "none"; + } + }); + + // v1.21 added rivers data to pack + pack.rivers = []; // rivers data + rivers.selectAll("path").each(function () { + const i = +this.id.slice(5); + const length = this.getTotalLength() / 2; + const s = this.getPointAtLength(length), + e = this.getPointAtLength(0); + const source = findCell(s.x, s.y), + mouth = findCell(e.x, e.y); + const name = Rivers.getName(mouth); + const type = length < 25 ? rw({Creek: 9, River: 3, Brook: 3, Stream: 1}) : "River"; + pack.rivers.push({i, parent: 0, length, source, mouth, basin: i, name, type}); + }); + } + + if (version < 1.22) { + // v1.22 changed state neighbors from Set object to array + BurgsAndStates.collectStatistics(); + } + + if (version < 1.3) { + // v1.3 added global options object + const winds = options.slice(); // previostly wind was saved in settings[19] + const year = rand(100, 2000); + const era = Names.getBaseShort(P(0.7) ? 1 : rand(nameBases.length)) + " Era"; + const eraShort = era[0] + "E"; + const military = Military.getDefaultOptions(); + options = {winds, year, era, eraShort, military}; + + // v1.3 added campaings data for all states + BurgsAndStates.generateCampaigns(); + + // v1.3 added militry layer + armies = viewbox.insert("g", "#icons").attr("id", "armies"); + armies.attr("opacity", 1).attr("fill-opacity", 1).attr("font-size", 6).attr("box-size", 3).attr("stroke", "#000").attr("stroke-width", 0.3); + turnButtonOn("toggleMilitary"); + Military.generate(); + } + + if (version < 1.4) { + // v1.35 added dry lakes + if (!lakes.select("#dry").size()) { + lakes.append("g").attr("id", "dry"); + lakes.select("#dry").attr("opacity", 1).attr("fill", "#c9bfa7").attr("stroke", "#8e816f").attr("stroke-width", 0.7).attr("filter", null); + } + + // v1.4 added ice layer + ice = viewbox.insert("g", "#coastline").attr("id", "ice").style("display", "none"); + ice.attr("opacity", null).attr("fill", "#e8f0f6").attr("stroke", "#e8f0f6").attr("stroke-width", 1).attr("filter", "url(#dropShadow05)"); + drawIce(); + + // v1.4 added icon and power attributes for units + for (const unit of options.military) { + if (!unit.icon) unit.icon = getUnitIcon(unit.type); + if (!unit.power) unit.power = unit.crew; + } + + function getUnitIcon(type) { + if (type === "naval") return "🌊"; + if (type === "ranged") return "🏹"; + if (type === "mounted") return "🐴"; + if (type === "machinery") return "💣"; + if (type === "armored") return "🐢"; + if (type === "aviation") return "🦅"; + if (type === "magical") return "🔮"; + else return "⚔️"; + } + + // v1.4 added state reference for regiments + pack.states.filter(s => s.military).forEach(s => s.military.forEach(r => (r.state = s.i))); + } + + if (version < 1.5) { + // not need to store default styles from v 1.5 + localStorage.removeItem("styleClean"); + localStorage.removeItem("styleGloom"); + localStorage.removeItem("styleAncient"); + localStorage.removeItem("styleMonochrome"); + + // v1.5 cultures has shield attribute + pack.cultures.forEach(culture => { + if (culture.removed) return; + culture.shield = Cultures.getRandomShield(); + }); + + // v1.5 added burg type value + pack.burgs.forEach(burg => { + if (!burg.i || burg.removed) return; + burg.type = BurgsAndStates.getType(burg.cell, burg.port); + }); + + // v1.5 added emblems + defs.append("g").attr("id", "defs-emblems"); + emblems = viewbox.insert("g", "#population").attr("id", "emblems").style("display", "none"); + emblems.append("g").attr("id", "burgEmblems"); + emblems.append("g").attr("id", "provinceEmblems"); + emblems.append("g").attr("id", "stateEmblems"); + regenerateEmblems(); + toggleEmblems(); + + // v1.5 changed releif icons data + terrain.selectAll("use").each(function () { + const type = this.getAttribute("data-type") || this.getAttribute("xlink:href"); + this.removeAttribute("xlink:href"); + this.removeAttribute("data-type"); + this.removeAttribute("data-size"); + this.setAttribute("href", type); + }); + } + + if (version < 1.6) { + // v1.6 changed rivers data + for (const river of pack.rivers) { + const el = document.getElementById("river" + river.i); + if (el) { + river.widthFactor = +el.getAttribute("data-width"); + el.removeAttribute("data-width"); + el.removeAttribute("data-increment"); + river.discharge = pack.cells.fl[river.mouth] || 1; + river.width = rn(river.length / 100, 2); + river.sourceWidth = 0.1; + } else { + Rivers.remove(river.i); + } + } + + // v1.6 changed lakes data + for (const f of pack.features) { + if (f.type !== "lake") continue; + if (f.evaporation) continue; + + f.flux = f.flux || f.cells * 3; + f.temp = grid.cells.temp[pack.cells.g[f.firstCell]]; + f.height = f.height || d3.min(pack.cells.c[f.firstCell].map(c => pack.cells.h[c]).filter(h => h >= 20)); + const height = (f.height - 18) ** heightExponentInput.value; + const evaporation = ((700 * (f.temp + 0.006 * height)) / 50 + 75) / (80 - f.temp); + f.evaporation = rn(evaporation * f.cells); + f.name = f.name || Lakes.getName(f); + delete f.river; + } + } + + if (version < 1.61) { + // v1.61 changed rulers data + ruler.style("display", null); + rulers = new Rulers(); + + ruler.selectAll(".ruler > .white").each(function () { + const x1 = +this.getAttribute("x1"); + const y1 = +this.getAttribute("y1"); + const x2 = +this.getAttribute("x2"); + const y2 = +this.getAttribute("y2"); + if (isNaN(x1) || isNaN(y1) || isNaN(x2) || isNaN(y2)) return; + const points = [ + [x1, y1], + [x2, y2] + ]; + rulers.create(Ruler, points); + }); + + ruler.selectAll("g.opisometer").each(function () { + const pointsString = this.dataset.points; + if (!pointsString) return; + const points = JSON.parse(pointsString); + rulers.create(Opisometer, points); + }); + + ruler.selectAll("path.planimeter").each(function () { + const length = this.getTotalLength(); + if (length < 30) return; + + const step = length > 1000 ? 40 : length > 400 ? 20 : 10; + const increment = length / Math.ceil(length / step); + const points = []; + for (let i = 0; i <= length; i += increment) { + const point = this.getPointAtLength(i); + points.push([point.x | 0, point.y | 0]); + } + + rulers.create(Planimeter, points); + }); + + ruler.selectAll("*").remove(); + + if (rulers.data.length) { + turnButtonOn("toggleRulers"); + rulers.draw(); + } else turnButtonOff("toggleRulers"); + + // 1.61 changed oceanicPattern from rect to image + const pattern = document.getElementById("oceanic"); + const filter = pattern.firstElementChild.getAttribute("filter"); + const href = filter ? "./images/" + filter.replace("url(#", "").replace(")", "") + ".png" : ""; + pattern.innerHTML = ``; + } + + if (version < 1.62) { + // v1.62 changed grid data + gridOverlay.attr("size", null); + } + + if (version < 1.63) { + // v1.63 changed ocean pattern opacity element + const oceanPattern = document.getElementById("oceanPattern"); + if (oceanPattern) oceanPattern.removeAttribute("opacity"); + const oceanicPattern = document.getElementById("oceanicPattern"); + if (!oceanicPattern.getAttribute("opacity")) oceanicPattern.setAttribute("opacity", 0.2); + + // v 1.63 moved label text-shadow from css to editable inline style + burgLabels.select("#cities").style("text-shadow", "white 0 0 4px"); + burgLabels.select("#towns").style("text-shadow", "white 0 0 4px"); + labels.select("#states").style("text-shadow", "white 0 0 4px"); + labels.select("#addedLabels").style("text-shadow", "white 0 0 4px"); + } + + if (version < 1.64) { + // v1.64 change states style + const opacity = regions.attr("opacity"); + const filter = regions.attr("filter"); + statesBody.attr("opacity", opacity).attr("filter", filter); + statesHalo.attr("opacity", opacity).attr("filter", "blur(5px)"); + regions.attr("opacity", null).attr("filter", null); + } + + if (version < 1.65) { + // v1.65 changed rivers data + d3.select("#rivers").attr("style", null); // remove style to unhide layer + const {cells, rivers} = pack; + const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2); + + for (const river of rivers) { + const node = document.getElementById("river" + river.i); + if (node && !river.cells) { + const riverCells = []; + const riverPoints = []; + + const length = node.getTotalLength() / 2; + if (!length) continue; + const segments = Math.ceil(length / 6); + const increment = length / segments; + + for (let i = 0; i <= segments; i++) { + const shift = increment * i; + const {x: x1, y: y1} = node.getPointAtLength(length + shift); + const {x: x2, y: y2} = node.getPointAtLength(length - shift); + const x = rn((x1 + x2) / 2, 1); + const y = rn((y1 + y2) / 2, 1); + + const cell = findCell(x, y); + riverPoints.push([x, y]); + riverCells.push(cell); + } + + river.cells = riverCells; + river.points = riverPoints; + } + + river.widthFactor = defaultWidthFactor; + + cells.i.forEach(i => { + const riverInWater = cells.r[i] && cells.h[i] < 20; + if (riverInWater) cells.r[i] = 0; + }); + } + } + + if (version < 1.652) { + // remove style to unhide layers + rivers.attr("style", null); + borders.attr("style", null); + } + + if (version < 1.7) { + // v1.7 changed markers data + const defs = document.getElementById("defs-markers"); + const markersGroup = document.getElementById("markers"); + + if (defs && markersGroup) { + const markerElements = markersGroup.querySelectorAll("use"); + const rescale = +markersGroup.getAttribute("rescale"); + + pack.markers = Array.from(markerElements).map((el, i) => { + const id = el.getAttribute("id"); + const note = notes.find(note => note.id === id); + if (note) note.id = `marker${i}`; + + let x = +el.dataset.x; + let y = +el.dataset.y; + + const transform = el.getAttribute("transform"); + if (transform) { + const [dx, dy] = parseTransform(transform); + if (dx) x += +dx; + if (dy) y += +dy; + } + const cell = findCell(x, y); + const size = rn(rescale ? el.dataset.size * 30 : el.getAttribute("width"), 1); + + const href = el.href.baseVal; + const type = href.replace("#marker_", ""); + const symbol = defs?.querySelector(`symbol${href}`); + const text = symbol?.querySelector("text"); + const circle = symbol?.querySelector("circle"); + + const icon = text?.innerHTML; + const px = text && Number(text.getAttribute("font-size")?.replace("px", "")); + const dx = text && Number(text.getAttribute("x")?.replace("%", "")); + const dy = text && Number(text.getAttribute("y")?.replace("%", "")); + const fill = circle && circle.getAttribute("fill"); + const stroke = circle && circle.getAttribute("stroke"); + + const marker = {i, icon, type, x, y, size, cell}; + if (size && size !== 30) marker.size = size; + if (!isNaN(px) && px !== 12) marker.px = px; + if (!isNaN(dx) && dx !== 50) marker.dx = dx; + if (!isNaN(dy) && dy !== 50) marker.dy = dy; + if (fill && fill !== "#ffffff") marker.fill = fill; + if (stroke && stroke !== "#000000") marker.stroke = stroke; + if (circle?.getAttribute("opacity") === "0") marker.pin = "no"; + + return marker; + }); + + markersGroup.style.display = null; + defs?.remove(); + markerElements.forEach(el => el.remove()); + if (layerIsOn("markers")) drawMarkers(); + } + } + + if (version < 1.72) { + // v1.72 renamed custom style presets + const storedStyles = Object.keys(localStorage).filter(key => key.startsWith("style")); + storedStyles.forEach(styleName => { + const style = localStorage.getItem(styleName); + const newStyleName = styleName.replace(/^style/, customPresetPrefix); + localStorage.setItem(newStyleName, style); + localStorage.removeItem(styleName); + }); + } + + if (version < 1.73) { + // v1.73 moved the hatching patterns out of the user's SVG + document.getElementById("hatching")?.remove(); + + // v1.73 added zone type to UI, ensure type is populated + const zones = Array.from(document.querySelectorAll("#zones > g")); + zones.forEach(zone => { + if (!zone.dataset.type) zone.dataset.type = "Unknown"; + }); + } +} diff --git a/modules/cloud.js b/modules/io/cloud.js similarity index 100% rename from modules/cloud.js rename to modules/io/cloud.js diff --git a/modules/export-json.js b/modules/io/export-json.js similarity index 100% rename from modules/export-json.js rename to modules/io/export-json.js diff --git a/modules/export.js b/modules/io/export.js similarity index 100% rename from modules/export.js rename to modules/io/export.js diff --git a/modules/load.js b/modules/io/load.js similarity index 52% rename from modules/load.js rename to modules/io/load.js index a8893592..9ff94d33 100644 --- a/modules/load.js +++ b/modules/io/load.js @@ -182,17 +182,15 @@ function showUploadMessage(type, mapData, mapVersion) { $("#alert").dialog({title, buttons}); } -function parseLoadedData(data) { +async function parseLoadedData(data) { try { // exit customization if (window.closeDialogs) closeDialogs(); customization = 0; if (customizationMenu.offsetParent) styleTab.click(); - const reliefIcons = document.getElementById("defs-relief").innerHTML; // save relief icons - + const params = data[0].split("|"); void (function parseParameters() { - const params = data[0].split("|"); if (params[3]) { seed = params[3]; optionsSeed.value = seed; @@ -422,532 +420,12 @@ function parseLoadedData(data) { legend.on("mousemove", () => tip("Drag to change the position. Click to hide the legend")).on("click", () => clearLegend()); })(); - void (function resolveVersionConflicts() { - const version = parseFloat(data[0].split("|")[0]); - if (version < 0.9) { - // v0.9 has additional relief icons to be included into older maps - document.getElementById("defs-relief").innerHTML = reliefIcons; - } - - if (version < 1) { - // v1.0 adds a new religions layer - relig = viewbox.insert("g", "#terrain").attr("id", "relig"); - Religions.generate(); - - // v1.0 adds a legend box - legend = svg.append("g").attr("id", "legend"); - legend - .attr("font-family", "Almendra SC") - .attr("font-size", 13) - .attr("data-size", 13) - .attr("data-x", 99) - .attr("data-y", 93) - .attr("stroke-width", 2.5) - .attr("stroke", "#812929") - .attr("stroke-dasharray", "0 4 10 4") - .attr("stroke-linecap", "round"); - - // v1.0 separated drawBorders fron drawStates() - stateBorders = borders.append("g").attr("id", "stateBorders"); - provinceBorders = borders.append("g").attr("id", "provinceBorders"); - borders - .attr("opacity", null) - .attr("stroke", null) - .attr("stroke-width", null) - .attr("stroke-dasharray", null) - .attr("stroke-linecap", null) - .attr("filter", null); - stateBorders.attr("opacity", 0.8).attr("stroke", "#56566d").attr("stroke-width", 1).attr("stroke-dasharray", "2").attr("stroke-linecap", "butt"); - provinceBorders.attr("opacity", 0.8).attr("stroke", "#56566d").attr("stroke-width", 0.5).attr("stroke-dasharray", "1").attr("stroke-linecap", "butt"); - - // v1.0 adds state relations, provinces, forms and full names - provs = viewbox.insert("g", "#borders").attr("id", "provs").attr("opacity", 0.6); - BurgsAndStates.collectStatistics(); - BurgsAndStates.generateCampaigns(); - BurgsAndStates.generateDiplomacy(); - BurgsAndStates.defineStateForms(); - drawStates(); - BurgsAndStates.generateProvinces(); - drawBorders(); - if (!layerIsOn("toggleBorders")) $("#borders").fadeOut(); - if (!layerIsOn("toggleStates")) regions.attr("display", "none").selectAll("path").remove(); - - // v1.0 adds zones layer - zones = viewbox.insert("g", "#borders").attr("id", "zones").attr("display", "none"); - zones.attr("opacity", 0.6).attr("stroke", null).attr("stroke-width", 0).attr("stroke-dasharray", null).attr("stroke-linecap", "butt"); - addZones(); - if (!markers.selectAll("*").size()) { - Markers.generate(); - turnButtonOn("toggleMarkers"); - } - - // v1.0 add fogging layer (state focus) - fogging = viewbox.insert("g", "#ruler").attr("id", "fogging-cont").attr("mask", "url(#fog)").append("g").attr("id", "fogging").style("display", "none"); - fogging.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%"); - defs.append("mask").attr("id", "fog").append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%").attr("fill", "white"); - - // v1.0 changes states opacity bask to regions level - if (statesBody.attr("opacity")) { - regions.attr("opacity", statesBody.attr("opacity")); - statesBody.attr("opacity", null); - } - - // v1.0 changed labels to multi-lined - labels.selectAll("textPath").each(function () { - const text = this.textContent; - const shift = this.getComputedTextLength() / -1.5; - this.innerHTML = `${text}`; - }); - - // v1.0 added new biome - Wetland - biomesData.name.push("Wetland"); - biomesData.color.push("#0b9131"); - biomesData.habitability.push(12); - } - - if (version < 1.1) { - // v1.0 initial 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 - for (const s of pack.states) { - if (!s.diplomacy) continue; - s.diplomacy = s.diplomacy.map(r => (r === "Sympathy" ? "Friendly" : r)); - } - - // labels should be toggled via style attribute, so remove display attribute - labels.attr("display", null); - - // v1.0 added religions heirarchy tree - if (pack.religions[1] && !pack.religions[1].code) { - pack.religions - .filter(r => r.i) - .forEach(r => { - r.origin = 0; - r.code = r.name.slice(0, 2); - }); - } - - if (!document.getElementById("freshwater")) { - lakes.append("g").attr("id", "freshwater"); - lakes.select("#freshwater").attr("opacity", 0.5).attr("fill", "#a6c1fd").attr("stroke", "#5f799d").attr("stroke-width", 0.7).attr("filter", null); - } - - if (!document.getElementById("salt")) { - lakes.append("g").attr("id", "salt"); - lakes.select("#salt").attr("opacity", 0.5).attr("fill", "#409b8a").attr("stroke", "#388985").attr("stroke-width", 0.7).attr("filter", null); - } - - // v1.1 added new lake and coast groups - if (!document.getElementById("sinkhole")) { - lakes.append("g").attr("id", "sinkhole"); - lakes.append("g").attr("id", "frozen"); - lakes.append("g").attr("id", "lava"); - lakes.select("#sinkhole").attr("opacity", 1).attr("fill", "#5bc9fd").attr("stroke", "#53a3b0").attr("stroke-width", 0.7).attr("filter", null); - lakes.select("#frozen").attr("opacity", 0.95).attr("fill", "#cdd4e7").attr("stroke", "#cfe0eb").attr("stroke-width", 0).attr("filter", null); - lakes.select("#lava").attr("opacity", 0.7).attr("fill", "#90270d").attr("stroke", "#f93e0c").attr("stroke-width", 2).attr("filter", "url(#crumpled)"); - - coastline.append("g").attr("id", "sea_island"); - coastline.append("g").attr("id", "lake_island"); - coastline.select("#sea_island").attr("opacity", 0.5).attr("stroke", "#1f3846").attr("stroke-width", 0.7).attr("filter", "url(#dropShadow)"); - coastline.select("#lake_island").attr("opacity", 1).attr("stroke", "#7c8eaf").attr("stroke-width", 0.35).attr("filter", null); - } - - // v1.1 features stores more data - defs.select("#land").selectAll("path").remove(); - defs.select("#water").selectAll("path").remove(); - coastline.selectAll("path").remove(); - lakes.selectAll("path").remove(); - drawCoastline(); - } - - if (version < 1.11) { - // 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"); - oceanLayers.attr("layers", "-6,-3,-1"); - gridOverlay.attr("type", "pointyHex").attr("size", 10); - - // v1.11 added cultures heirarchy tree - if (pack.cultures[1] && !pack.cultures[1].code) { - pack.cultures - .filter(c => c.i) - .forEach(c => { - c.origin = 0; - c.code = c.name.slice(0, 2); - }); - } - - // v1.11 had an issue with fogging being displayed on load - unfog(); - - // v1.2 added new terrain attributes - if (!terrain.attr("set")) terrain.attr("set", "simple"); - if (!terrain.attr("size")) terrain.attr("size", 1); - if (!terrain.attr("density")) terrain.attr("density", 0.4); - } - - if (version < 1.21) { - // v1.11 replaced "display" attribute by "display" style - viewbox.selectAll("g").each(function () { - if (this.hasAttribute("display")) { - this.removeAttribute("display"); - this.style.display = "none"; - } - }); - - // v1.21 added rivers data to pack - pack.rivers = []; // rivers data - rivers.selectAll("path").each(function () { - const i = +this.id.slice(5); - const length = this.getTotalLength() / 2; - const s = this.getPointAtLength(length), - e = this.getPointAtLength(0); - const source = findCell(s.x, s.y), - mouth = findCell(e.x, e.y); - const name = Rivers.getName(mouth); - const type = length < 25 ? rw({Creek: 9, River: 3, Brook: 3, Stream: 1}) : "River"; - pack.rivers.push({i, parent: 0, length, source, mouth, basin: i, name, type}); - }); - } - - if (version < 1.22) { - // v1.22 changed state neighbors from Set object to array - BurgsAndStates.collectStatistics(); - } - - if (version < 1.3) { - // v1.3 added global options object - const winds = options.slice(); // previostly wind was saved in settings[19] - const year = rand(100, 2000); - const era = Names.getBaseShort(P(0.7) ? 1 : rand(nameBases.length)) + " Era"; - const eraShort = era[0] + "E"; - const military = Military.getDefaultOptions(); - options = {winds, year, era, eraShort, military}; - - // v1.3 added campaings data for all states - BurgsAndStates.generateCampaigns(); - - // v1.3 added militry layer - armies = viewbox.insert("g", "#icons").attr("id", "armies"); - armies.attr("opacity", 1).attr("fill-opacity", 1).attr("font-size", 6).attr("box-size", 3).attr("stroke", "#000").attr("stroke-width", 0.3); - turnButtonOn("toggleMilitary"); - Military.generate(); - } - - if (version < 1.4) { - // v1.35 added dry lakes - if (!lakes.select("#dry").size()) { - lakes.append("g").attr("id", "dry"); - lakes.select("#dry").attr("opacity", 1).attr("fill", "#c9bfa7").attr("stroke", "#8e816f").attr("stroke-width", 0.7).attr("filter", null); - } - - // v1.4 added ice layer - ice = viewbox.insert("g", "#coastline").attr("id", "ice").style("display", "none"); - ice.attr("opacity", null).attr("fill", "#e8f0f6").attr("stroke", "#e8f0f6").attr("stroke-width", 1).attr("filter", "url(#dropShadow05)"); - drawIce(); - - // v1.4 added icon and power attributes for units - for (const unit of options.military) { - if (!unit.icon) unit.icon = getUnitIcon(unit.type); - if (!unit.power) unit.power = unit.crew; - } - - function getUnitIcon(type) { - if (type === "naval") return "🌊"; - if (type === "ranged") return "🏹"; - if (type === "mounted") return "🐴"; - if (type === "machinery") return "💣"; - if (type === "armored") return "🐢"; - if (type === "aviation") return "🦅"; - if (type === "magical") return "🔮"; - else return "⚔️"; - } - - // v1.4 added state reference for regiments - pack.states.filter(s => s.military).forEach(s => s.military.forEach(r => (r.state = s.i))); - } - - if (version < 1.5) { - // not need to store default styles from v 1.5 - localStorage.removeItem("styleClean"); - localStorage.removeItem("styleGloom"); - localStorage.removeItem("styleAncient"); - localStorage.removeItem("styleMonochrome"); - - // v1.5 cultures has shield attribute - pack.cultures.forEach(culture => { - if (culture.removed) return; - culture.shield = Cultures.getRandomShield(); - }); - - // v1.5 added burg type value - pack.burgs.forEach(burg => { - if (!burg.i || burg.removed) return; - burg.type = BurgsAndStates.getType(burg.cell, burg.port); - }); - - // v1.5 added emblems - defs.append("g").attr("id", "defs-emblems"); - emblems = viewbox.insert("g", "#population").attr("id", "emblems").style("display", "none"); - emblems.append("g").attr("id", "burgEmblems"); - emblems.append("g").attr("id", "provinceEmblems"); - emblems.append("g").attr("id", "stateEmblems"); - regenerateEmblems(); - toggleEmblems(); - - // v1.5 changed releif icons data - terrain.selectAll("use").each(function () { - const type = this.getAttribute("data-type") || this.getAttribute("xlink:href"); - this.removeAttribute("xlink:href"); - this.removeAttribute("data-type"); - this.removeAttribute("data-size"); - this.setAttribute("href", type); - }); - } - - if (version < 1.6) { - // v1.6 changed rivers data - for (const river of pack.rivers) { - const el = document.getElementById("river" + river.i); - if (el) { - river.widthFactor = +el.getAttribute("data-width"); - el.removeAttribute("data-width"); - el.removeAttribute("data-increment"); - river.discharge = pack.cells.fl[river.mouth] || 1; - river.width = rn(river.length / 100, 2); - river.sourceWidth = 0.1; - } else { - Rivers.remove(river.i); - } - } - - // v1.6 changed lakes data - for (const f of pack.features) { - if (f.type !== "lake") continue; - if (f.evaporation) continue; - - f.flux = f.flux || f.cells * 3; - f.temp = grid.cells.temp[pack.cells.g[f.firstCell]]; - f.height = f.height || d3.min(pack.cells.c[f.firstCell].map(c => pack.cells.h[c]).filter(h => h >= 20)); - const height = (f.height - 18) ** heightExponentInput.value; - const evaporation = ((700 * (f.temp + 0.006 * height)) / 50 + 75) / (80 - f.temp); - f.evaporation = rn(evaporation * f.cells); - f.name = f.name || Lakes.getName(f); - delete f.river; - } - } - - if (version < 1.61) { - // v1.61 changed rulers data - ruler.style("display", null); - rulers = new Rulers(); - - ruler.selectAll(".ruler > .white").each(function () { - const x1 = +this.getAttribute("x1"); - const y1 = +this.getAttribute("y1"); - const x2 = +this.getAttribute("x2"); - const y2 = +this.getAttribute("y2"); - if (isNaN(x1) || isNaN(y1) || isNaN(x2) || isNaN(y2)) return; - const points = [ - [x1, y1], - [x2, y2] - ]; - rulers.create(Ruler, points); - }); - - ruler.selectAll("g.opisometer").each(function () { - const pointsString = this.dataset.points; - if (!pointsString) return; - const points = JSON.parse(pointsString); - rulers.create(Opisometer, points); - }); - - ruler.selectAll("path.planimeter").each(function () { - const length = this.getTotalLength(); - if (length < 30) return; - - const step = length > 1000 ? 40 : length > 400 ? 20 : 10; - const increment = length / Math.ceil(length / step); - const points = []; - for (let i = 0; i <= length; i += increment) { - const point = this.getPointAtLength(i); - points.push([point.x | 0, point.y | 0]); - } - - rulers.create(Planimeter, points); - }); - - ruler.selectAll("*").remove(); - - if (rulers.data.length) { - turnButtonOn("toggleRulers"); - rulers.draw(); - } else turnButtonOff("toggleRulers"); - - // 1.61 changed oceanicPattern from rect to image - const pattern = document.getElementById("oceanic"); - const filter = pattern.firstElementChild.getAttribute("filter"); - const href = filter ? "./images/" + filter.replace("url(#", "").replace(")", "") + ".png" : ""; - pattern.innerHTML = ``; - } - - if (version < 1.62) { - // v1.62 changed grid data - gridOverlay.attr("size", null); - } - - if (version < 1.63) { - // v1.63 changed ocean pattern opacity element - const oceanPattern = document.getElementById("oceanPattern"); - if (oceanPattern) oceanPattern.removeAttribute("opacity"); - const oceanicPattern = document.getElementById("oceanicPattern"); - if (!oceanicPattern.getAttribute("opacity")) oceanicPattern.setAttribute("opacity", 0.2); - - // v 1.63 moved label text-shadow from css to editable inline style - burgLabels.select("#cities").style("text-shadow", "white 0 0 4px"); - burgLabels.select("#towns").style("text-shadow", "white 0 0 4px"); - labels.select("#states").style("text-shadow", "white 0 0 4px"); - labels.select("#addedLabels").style("text-shadow", "white 0 0 4px"); - } - - if (version < 1.64) { - // v1.64 change states style - const opacity = regions.attr("opacity"); - const filter = regions.attr("filter"); - statesBody.attr("opacity", opacity).attr("filter", filter); - statesHalo.attr("opacity", opacity).attr("filter", "blur(5px)"); - regions.attr("opacity", null).attr("filter", null); - } - - if (version < 1.65) { - // v1.65 changed rivers data - d3.select("#rivers").attr("style", null); // remove style to unhide layer - const {cells, rivers} = pack; - const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2); - - for (const river of rivers) { - const node = document.getElementById("river" + river.i); - if (node && !river.cells) { - const riverCells = []; - const riverPoints = []; - - const length = node.getTotalLength() / 2; - if (!length) continue; - const segments = Math.ceil(length / 6); - const increment = length / segments; - - for (let i = 0; i <= segments; i++) { - const shift = increment * i; - const {x: x1, y: y1} = node.getPointAtLength(length + shift); - const {x: x2, y: y2} = node.getPointAtLength(length - shift); - const x = rn((x1 + x2) / 2, 1); - const y = rn((y1 + y2) / 2, 1); - - const cell = findCell(x, y); - riverPoints.push([x, y]); - riverCells.push(cell); - } - - river.cells = riverCells; - river.points = riverPoints; - } - - river.widthFactor = defaultWidthFactor; - - cells.i.forEach(i => { - const riverInWater = cells.r[i] && cells.h[i] < 20; - if (riverInWater) cells.r[i] = 0; - }); - } - } - - if (version < 1.652) { - // remove style to unhide layers - rivers.attr("style", null); - borders.attr("style", null); - } - - if (version < 1.7) { - // v1.7 changed markers data - const defs = document.getElementById("defs-markers"); - const markersGroup = document.getElementById("markers"); - - if (defs && markersGroup) { - const markerElements = markersGroup.querySelectorAll("use"); - const rescale = +markersGroup.getAttribute("rescale"); - - pack.markers = Array.from(markerElements).map((el, i) => { - const id = el.getAttribute("id"); - const note = notes.find(note => note.id === id); - if (note) note.id = `marker${i}`; - - let x = +el.dataset.x; - let y = +el.dataset.y; - - const transform = el.getAttribute("transform"); - if (transform) { - const [dx, dy] = parseTransform(transform); - if (dx) x += +dx; - if (dy) y += +dy; - } - const cell = findCell(x, y); - const size = rn(rescale ? el.dataset.size * 30 : el.getAttribute("width"), 1); - - const href = el.href.baseVal; - const type = href.replace("#marker_", ""); - const symbol = defs?.querySelector(`symbol${href}`); - const text = symbol?.querySelector("text"); - const circle = symbol?.querySelector("circle"); - - const icon = text?.innerHTML; - const px = text && Number(text.getAttribute("font-size")?.replace("px", "")); - const dx = text && Number(text.getAttribute("x")?.replace("%", "")); - const dy = text && Number(text.getAttribute("y")?.replace("%", "")); - const fill = circle && circle.getAttribute("fill"); - const stroke = circle && circle.getAttribute("stroke"); - - const marker = {i, icon, type, x, y, size, cell}; - if (size && size !== 30) marker.size = size; - if (!isNaN(px) && px !== 12) marker.px = px; - if (!isNaN(dx) && dx !== 50) marker.dx = dx; - if (!isNaN(dy) && dy !== 50) marker.dy = dy; - if (fill && fill !== "#ffffff") marker.fill = fill; - if (stroke && stroke !== "#000000") marker.stroke = stroke; - if (circle?.getAttribute("opacity") === "0") marker.pin = "no"; - - return marker; - }); - - markersGroup.style.display = null; - defs?.remove(); - markerElements.forEach(el => el.remove()); - if (layerIsOn("markers")) drawMarkers(); - } - } - - if (version < 1.72) { - // v1.72 renamed custom style presets - const storedStyles = Object.keys(localStorage).filter(key => key.startsWith("style")); - storedStyles.forEach(styleName => { - const style = localStorage.getItem(styleName); - const newStyleName = styleName.replace(/^style/, customPresetPrefix); - localStorage.setItem(newStyleName, style); - localStorage.removeItem(styleName); - }); - } - - if (version < 1.73) { - // v1.73 moved the hatching patterns out of the user's SVG - document.getElementById("hatching")?.remove(); - - // v1.73 added zone type to UI, ensure type is populated - const zones = Array.from(document.querySelectorAll("#zones > g")); - zones.forEach(zone => { - if (!zone.dataset.type) zone.dataset.type = "Unknown"; - }); - } - })(); + { + // dynamically import and run auto-udpdate script + const version = parseFloat(params[0]); + const {resolveVersionConflicts} = await import("./auto-update.js"); + resolveVersionConflicts(version); + } void (function checkDataIntegrity() { const cells = pack.cells; @@ -1088,6 +566,7 @@ function parseLoadedData(data) { alertMessage.innerHTML = `An error is occured on map loading. Select a different file to load,
generate a new random map or cancel the loading

${parseError(error)}

`; + $("#alert").dialog({ resizable: false, title: "Loading error", diff --git a/modules/save.js b/modules/io/save.js similarity index 100% rename from modules/save.js rename to modules/io/save.js