From af62ff915c1d85ad66e75554c7c2241dfc8d7c87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A9sz=C3=A1ros=20Anna=20Veronika?= <39062493+annapanni@users.noreply.github.com> Date: Sat, 2 Apr 2022 22:42:54 +0200 Subject: [PATCH 01/25] Dev csv (#761) * import cultures feature * remove debug messages * code cleanup --- index.html | 9 ++- modules/io/formats.js | 19 ++++++ modules/ui/cultures-editor.js | 114 +++++++++++++++++++++++++--------- 3 files changed, 110 insertions(+), 32 deletions(-) create mode 100644 modules/io/formats.js diff --git a/index.html b/index.html index a5733917..20930605 100644 --- a/index.html +++ b/index.html @@ -1426,7 +1426,7 @@

Join our Discord server and Reddit community to ask questions, get help and share maps.

- +

The project is under active development. Creator and main maintainer: Azgaar. To track the development progress see the devboard. For older versions see the changelog. Please report bugs here. You can also contact me directly via email. @@ -2610,7 +2610,7 @@ - + @@ -2852,6 +2852,7 @@ + @@ -3692,6 +3693,7 @@ + @@ -4341,7 +4343,7 @@ - + @@ -4583,6 +4585,7 @@ + diff --git a/modules/io/formats.js b/modules/io/formats.js new file mode 100644 index 00000000..329d85db --- /dev/null +++ b/modules/io/formats.js @@ -0,0 +1,19 @@ +"use strict" + +window.Formats = (function () { + async function csvParser (file, separator=",") { + const txt = await file.text(); + const rows = txt.split("\n"); + const headers = rows.shift().split(separator).map(x => x.toLowerCase()); + const data = rows.filter(a => a.trim()!=="").map(r=>r.split(separator)); + return { + headers, + data, + iterator: function* (sortf){ + const dataset = sortf? this.data.sort(sortf):this.data; + for (const d of dataset) + yield Object.fromEntries(d.map((a, i) => [this.headers[i], a])); + }}; + } + return {csvParser}; +})(); diff --git a/modules/ui/cultures-editor.js b/modules/ui/cultures-editor.js index 50b5bc63..65d5fd38 100644 --- a/modules/ui/cultures-editor.js +++ b/modules/ui/cultures-editor.js @@ -1,5 +1,6 @@ "use strict"; function editCultures() { + const cultureTypes = ["Generic", "River", "Lake", "Naval", "Nomadic", "Hunting", "Highland"]; if (customization) return; closeDialogs("#culturesEditor, .stable"); if (!layerIsOn("toggleCultures")) toggleCultures(); @@ -37,6 +38,9 @@ function editCultures() { document.getElementById("culturesEditNamesBase").addEventListener("click", editNamesbase); document.getElementById("culturesAdd").addEventListener("click", enterAddCulturesMode); document.getElementById("culturesExport").addEventListener("click", downloadCulturesData); + document.getElementById("culturesImport").addEventListener("click", () => document.getElementById("culturesCSVToLoad").click()); + + document.getElementById("culturesCSVToLoad").addEventListener("change", uploadCulturesData); function refreshCulturesEditor() { culturesCollectStatistics(); @@ -169,8 +173,7 @@ function editCultures() { function getTypeOptions(type) { let options = ""; - const types = ["Generic", "River", "Lake", "Naval", "Nomadic", "Hunting", "Highland"]; - types.forEach(t => (options += ``)); + cultureTypes.forEach(t => (options += ``)); return options; } @@ -366,7 +369,7 @@ function editCultures() { width: "24em", buttons: { Apply: function () { - applyPopulationChange(); + applyPopulationChange(rural, urban, ruralPop.value, urbanPop.value, culture); $(this).dialog("close"); }, Cancel: function () { @@ -375,32 +378,33 @@ function editCultures() { }, position: {my: "center", at: "center", of: "svg"} }); + } - function applyPopulationChange() { - const ruralChange = ruralPop.value / rural; - if (isFinite(ruralChange) && ruralChange !== 1) { - const cells = pack.cells.i.filter(i => pack.cells.culture[i] === culture); - cells.forEach(i => (pack.cells.pop[i] *= ruralChange)); - } - if (!isFinite(ruralChange) && +ruralPop.value > 0) { - const points = ruralPop.value / populationRate; - const cells = pack.cells.i.filter(i => pack.cells.culture[i] === culture); - const pop = rn(points / cells.length); - cells.forEach(i => (pack.cells.pop[i] = pop)); - } - - const urbanChange = urbanPop.value / urban; - if (isFinite(urbanChange) && urbanChange !== 1) { - burgs.forEach(b => (b.population = rn(b.population * urbanChange, 4))); - } - if (!isFinite(urbanChange) && +urbanPop.value > 0) { - const points = urbanPop.value / populationRate / urbanization; - const population = rn(points / burgs.length, 4); - burgs.forEach(b => (b.population = population)); - } - - refreshCulturesEditor(); + function applyPopulationChange(oldRural, oldUrban, newRural, newUrban, culture) { + const ruralChange = newRural / oldRural; + if (isFinite(ruralChange) && ruralChange !== 1) { + const cells = pack.cells.i.filter(i => pack.cells.culture[i] === culture); + cells.forEach(i => (pack.cells.pop[i] *= ruralChange)); } + if (!isFinite(ruralChange) && +newRural > 0) { + const points = newRural / populationRate; + const cells = pack.cells.i.filter(i => pack.cells.culture[i] === culture); + const pop = rn(points / cells.length); + cells.forEach(i => (pack.cells.pop[i] = pop)); + } + + const burgs = pack.burgs.filter(b => !b.removed && b.culture === culture); + const urbanChange = newUrban / oldUrban; + if (isFinite(urbanChange) && urbanChange !== 1) { + burgs.forEach(b => (b.population = rn(b.population * urbanChange, 4))); + } + if (!isFinite(urbanChange) && +newUrban > 0) { + const points = newUrban / populationRate / urbanization; + const population = rn(points / burgs.length, 4); + burgs.forEach(b => (b.population = population)); + } + + refreshCulturesEditor(); } function cultureRegenerateBurgs() { @@ -856,7 +860,7 @@ function editCultures() { function downloadCulturesData() { const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value; - let data = "Id,Culture,Color,Cells,Expansionism,Type,Area " + unit + ",Population,Namesbase,Emblems Shape\n"; // headers + let data = "Id,Culture,Color,Cells,Expansionism,Type,Area " + unit + ",Population,Namesbase,Emblems Shape,Origin\n"; // headers body.querySelectorAll(":scope > div").forEach(function (el) { data += el.dataset.id + ","; @@ -869,7 +873,8 @@ function editCultures() { data += el.dataset.population + ","; const base = +el.dataset.base; data += nameBases[base].name + ","; - data += el.dataset.emblems + "\n"; + data += el.dataset.emblems + ","; + data += pack.cultures[+el.dataset.id].origin + "\n"; }); const name = getFileName("Cultures") + ".csv"; @@ -881,4 +886,55 @@ function editCultures() { exitCulturesManualAssignment("close"); exitAddCultureMode(); } + async function uploadCulturesData() { + const csv = await Formats.csvParser(this.files[0]); + this.value = ""; + const cultures = pack.cultures; + const shapes = Object.keys(COA.shields.types) + .map(type => Object.keys(COA.shields[type])) + .flat(); + const populated = pack.cells.pop.map((c, i) => c? i: null).filter(c => c); + cultures.forEach((item) => {if (item.i) item.removed = true}); + for (const c of csv.iterator((a, b) => +a[0] > +b[0])) { + let current; + if (+c.id < cultures.length) { + current = cultures[c.id]; + const ratio = current.urban / (current.rural + current.urban); + applyPopulationChange(current.rural, current.urban, c.population*(1 - ratio), c.population*ratio, +c.id); + } else { + current = { + i: cultures.length, + center: ra(populated), + area: 0, + cells: 0, + origin: 0, + rural: 0, + urban: 0, + } + cultures.push(current) + } + current.name = c.culture; + current.code = abbreviate(current.name, cultures.map(c => c.code)); + current.color = c.color; + current.expansionism = +c.expansionism; + current.origin = +c.origin; + if (cultureTypes.includes(c.type)) + current.type = c.type; + else + current.type = "Generic"; + current.removed = false; + const shieldShape = c["emblems shape"].toLowerCase(); + if (shapes.includes(shieldShape)) + current.shield = shieldShape + else + current.shield = "heater"; + + const nbi = nameBases.findIndex(n => n.name==c.namesbase); + current.base = nbi==-1? 0: nbi; + } + const validId = cultures.filter(c => !c.removed).map(c => c.i); + cultures.forEach(item => item.origin = validId.includes(item.origin)? item.origin:0); + cultures[0].origin = null; + refreshCulturesEditor(); + } } From fcda552719d6ca9b5c470757294d03ecd489f2fe Mon Sep 17 00:00:00 2001 From: Azgaar Date: Tue, 5 Apr 2022 00:14:01 +0500 Subject: [PATCH 02/25] addPortal fix --- modules/markers-generator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/markers-generator.js b/modules/markers-generator.js index 6c3e0398..e2bb55d2 100644 --- a/modules/markers-generator.js +++ b/modules/markers-generator.js @@ -788,7 +788,7 @@ window.Markers = (function () { const {cells, burgs} = pack; // Portals can only be added to burgs - if (cells.burg[cell]) return; + if (!cells.burg[cell]) return; const burgName = burgs[cells.burg[cell]].name; const name = `${burgName} Portal`; From d18bb87ddfdef33680ca75576ac19c57811a9d08 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Tue, 5 Apr 2022 00:18:04 +0500 Subject: [PATCH 03/25] update supporters --- modules/ui/options.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/ui/options.js b/modules/ui/options.js index ad90d7fe..f9c9473c 100644 --- a/modules/ui/options.js +++ b/modules/ui/options.js @@ -105,7 +105,8 @@ function showSupporters() { Jonathan Williams,ojacid .,Brian Wilson,A Patreon of the Ahts,Shubham Jakhotiya,www15o,Jan Bundesmann,Angelique Badger,Joshua Xiong,Moist mongol, Frank Fewkes,jason baldrick,Game Master Pro,Andrew Kircher,Preston Mitchell,Chris Kohut,Emarandzeb,Trentin Bergeron,Damon Gallaty,Pleaseworkforonce, Jordan,William Markus,Sidr Dim,Alexander Whittaker,The Next Level,Patrick Valverde,Markus Peham,Daniel Cooper,the Beagles of Neorbus,Marley Moule, - Maximilian Schielke,Johnathan Xavier Hutchinson,Ele,Rita,Randy Ross,John Wick,RedSpaz,cameron cannon,Ian Grau-Fay,Kyle Barrett,Charlotte Wiland`; + Maximilian Schielke,Johnathan Xavier Hutchinson,Ele,Rita,Randy Ross,John Wick,RedSpaz,cameron cannon,Ian Grau-Fay,Kyle Barrett,Charlotte Wiland, + David Kaul,E. Jason Davis,Cyberate,Atenfox,Sea Wolf,Holly Loveless`; const array = supporters .replace(/(?:\r\n|\r|\n)/g, "") From 1bec723d4f8dbfbbc22b474f0212ea5a5b2c033c Mon Sep 17 00:00:00 2001 From: Azgaar Date: Sat, 9 Apr 2022 20:18:25 +0500 Subject: [PATCH 04/25] style uploadCulturesData, fix removed issue --- modules/io/formats.js | 23 ++++++++------ modules/ui/cultures-editor.js | 56 +++++++++++++++++------------------ 2 files changed, 42 insertions(+), 37 deletions(-) diff --git a/modules/io/formats.js b/modules/io/formats.js index 329d85db..439e124e 100644 --- a/modules/io/formats.js +++ b/modules/io/formats.js @@ -1,19 +1,24 @@ -"use strict" +"use strict"; window.Formats = (function () { - async function csvParser (file, separator=",") { + async function csvParser(file, separator = ",") { const txt = await file.text(); const rows = txt.split("\n"); - const headers = rows.shift().split(separator).map(x => x.toLowerCase()); - const data = rows.filter(a => a.trim()!=="").map(r=>r.split(separator)); + const headers = rows + .shift() + .split(separator) + .map(x => x.toLowerCase()); + const data = rows.filter(a => a.trim() !== "").map(r => r.split(separator)); + return { headers, data, - iterator: function* (sortf){ - const dataset = sortf? this.data.sort(sortf):this.data; - for (const d of dataset) - yield Object.fromEntries(d.map((a, i) => [this.headers[i], a])); - }}; + iterator: function* (sortf) { + const dataset = sortf ? this.data.sort(sortf) : this.data; + for (const d of dataset) yield Object.fromEntries(d.map((a, i) => [this.headers[i], a])); + } + }; } + return {csvParser}; })(); diff --git a/modules/ui/cultures-editor.js b/modules/ui/cultures-editor.js index 65d5fd38..484346cc 100644 --- a/modules/ui/cultures-editor.js +++ b/modules/ui/cultures-editor.js @@ -39,7 +39,6 @@ function editCultures() { document.getElementById("culturesAdd").addEventListener("click", enterAddCulturesMode); document.getElementById("culturesExport").addEventListener("click", downloadCulturesData); document.getElementById("culturesImport").addEventListener("click", () => document.getElementById("culturesCSVToLoad").click()); - document.getElementById("culturesCSVToLoad").addEventListener("change", uploadCulturesData); function refreshCulturesEditor() { @@ -886,55 +885,56 @@ function editCultures() { exitCulturesManualAssignment("close"); exitAddCultureMode(); } + async function uploadCulturesData() { const csv = await Formats.csvParser(this.files[0]); this.value = ""; + const cultures = pack.cultures; const shapes = Object.keys(COA.shields.types) .map(type => Object.keys(COA.shields[type])) .flat(); - const populated = pack.cells.pop.map((c, i) => c? i: null).filter(c => c); - cultures.forEach((item) => {if (item.i) item.removed = true}); + + const populated = pack.cells.pop.map((c, i) => (c ? i : null)).filter(c => c); + for (const c of csv.iterator((a, b) => +a[0] > +b[0])) { let current; if (+c.id < cultures.length) { current = cultures[c.id]; + current.removed = false; const ratio = current.urban / (current.rural + current.urban); - applyPopulationChange(current.rural, current.urban, c.population*(1 - ratio), c.population*ratio, +c.id); + applyPopulationChange(current.rural, current.urban, c.population * (1 - ratio), c.population * ratio, +c.id); } else { - current = { - i: cultures.length, - center: ra(populated), - area: 0, - cells: 0, - origin: 0, - rural: 0, - urban: 0, - } - cultures.push(current) + current = {i: cultures.length, center: ra(populated), area: 0, cells: 0, origin: 0, rural: 0, urban: 0}; + cultures.push(current); } + current.name = c.culture; - current.code = abbreviate(current.name, cultures.map(c => c.code)); + current.code = abbreviate( + current.name, + cultures.map(c => c.code) + ); + current.color = c.color; current.expansionism = +c.expansionism; current.origin = +c.origin; - if (cultureTypes.includes(c.type)) - current.type = c.type; - else - current.type = "Generic"; - current.removed = false; - const shieldShape = c["emblems shape"].toLowerCase(); - if (shapes.includes(shieldShape)) - current.shield = shieldShape - else - current.shield = "heater"; - const nbi = nameBases.findIndex(n => n.name==c.namesbase); - current.base = nbi==-1? 0: nbi; + if (cultureTypes.includes(c.type)) current.type = c.type; + else current.type = "Generic"; + + const shieldShape = c["emblems shape"].toLowerCase(); + if (shapes.includes(shieldShape)) current.shield = shieldShape; + else current.shield = "heater"; + + const nameBaseIndex = nameBases.findIndex(n => n.name == c.namesbase); + current.base = nameBaseIndex === -1 ? 0 : nameBaseIndex; } + const validId = cultures.filter(c => !c.removed).map(c => c.i); - cultures.forEach(item => item.origin = validId.includes(item.origin)? item.origin:0); + cultures.forEach(item => (item.origin = validId.includes(item.origin) ? item.origin : 0)); cultures[0].origin = null; + + drawCultures(); refreshCulturesEditor(); } } From f2fcef02849ea132214285c7b6f45a4cce5607cd Mon Sep 17 00:00:00 2001 From: Azgaar Date: Sat, 9 Apr 2022 21:52:37 +0500 Subject: [PATCH 05/25] routes - don't break if feature is not found --- modules/routes-generator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/routes-generator.js b/modules/routes-generator.js index e7a909cb..e4ec3374 100644 --- a/modules/routes-generator.js +++ b/modules/routes-generator.js @@ -74,7 +74,7 @@ window.Routes = (function () { const ports = allPorts.filter(b => b.port === f); // all ports on the same feature if (!ports.length) return; - if (features[f].border) addOverseaRoute(f, ports[0]); + if (features[f]?.border) addOverseaRoute(f, ports[0]); // get inner-map routes for (let s = 0; s < ports.length; s++) { From 3cbd451df947a0cc8aedc9bd3c3157049d3f672b Mon Sep 17 00:00:00 2001 From: Azgaar Date: Sun, 10 Apr 2022 14:45:38 +0500 Subject: [PATCH 06/25] fix for Data Integrity Check log --- modules/io/load.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/io/load.js b/modules/io/load.js index 9ff94d33..14a59666 100644 --- a/modules/io/load.js +++ b/modules/io/load.js @@ -459,7 +459,7 @@ async function parseLoadedData(data) { invalidReligions.forEach(r => { const invalidCells = cells.i.filter(i => cells.religion[i] === r); invalidCells.forEach(i => (cells.religion[i] = 0)); - ERROR && console.error("Data Integrity Check. Invalid religion", c, "is assigned to cells", invalidCells); + ERROR && console.error("Data Integrity Check. Invalid religion", r, "is assigned to cells", invalidCells); }); const invalidFeatures = [...new Set(cells.f)].filter(f => f && !pack.features[f]); From 5703e621772624127c8349d8a5baafa423f4e960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gergely=20M=C3=A9sz=C3=A1ros=2C=20Ph=2ED?= Date: Fri, 15 Apr 2022 11:45:02 +0200 Subject: [PATCH 07/25] Dev submaps (#770) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * bioms shouldn't be masked or the style selection box is useless * fix: misleading comment * experimental submapping feature * burg remapping * Submap with options * Fix: calculating absolute flux from precipitation normal-value. * effective distanceScale * updated resampler * fix: missing cell * Fix: River automatic rerender on regeneration. * FIX: wrong culture migration * fixed 0 index burg bug, more accurate coast detection for burgs * FIX: wrong burg cell id * fix invalid feature number at burg.ports, option to disable regenerations * Relocate submap * update height model and scale parameters * new menu * Dropbox OAuth implementation and Cloud framework * add some space * removing uneccesary logs, defer script load * map position on planet, fix wrong riverbed generation * fix:riverbed generation * better cell sampler * Auto-Smoothing,dist fix * FIX: incorrect province copy and minor fix of rebels * Cleanup * FIX: water detection bug * Recompute centers (states, cultures, provinces) * activating forwardmap * FIX: port burg relocation algo * FIX: coast detection (for burgs) * Fix: invalid html id * add dot * update for FMG 1.73 * Update submap gui * refactored submap ui options * Copy all visible military units from the old map. * add info text * Add Markers.deleteMarker API. * Lock markers and lock burgs options * better comment * submapper gui updates, remove feature mapping on/off * Fix typo (thx evolvedexperiment) * fix ugly GUI (2 digit roundoff) * resample dialog * Town Promotion to largetown * don't promote to capitals. * Fix typo * round style settings * do not draw removed burgs * Fix port cell search algo * Fix: robust error handling, no error for 0. * submap: projection moved to options, fix double burg error * complete rewrite of burg relocation * findcell by coordinates * prepare to merge, add comments, remove fluff * replacing lodash with deepCopy implementation Co-authored-by: Mészáros Gergely --- index.html | 62 ++++- main.js | 12 +- modules/burgs-and-states.js | 4 +- modules/io/load.js | 2 +- modules/markers-generator.js | 8 +- modules/military-generator.js | 23 +- modules/river-generator.js | 2 + modules/submap.js | 411 ++++++++++++++++++++++++++++++++++ modules/ui/burg-editor.js | 9 +- modules/ui/editors.js | 21 +- modules/ui/markers-editor.js | 3 +- modules/ui/options.js | 36 +-- modules/ui/style.js | 10 +- modules/ui/submap.js | 156 +++++++++++++ modules/ui/units-editor.js | 1 + utils/arrayUtils.js | 34 +++ 16 files changed, 741 insertions(+), 53 deletions(-) create mode 100644 modules/submap.js create mode 100644 modules/ui/submap.js diff --git a/index.html b/index.html index 20930605..d7ce4e0a 100644 --- a/index.html +++ b/index.html @@ -1366,6 +1366,12 @@ + +

Click to create a new map:

+
+ + +
@@ -2234,7 +2240,7 @@
- Defenders + Defenders
@@ -3659,6 +3665,58 @@
+ + + @@ -4523,6 +4581,7 @@ + @@ -4574,6 +4633,7 @@ + diff --git a/main.js b/main.js index 6f4fd5fc..f9146b6f 100644 --- a/main.js +++ b/main.js @@ -155,6 +155,7 @@ let options = { }; let mapCoordinates = {}; // map coordinates on globe let populationRate = +document.getElementById("populationRateInput").value; +let distanceScale = +document.getElementById("distanceScaleInput").value; let urbanization = +document.getElementById("urbanizationInput").value; let urbanDensity = +document.getElementById("urbanDensityInput").value; @@ -826,6 +827,7 @@ function markupGridOcean() { TIME && console.timeEnd("markupGridOcean"); } +// Calculate cell-distance to coast for every cell function markup(cells, start, increment, limit) { for (let t = start, count = Infinity; count > 0 && t > limit; t += increment) { count = 0; @@ -1617,14 +1619,16 @@ function addZones(number = 1) { } function addRebels() { - const state = ra(states.filter(s => s.i && s.neighbors.some(n => n))); + 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)); - const cell = cells.i.find(i => cells.state[i] === state.i && cells.c[i].some(c => cells.state[c] === neib)); + 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 = [], - queue = [cell], + queue = [], power = rand(10, 30); + if (cell) queue.push.cell; while (queue.length) { const q = queue.shift(); diff --git a/modules/burgs-and-states.js b/modules/burgs-and-states.js index a9200868..e64e3e77 100644 --- a/modules/burgs-and-states.js +++ b/modules/burgs-and-states.js @@ -256,7 +256,7 @@ window.BurgsAndStates = (function () { icons.selectAll("use").remove(); // capitals - const capitals = pack.burgs.filter(b => b.capital); + const capitals = pack.burgs.filter(b => b.capital && !b.removed); const capitalIcons = burgIcons.select("#cities"); const capitalLabels = burgLabels.select("#cities"); const capitalSize = capitalIcons.attr("size") || 1; @@ -299,7 +299,7 @@ window.BurgsAndStates = (function () { .attr("height", caSize); // towns - const towns = pack.burgs.filter(b => b.i && !b.capital); + const towns = pack.burgs.filter(b => b.i && !b.capital && !b.removed); const townIcons = burgIcons.select("#towns"); const townLabels = burgLabels.select("#towns"); const townSize = townIcons.attr("size") || 0.5; diff --git a/modules/io/load.js b/modules/io/load.js index 14a59666..75687c05 100644 --- a/modules/io/load.js +++ b/modules/io/load.js @@ -205,7 +205,7 @@ async function parseLoadedData(data) { void (function parseSettings() { const settings = data[1].split("|"); if (settings[0]) applyOption(distanceUnitInput, settings[0]); - if (settings[1]) distanceScaleInput.value = distanceScaleOutput.value = settings[1]; + if (settings[1]) distanceScale = distanceScaleInput.value = distanceScaleOutput.value = settings[1]; if (settings[2]) areaUnit.value = settings[2]; if (settings[3]) applyOption(heightUnit, settings[3]); if (settings[4]) heightExponentInput.value = heightExponentOutput.value = settings[4]; diff --git a/modules/markers-generator.js b/modules/markers-generator.js index e2bb55d2..156e348c 100644 --- a/modules/markers-generator.js +++ b/modules/markers-generator.js @@ -129,6 +129,12 @@ window.Markers = (function () { return marker; } + function deleteMarker(markerId) { + const noteId = 'marker' + markerId; + notes = notes.filter(note => note.id !== noteId); + pack.markers = pack.markers.filter(m => m.i !== markerId); + } + function listVolcanoes({cells}) { return cells.i.filter(i => !occupied[i] && cells.h[i] >= 70); } @@ -796,5 +802,5 @@ window.Markers = (function () { notes.push({id, name, legend}); } - return {add, generate, regenerate, getConfig, setConfig}; + return {add, generate, regenerate, getConfig, setConfig, deleteMarker}; })(); diff --git a/modules/military-generator.js b/modules/military-generator.js index c69dc1e3..648f5637 100644 --- a/modules/military-generator.js +++ b/modules/military-generator.js @@ -157,14 +157,6 @@ window.Military = (function () { } } - void (function removeExistingRegiments() { - armies.selectAll("g > g").each(function () { - const index = notes.findIndex(n => n.id === this.id); - if (index != -1) notes.splice(index, 1); - }); - armies.selectAll("g").remove(); - })(); - const expected = 3 * populationRate; // expected regiment size const mergeable = (n0, n1) => (!n0.s && !n1.s) || n0.u === n1.u; // check if regiments can be merged @@ -172,9 +164,10 @@ window.Military = (function () { valid.forEach(s => { s.military = createRegiments(s.temp.platoons, s); delete s.temp; // do not store temp data - drawRegiments(s.military, s.i); }); + redraw(); + function createRegiments(nodes, s) { if (!nodes.length) return []; @@ -236,6 +229,16 @@ window.Military = (function () { TIME && console.timeEnd("generateMilitaryForces"); }; + function redraw() { + const validStates = pack.states.filter(s => s.i && !s.removed); + armies.selectAll("g > g").each(function () { + const index = notes.findIndex(n => n.id === this.id); + if (index != -1) notes.splice(index, 1); + }); + armies.selectAll("g").remove(); + validStates.forEach(s => drawRegiments(s.military, s.i)); + } + const getDefaultOptions = function () { return [ {icon: "⚔️", name: "infantry", rural: 0.25, urban: 0.2, crew: 1, power: 1, type: "melee", separate: 0}, @@ -406,5 +409,5 @@ window.Military = (function () { notes.push({id: `regiment${s.i}-${r.i}`, name: `${r.icon} ${r.name}`, legend}); }; - return {generate, getDefaultOptions, getName, generateNote, drawRegiments, drawRegiment, moveRegiment, getTotal, getEmblem}; + return {generate, redraw, getDefaultOptions, getName, generateNote, drawRegiments, drawRegiment, moveRegiment, getTotal, getEmblem}; })(); diff --git a/modules/river-generator.js b/modules/river-generator.js index 0943ea9c..957fe6fc 100644 --- a/modules/river-generator.js +++ b/modules/river-generator.js @@ -35,10 +35,12 @@ window.Rivers = (function () { TIME && console.timeEnd("generateRivers"); function drainWater() { + //const MIN_FLUX_TO_FORM_RIVER = 10 * distanceScale; const MIN_FLUX_TO_FORM_RIVER = 30; const cellsNumberModifier = (pointsInput.dataset.cells / 10000) ** 0.25; const prec = grid.cells.prec; + const area = pack.cells.area; const land = cells.i.filter(i => h[i] >= 20).sort((a, b) => h[b] - h[a]); const lakeOutCells = Lakes.setClimateData(h); diff --git a/modules/submap.js b/modules/submap.js new file mode 100644 index 00000000..d46be5e9 --- /dev/null +++ b/modules/submap.js @@ -0,0 +1,411 @@ +"use strict"; +/* +Cell resampler module used by submapper and resampler (transform) +main function: resample(options); +*/ + +window.Submap = (function () { + const isWater = (map, id) => map.grid.cells.h[map.pack.cells.g[id]] < 20? true: false; + const inMap = (x,y) => x>0 && x0 && y[Number, Number] + function to calculate new coordinates + inverse: g(Number,Number)->[Number, Number] + inverse of f + depressRivers: Bool carve out riverbeds? + smoothHeightMap: Bool run smooth filter on heights + addLakesInDepressions: call FMG original funtion on heightmap + + lockMarkers: Bool Auto lock all copied markers + lockBurgs: Bool Auto lock all copied burgs + } + */ + + const projection = options.projection; + const inverse = options.inverse; + const stage = s => INFO && console.log('SUBMAP:', s); + const timeStart = performance.now(); + const childMap = { grid, pack } + invokeActiveZooming(); + + // copy seed + seed = parentMap.seed; + Math.random = aleaPRNG(seed); + INFO && console.group("SubMap with seed: " + seed); + DEBUG && console.log("Using Options:", options); + + // create new grid + applyMapSize(); + placePoints(); + calculateVoronoi(grid, grid.points); + drawScaleBar(scale); + + const resampler = (points, qtree, f) => { + for(const [i,[x, y]] of points.entries()) { + const [tx, ty] = inverse(x, y); + const oldid = qtree.find(tx,ty,Infinity)[2]; + f(i, oldid); + } + } + + stage("Resampling heightmap, temperature and precipitation.") + // resample heightmap from old WorldState + const n = grid.points.length; + grid.cells.h = new Uint8Array(n); // heightmap + grid.cells.temp = new Int8Array(n); // temperature + grid.cells.prec = new Int8Array(n); // precipitation + const reverseGridMap = new Uint32Array(n); // cellmap from new -> oldcell + + const oldGrid = parentMap.grid; + // build cache old -> [newcelllist] + const forwardGridMap = parentMap.grid.points.map(_=>[]); + resampler(grid.points, parentMap.pack.cells.q, (id, oldid) => { + const cid = parentMap.pack.cells.g[oldid]; + grid.cells.h[id] = oldGrid.cells.h[cid]; + grid.cells.temp[id] = oldGrid.cells.temp[cid]; + grid.cells.prec[id] = oldGrid.cells.prec[cid]; + if (options.depressRivers) forwardGridMap[cid].push(id); + reverseGridMap[id] = cid; + }) + // TODO: add smooth/noise function for h, temp, prec n times + + // smooth heightmap + // smoothing should never change cell type (land->water or water->land) + + if (options.smoothHeightMap) { + const gcells = grid.cells; + gcells.h.forEach((h,i) => { + const hs = gcells.c[i].map(c=>gcells.h[c]) + hs.push(h) + gcells.h[i] = h>=20 + ? Math.max(d3.mean(hs),20) + : Math.min(d3.mean(hs),19); + }); + } + + if (options.depressRivers) { + stage("Generating riverbeds.") + const rbeds = new Uint16Array(grid.cells.i.length); + + // and erode riverbeds + parentMap.pack.rivers.forEach(r => + r.cells.forEach(oldpc => { + if (oldpc < 0) return; // ignore out-of-map marker (-1) + const oldc = parentMap.pack.cells.g[oldpc]; + const targetCells = forwardGridMap[oldc]; + if (!targetCells) + throw "TargetCell shouldn't be empty."; + targetCells.forEach(c => { + if (grid.cells.h[c]<20) return; + rbeds[c] = 1; + }); + }) + ); + // raise every land cell a bit except riverbeds + grid.cells.h.forEach((h, i) => { + if (rbeds[i] || h<20) return; + grid.cells.h[i] = Math.min(h+2, 100); + }); + } + + stage("Detect features, ocean and generating lakes.") + markFeatures(); + markupGridOcean(); + + // Warning: addLakesInDeepDepressions can be very slow! + if (options.addLakesInDepressions) { + addLakesInDeepDepressions(); + openNearSeaLakes(); + } + + OceanLayers(); + + calculateMapCoordinates(); + // calculateTemperatures(); + // generatePrecipitation(); + stage("Cell cleanup.") + reGraph(); + + // remove misclassified cells + stage("Define coastline.") + drawCoastline(); + + /****************************************************/ + /* Packed Graph */ + /****************************************************/ + const oldCells = parentMap.pack.cells; + // const reverseMap = new Map(); // cellmap from new -> oldcell + const forwardMap = parentMap.pack.cells.p.map(_=>[]); // old -> [newcelllist] + + const pn = pack.cells.i.length; + const cells = pack.cells; + cells.culture = new Uint16Array(pn); + cells.state = new Uint16Array(pn); + cells.burg = new Uint16Array(pn); + cells.religion = new Uint16Array(pn); + cells.road = new Uint16Array(pn); + cells.crossroad = new Uint16Array(pn); + cells.province = new Uint16Array(pn); + + stage("Resampling culture, state and religion map.") + for(const [id, gridCellId] of cells.g.entries()) { + const oldGridId = reverseGridMap[gridCellId]; + if (oldGridId === undefined) { + console.error('Can not find old cell id', reverseGridMap, 'in', gridCellId); + continue; + } + // find old parent's children + const oldChildren = oldCells.i.filter(oid=>oldCells.g[oid]==oldGridId); + let oldid; // matching cell on the original map + + if (!oldChildren.length) { + // it *must* be a (deleted) deep ocean cell + if (!oldGrid.cells.h[oldGridId] < 20) { + console.error(`Warning, ${gridCellId} should be water cell, not ${oldGrid.cells.h[oldGridId]}`); + continue; + } + // find replacement: closest water cell + const [ox, oy] = cells.p[id]; + const [tx, ty] = inverse(x, y); + oldid = oldCells.q.find(tx,ty,Infinity)[2]; + if (!oldid) { + console.warn("Warning, no id found in quad", id, "parent", gridCellId); + continue; + } + } else { + // find closest children (packcell) on the parent map + const distance = x => (x[0]-cells.p[id][0])**2 + (x[1]-cells.p[id][1])**2; + let d = Infinity; + oldChildren.forEach(oid => { + // this should be always true, unless some algo modded the height! + if (isWater(parentMap, oid) !== isWater(childMap, id)) { + console.warn(`cell sank because of addLakesInDepressions: ${oid}`); + return; + } + const [oldpx, oldpy] = oldCells.p[oid]; + const nd = distance(projection(oldpx, oldpy)); + if (isNaN(nd)) { + console.error("Distance is not a number!", "Old point:", oldpx, oldpy); + } + if (nd < d) [d, oldid] = [nd, oid]; + }) + if (oldid === undefined) { + console.warn("Warning, no match for", id, "(parent:", gridCellId, ")"); + continue; + } + } + + if (isWater(childMap, id) !== isWater(parentMap, oldid)) { + WARN && console.warn('Type discrepancy detected:', id, oldid, `${pack.cells.t[id]} != ${oldCells.t[oldid]}`); + } + + cells.culture[id] = oldCells.culture[oldid]; + cells.state[id] = oldCells.state[oldid]; + cells.religion[id] = oldCells.religion[oldid]; + cells.province[id] = oldCells.province[oldid]; + // reverseMap.set(id, oldid) + forwardMap[oldid].push(id) + } + + stage("Regenerating river network.") + Rivers.generate(); + drawRivers(); + Lakes.defineGroup(); + + // biome calculation based on (resampled) grid.cells.temp and prec + // it's safe to recalculate. + stage("Regenerating Biome."); + defineBiomes(); + // recalculate suitability and population + // TODO: normalize according to the base-map + rankCells(); + + stage("Porting Cultures"); + pack.cultures = parentMap.pack.cultures; + // fix culture centers + const validCultures = new Set(pack.cells.culture); + pack.cultures.forEach((c, i) => { + if (!i) return // ignore wildlands + if (!validCultures.has(i)) { + c.removed = true; + c.center = null; + return + } + const newCenters = forwardMap[c.center] + c.center = newCenters.length + ? newCenters[0] + : pack.cells.culture.findIndex(x => x===i); + }); + + stage("Porting and locking burgs."); + copyBurgs(parentMap, projection, options); + + // transfer states, mark states without land as removed. + stage("Porting states."); + const validStates = new Set(pack.cells.state); + pack.states = parentMap.pack.states; + // keep valid states and neighbors only + pack.states.forEach((s, i) => { + if (!s.i || s.removed) return; // ignore removed and neutrals + if (!validStates.has(i)) s.removed = true; + s.neighbors = s.neighbors.filter(n => validStates.has(n)); + + // find center + s.center = pack.burgs[s.capital].cell + ? pack.burgs[s.capital].cell // capital is the best bet + : pack.cells.state.findIndex(x => x===i); // otherwise use the first valid cell + }); + + // transfer provinces, mark provinces without land as removed. + stage("Porting provinces."); + const validProvinces = new Set(pack.cells.province); + pack.provinces = parentMap.pack.provinces; + // mark uneccesary provinces + pack.provinces.forEach((p, i) => { + if (!p || p.removed) return; + if (!validProvinces.has(i)) { + p.removed = true; + return + } + const newCenters = forwardMap[p.center] + p.center = newCenters.length + ? newCenters[0] + : pack.cells.province.findIndex(x => x===i); + }); + + BurgsAndStates.drawBurgs(); + + stage("Regenerating road network."); + Routes.regenerate(); + + drawStates(); + drawBorders(); + BurgsAndStates.drawStateLabels(); + + Rivers.specify(); + Lakes.generateName(); + + stage("Porting military."); + for (const s of pack.states) { + if (!s.military) continue; + for (const m of s.military) { + [m.x, m.y] = projection(m.x, m.y); + [m.bx, m.by] = projection(m.bx, m.by); + const cc = forwardMap[m.cell]; + m.cell = (cc && cc.length)? cc[0]: null; + } + s.military = s.military.filter(m=>m.cell).map((m, i) => ({...m, i})); + } + Military.redraw(); + + stage("Copying markers."); + for (const m of pack.markers) { + const [x, y] = projection(m.x, m.y); + if (!inMap(x, y)) { + Markers.deleteMarker(m.i); + } else { + m.x = x; + m.y = y; + m.cell = findCell(x, y); + if (options.lockMarkers) m.lock = true; + } + } + drawMarkers(); + + stage("Regenerating Zones."); + addZones(); + Names.getMapName(); + stage("Submap done."); + + WARN && console.warn(`TOTAL: ${rn((performance.now() - timeStart) / 1000, 2)}s`); + showStatistics(); + INFO && console.groupEnd("Generated Map " + seed); + } + + /* find the nearest cell accepted by filter f *and* having at + * least one *neighbor* fulfilling filter g, up to cell-distance `max` + * returns [cellid, neighbor] tuple or undefined if no such cell. + * accepts coordinates (x, y) + */ + const findNearest = (f, g, max=3) => (px,py) => { + const d2 = c => (px-pack.cells.p[c][0])**2 + (py-pack.cells.p[c][0])**2 + const startCell = findCell(px, py); + const tested = new Set([startCell]); // ignore analyzed cells + const kernel = (cs, level) => { + const [bestf, bestg] = cs.filter(f).reduce(([cf, cg], c) => { + const neighbors = pack.cells.c[c]; + const betterg = neighbors.filter(g).reduce((u, x) => d2(x) pack.cells.c[c]).flat()) + const ring = Array.from(targets).filter(nc => !tested.has(nc)); + if (level >= max || !ring.length) + return [undefined, undefined]; + ring.forEach(c => tested.add(c)); + return kernel(ring, level+1); + } + const pair = kernel([startCell], 1); + return pair; + } + + function copyBurgs(parentMap, projection, options) { + const cells = pack.cells; + const childMap = { grid, pack } + pack.burgs = parentMap.pack.burgs; + + // remap burgs to the best new cell + pack.burgs.forEach( (b, id) => { + if (id == 0) return; // skip empty city of neturals + [b.x, b.y] = projection(b.x, b.y); + + // disable out-of-map (removed) burgs + if (!inMap(b.x,b.y)) { + b.removed = true; + b.cell = null; + return; + } + + const cityCell = findCell(b.x, b.y); + let searchFunc; + const isFreeLand = c => cells.t[c] === 1 && !cells.burg[c]; + const nearCoast = c => cells.t[c] === -1; + + // check if we need to relocate the burg + if (cells.burg[cityCell]) // already occupied + searchFunc = findNearest(isFreeLand, _ => true, 3); + + if (isWater(childMap, cityCell) || b.port) // burg is in water or port + searchFunc = findNearest(isFreeLand, nearCoast, 6); + + if (searchFunc) { + const [newCell, neighbor] = searchFunc(b.x, b.y); + if (!newCell) { + WARN && console.warn(`Can not relocate Burg: ${b.name} sunk and destroyed. :-(`); + b.cell = null; + b.removed = true; + return; + } + DEBUG && console.log(`Moving ${b.name} from ${cityCell} to ${newCell} near ${neighbor}.`); + [b.x, b.y] = b.port? getMiddlePoint(newCell, neighbor): cells.p[newCell]; + if (b.port) b.port = cells.f[neighbor]; // copy feature number + b.cell = newCell; + if (b.port && !isWater(childMap, neighbor)) console.error('betrayal! negihbor must be water!', b); + } else { + b.cell = cityCell; + } + if (!b.lock) b.lock = options.lockBurgs; + cells.burg[b.cell] = id; + }); + } + + // export + return { resample, findNearest } +})(); diff --git a/modules/ui/burg-editor.js b/modules/ui/burg-editor.js index 8ec8b61a..0e7aacf0 100644 --- a/modules/ui/burg-editor.js +++ b/modules/ui/burg-editor.js @@ -266,14 +266,7 @@ function editBurg(id) { toggleNewGroupInput(); document.getElementById("burgInputGroup").value = ""; - const newLabelG = document.querySelector("#burgLabels").appendChild(labelG.cloneNode(false)); - newLabelG.id = group; - const newIconG = document.querySelector("#burgIcons").appendChild(iconG.cloneNode(false)); - newIconG.id = group; - if (anchor) { - const newAnchorG = document.querySelector("#anchors").appendChild(anchorG.cloneNode(false)); - newAnchorG.id = group; - } + addBurgsGroup(group); moveBurgToGroup(id, group); } diff --git a/modules/ui/editors.js b/modules/ui/editors.js index e13f8eff..819a2637 100644 --- a/modules/ui/editors.js +++ b/modules/ui/editors.js @@ -169,7 +169,7 @@ function moveBurgToGroup(id, g) { const icon = document.querySelector("#burgIcons [data-id='" + id + "']"); const anchor = document.querySelector("#anchors [data-id='" + id + "']"); if (!label || !icon) { - ERROR && console.error("Cannot find label or icon elements"); + ERROR && console.error(`Cannot find label or icon elements for id ${id}`); return; } @@ -190,6 +190,25 @@ function moveBurgToGroup(id, g) { } } +function moveAllBurgsToGroup(fromGroup, toGroup) { + const groupToMove = document.querySelector(`#burgIcons #${fromGroup}`); + const burgsToMove = Array.from(groupToMove.children).map(x=>x.dataset.id); + addBurgsGroup(toGroup) + burgsToMove.forEach(x=>moveBurgToGroup(x, toGroup)); +} + +function addBurgsGroup(group) { + if (document.querySelector(`#burgLabels > #${group}`)) return; + const labelCopy = document.querySelector("#burgLabels > #towns").cloneNode(false); + const iconCopy = document.querySelector("#burgIcons > #towns").cloneNode(false); + const anchorCopy = document.querySelector("#anchors > #towns").cloneNode(false); + + // FIXME: using the same id is against the spec! + document.querySelector("#burgLabels").appendChild(labelCopy).id = group; + document.querySelector("#burgIcons").appendChild(iconCopy).id = group; + document.querySelector("#anchors").appendChild(anchorCopy).id = group; +} + function removeBurg(id) { const label = document.querySelector("#burgLabels [data-id='" + id + "']"); const icon = document.querySelector("#burgIcons [data-id='" + id + "']"); diff --git a/modules/ui/markers-editor.js b/modules/ui/markers-editor.js index d1f1cba4..26f035fa 100644 --- a/modules/ui/markers-editor.js +++ b/modules/ui/markers-editor.js @@ -241,8 +241,7 @@ function editMarker(markerI) { } function deleteMarker() { - notes = notes.filter(note => note.id !== element.id); - pack.markers = pack.markers.filter(m => m.i !== marker.i); + Markers.deleteMarker(marker.i) element.remove(); $("#markerEditor").dialog("close"); if (document.getElementById("markersOverviewRefresh").offsetParent) markersOverviewRefresh.click(); diff --git a/modules/ui/options.js b/modules/ui/options.js index f9c9473c..89642f5d 100644 --- a/modules/ui/options.js +++ b/modules/ui/options.js @@ -335,25 +335,25 @@ function copyMapURL() { .catch(err => tip("Could not copy URL: " + err, false, "error", 5000)); } -function changeCellsDensity(value) { - const convert = v => { - if (v == 1) return 1000; - if (v == 2) return 2000; - if (v == 3) return 5000; - if (v == 4) return 10000; - if (v == 5) return 20000; - if (v == 6) return 30000; - if (v == 7) return 40000; - if (v == 8) return 50000; - if (v == 9) return 60000; - if (v == 10) return 70000; - if (v == 11) return 80000; - if (v == 12) return 90000; - if (v == 13) return 100000; - }; - const cells = convert(value); +const cellsDensityConstants = { + 1: 1000, + 2: 2000, + 3: 5000, + 4: 10000, + 5: 20000, + 6: 30000, + 7: 40000, + 8: 50000, + 9: 60000, + 10: 70000, + 11: 80000, + 12: 90000, + 13: 100000, +}; - pointsInput.setAttribute("data-cells", cells); +function changeCellsDensity(value) { + const cells = value in cellsDensityConstants? cellsDensityConstants[value]: 1000; + pointsInput.dataset.cells = cells; pointsOutput_formatted.value = cells / 1000 + "K"; pointsOutput_formatted.style.color = cells > 50000 ? "#b12117" : cells !== 10000 ? "#dfdf12" : "#053305"; } diff --git a/modules/ui/style.js b/modules/ui/style.js index b4179fea..f1586f35 100644 --- a/modules/ui/style.js +++ b/modules/ui/style.js @@ -574,20 +574,20 @@ addFontMethod.addEventListener("change", function () { }); styleFontSize.addEventListener("change", function () { - changeFontSize(+this.value); + changeFontSize(getEl(), +this.value); }); styleFontPlus.addEventListener("click", function () { const size = +getEl().attr("data-size") + 1; - changeFontSize(Math.min(size, 999)); + changeFontSize(getEl(), Math.min(size, 999)); }); styleFontMinus.addEventListener("click", function () { const size = +getEl().attr("data-size") - 1; - changeFontSize(Math.max(size, 1)); + changeFontSize(getEl(), Math.max(size, 1)); }); -function changeFontSize(size) { +function changeFontSize(el, size) { styleFontSize.value = size; const getSizeOnScale = element => { @@ -600,7 +600,7 @@ function changeFontSize(size) { }; const scaleSize = getSizeOnScale(styleElementSelect.value); - getEl().attr("data-size", size).attr("font-size", scaleSize); + el.attr("data-size", size).attr("font-size", scaleSize); if (styleElementSelect.value === "legend") redrawLegend(); } diff --git a/modules/ui/submap.js b/modules/ui/submap.js new file mode 100644 index 00000000..d3e2d917 --- /dev/null +++ b/modules/ui/submap.js @@ -0,0 +1,156 @@ +"use strict"; + +/* +UI elements for submap generation +*/ + +function openSubmapOptions() { + $("#submapOptionsDialog").dialog({ + title: "Submap options", + resizable: false, + width: fitContent(), + position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}, + buttons: { + Submap: function () { + $(this).dialog("close"); + generateSubmap(); + }, + Cancel: function () { $(this).dialog("close"); }, + } + }); +} + +function openRemapOptions() { + resetZoom(0); + $("#remapOptionsDialog").dialog({ + title: "Resampler options", + resizable: false, + width: fitContent(), + position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}, + buttons: { + Resample: function () { + const cellNumId = Number(document.getElementById('submapPointsInput').value); + const cells = cellsDensityConstants[cellNumId]; + $(this).dialog("close"); + if (!cells) { + console.error('Unknown cell number!'); + return; + } + changeCellsDensity(cellNumId); + resampleCurrentMap(); + }, + Cancel: function () { $(this).dialog("close"); }, + }, + }); +} + +/* callbacks */ + +const resampleCurrentMap = debounce(function () { + // Resample the whole map to different cell resolution or shape + WARN && console.warn("Resampling current map"); + const options = { + lockMarkers: false, + lockBurgs: false, + depressRivers: false, + addLakesInDepressions: false, + promoteTowns: false, + smoothHeightMap: false, + projection: (x,y) => [x, y], + inverse: (x,y) => [x, y], + } + + startResample(options); +}, 1000); + + +const generateSubmap = debounce(function () { + // Create submap from the current map + // submap limits defined by the current window size (canvas viewport) + + WARN && console.warn("Resampling current map"); + closeDialogs("#worldConfigurator, #options3d"); + const checked = id => Boolean(document.getElementById(id).checked) + // Create projection func from current zoom extents + const [[x0, y0], [x1, y1]] = getViewBoxExtent(); + + const options = { + lockMarkers: checked("submapLockMarkers"), + lockBurgs: checked("submapLockBurgs"), + + depressRivers: checked("submapDepressRivers"), + addLakesInDepressions: checked("submapAddLakeInDepression"), + promoteTowns: checked("submapPromoteTowns"), + smoothHeightMap: scale > 2, + inverse: (x,y) => [x * (x1-x0) / graphWidth + x0, y * (y1-y0) / graphHeight + y0], + projection: (x, y) => [(x-x0) * graphWidth / (x1-x0), (y-y0) * graphHeight / (y1-y0)], + } + + // converting map position on the planet + const mapSizeOutput = document.getElementById("mapSizeOutput"); + const latitudeOutput = document.getElementById("latitudeOutput"); + const latN = 90 - (180 - mapSizeInput.value / 100 * 180) * latitudeOutput.value / 100; + const newLatN = latN - y0 / graphHeight * mapSizeOutput.value * 180 / 100; + mapSizeOutput.value /= scale; + latitudeOutput.value = (90 - newLatN) / (180 - mapSizeOutput.value / 100 * 180) * 100; + document.getElementById("mapSizeInput").value = mapSizeOutput.value; + document.getElementById("latitudeInput").value = latitudeOutput.value; + + // fix scale + distanceScaleInput.value = distanceScaleOutput.value = rn(distanceScale = distanceScaleOutput.value / scale, 2); + populationRateInput.value = populationRateOutput.value = rn(populationRate = populationRateOutput.value / scale, 2); + customization = 0; + startResample(options); +}, 1000); + + +async function startResample(options) { + undraw(); + resetZoom(0); + let oldstate = { + grid: deepCopy(grid), + pack: deepCopy(pack), + seed, + graphWidth, + graphHeight, + }; + + try { + const oldScale = scale; + await Submap.resample(oldstate, options); + if (options.promoteTowns) { + const groupName = 'largetowns'; + moveAllBurgsToGroup('towns', groupName); + changeRadius(rn(oldScale * 0.8,2), groupName); + changeFontSize(svg.select(`#labels #${groupName}`), rn(oldScale*2, 2)); + invokeActiveZooming(); + } + } catch (error) { + showSubmapErrorHandler(error); + } + + oldstate = null; // destroy old state to free memory + + restoreLayers(); + turnButtonOn('toggleMarkers'); + if (ThreeD.options.isOn) ThreeD.redraw(); + if ($("#worldConfigurator").is(":visible")) editWorld(); +} + +function showSubmapErrorHandler(error) { + ERROR && console.error(error); + clearMainTip(); + + alertMessage.innerHTML = `Map resampling failed :_(. +
You may retry after clearing stored data or contact us at discord. +

${parseError(error)}

`; + $("#alert").dialog({ + resizable: false, + title: "Generation error", + width: "32em", + buttons: { + Ok: function () { $(this).dialog("close"); } + }, + position: {my: "center", at: "center", of: "svg"} + }); +} diff --git a/modules/ui/units-editor.js b/modules/ui/units-editor.js index 535b7e14..0f1345e0 100644 --- a/modules/ui/units-editor.js +++ b/modules/ui/units-editor.js @@ -103,6 +103,7 @@ function editUnits() { function restoreDefaultUnits() { // distanceScale + distanceScale = 3; document.getElementById("distanceScaleOutput").value = 3; document.getElementById("distanceScaleInput").value = 3; unlock("distanceScale"); diff --git a/utils/arrayUtils.js b/utils/arrayUtils.js index 854800ce..b59d48e3 100644 --- a/utils/arrayUtils.js +++ b/utils/arrayUtils.js @@ -15,3 +15,37 @@ function common(a, b) { function unique(array) { return [...new Set(array)]; } + +// deep copy for Arrays (and other objects) +function deepCopy(obj) { + const id = x=>x; + const dcTArray = a => a.map(id); + const dcObject = x => Object.fromEntries(Object.entries(x).map(([k,d])=>[k,dcAny(d)])); + const dcAny = x => x instanceof Object ? (cf.get(x.constructor)||id)(x) : x; + // don't map keys, probably this is what we would expect + const dcMapCore = m => [...m.entries()].map(([k,v])=>[k, dcAny(v)]); + + const cf = new Map([ + [Int8Array, dcTArray], + [Uint8Array, dcTArray], + [Uint8ClampedArray, dcTArray], + [Int16Array, dcTArray], + [Uint16Array, dcTArray], + [Int32Array, dcTArray], + [Uint32Array, dcTArray], + [Float32Array, dcTArray], + [Float64Array, dcTArray], + [BigInt64Array, dcTArray], + [BigUint64Array, dcTArray], + [Map, m => new Map(dcMapCore(m))], + [WeakMap, m => new WeakMap(dcMapCore(m))], + [Array, a => a.map(dcAny)], + [Set, s => [...s.values()].map(dcAny)], + [Date, d => new Date(d.getTime())], + [Object, dcObject], + // other types will be referenced + // ... extend here to implement their custom deep copy + ]); + + return dcAny(obj); +} From ae0979fc68addd8daf93279ff5140f55dbf54fa8 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Fri, 15 Apr 2022 14:47:56 +0500 Subject: [PATCH 08/25] prettify --- index.html | 10522 ++++++++++++++++++++++++----------------- modules/submap.js | 172 +- modules/ui/submap.js | 56 +- 3 files changed, 6211 insertions(+), 4539 deletions(-) diff --git a/index.html b/index.html index d7ce4e0a..88e63977 100644 --- a/index.html +++ b/index.html @@ -1,1300 +1,1648 @@ - - - - Azgaar's Fantasy Map Generator - - - - - - - - - - + + + + Azgaar's Fantasy Map Generator + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
Azgaar's
-
Fantasy Map Generator
-
v. 1.73
-

LOADING...

-
- -
- -
- - +
+
Azgaar's
+
Fantasy Map Generator
+
v. 1.73
+

LOADING...

-