From 9064ffb388a3fa0e02d010e21b11fa61aa556b74 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Fri, 13 Jan 2023 01:53:23 +0400 Subject: [PATCH 01/28] fix: define religions array even if religions count is 0, v1.89.01 --- index.html | 2 +- modules/religions-generator.js | 8 ++++++-- versioning.js | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/index.html b/index.html index b852ad3c..5df7f98f 100644 --- a/index.html +++ b/index.html @@ -7848,7 +7848,7 @@ - + diff --git a/modules/religions-generator.js b/modules/religions-generator.js index d823b4c6..4d06d327 100644 --- a/modules/religions-generator.js +++ b/modules/religions-generator.js @@ -397,8 +397,12 @@ window.Religions = (function () { }); }); - if (religionsInput.value == 0 || pack.cultures.length < 2) - return religions.filter(r => r.i).forEach(r => (r.code = abbreviate(r.name))); + if (religionsInput.value == 0 || pack.cultures.length < 2) { + religions.filter(r => r.i).forEach(r => (r.code = abbreviate(r.name))); + cells.religion = religionIds; + pack.religions = religions; + return; + } const burgs = pack.burgs.filter(b => b.i && !b.removed); const sorted = diff --git a/versioning.js b/versioning.js index 89816315..ae9969f2 100644 --- a/versioning.js +++ b/versioning.js @@ -1,7 +1,7 @@ "use strict"; // version and caching control -const version = "1.89.00"; // generator version, update each time +const version = "1.89.01"; // generator version, update each time { document.title += " v" + version; From 8a1122e6689fc24a556b4ccb8afa7c7db95790f1 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Sun, 22 Jan 2023 14:18:59 +0400 Subject: [PATCH 02/28] fix: remove label path of regeneration --- modules/burgs-and-states.js | 2 +- modules/ui/tools.js | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/modules/burgs-and-states.js b/modules/burgs-and-states.js index c3d2704d..e9069e83 100644 --- a/modules/burgs-and-states.js +++ b/modules/burgs-and-states.js @@ -608,7 +608,7 @@ window.BurgsAndStates = (function () { if (list && !list.includes(state.i)) continue; byId(`stateLabel${state.i}`)?.remove(); - byId(`textPath_stateLabel6${state.i}`)?.remove(); + byId(`textPath_stateLabel${state.i}`)?.remove(); } const example = g.append("text").attr("x", 0).attr("x", 0).text("Average"); diff --git a/modules/ui/tools.js b/modules/ui/tools.js index 12d76bb0..00913e0b 100644 --- a/modules/ui/tools.js +++ b/modules/ui/tools.js @@ -74,10 +74,8 @@ toolsContent.addEventListener("click", function (event) { }); function processFeatureRegeneration(event, button) { - if (button === "regenerateStateLabels") { - BurgsAndStates.drawStateLabels(); - if (!layerIsOn("toggleLabels")) toggleLabels(); - } else if (button === "regenerateReliefIcons") { + if (button === "regenerateStateLabels") BurgsAndStates.drawStateLabels(); + else if (button === "regenerateReliefIcons") { ReliefIcons(); if (!layerIsOn("toggleRelief")) toggleRelief(); } else if (button === "regenerateRoutes") { From 660316e4bf5bbfad2f3bf1a71448fe62048a98b1 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Sun, 22 Jan 2023 22:37:18 +0400 Subject: [PATCH 03/28] fix: culture data change must not ignore lock status --- index.html | 2 +- modules/dynamic/editors/cultures-editor.js | 11 ++--------- modules/dynamic/editors/states-editor.js | 4 ++-- modules/ui/editors.js | 4 ++-- sw.js | 8 ++++++-- versioning.js | 2 +- 6 files changed, 14 insertions(+), 17 deletions(-) diff --git a/index.html b/index.html index 5df7f98f..7684c476 100644 --- a/index.html +++ b/index.html @@ -7867,7 +7867,7 @@ - + diff --git a/modules/dynamic/editors/cultures-editor.js b/modules/dynamic/editors/cultures-editor.js index 44565449..3f8018d6 100644 --- a/modules/dynamic/editors/cultures-editor.js +++ b/modules/dynamic/editors/cultures-editor.js @@ -228,7 +228,7 @@ function culturesEditorAddLines() { style="width: 5em">${si(population)} ${getShapeOptions(selectShape, c.shield)} - + `; } @@ -251,7 +251,7 @@ function culturesEditorAddLines() { $body.querySelectorAll("fill-box").forEach($el => $el.on("click", cultureChangeColor)); $body.querySelectorAll("div > input.cultureName").forEach($el => $el.on("input", cultureChangeName)); $body.querySelectorAll("div > span.icon-cw").forEach($el => $el.on("click", cultureRegenerateName)); - $body.querySelectorAll("div > input.cultureExpan").forEach($el => $el.on("input", cultureChangeExpansionism)); + $body.querySelectorAll("div > input.cultureExpan").forEach($el => $el.on("change", cultureChangeExpansionism)); $body.querySelectorAll("div > select.cultureType").forEach($el => $el.on("change", cultureChangeType)); $body.querySelectorAll("div > select.cultureBase").forEach($el => $el.on("change", cultureChangeBase)); $body.querySelectorAll("div > select.cultureEmblems").forEach($el => $el.on("change", cultureChangeEmblemsShape)); @@ -666,17 +666,10 @@ async function showHierarchy() { function recalculateCultures(must) { if (!must && !culturesAutoChange.checked) return; - pack.cells.culture = new Uint16Array(pack.cells.i.length); - pack.cultures.forEach(function (c) { - if (!c.i || c.removed) return; - pack.cells.culture[c.center] = c.i; - }); - Cultures.expand(); drawCultures(); pack.burgs.forEach(b => (b.culture = pack.cells.culture[b.cell])); refreshCulturesEditor(); - document.querySelector("input.cultureExpan").focus(); // to not trigger hotkeys } function enterCultureManualAssignent() { diff --git a/modules/dynamic/editors/states-editor.js b/modules/dynamic/editors/states-editor.js index 773d421d..87592706 100644 --- a/modules/dynamic/editors/states-editor.js +++ b/modules/dynamic/editors/states-editor.js @@ -163,8 +163,6 @@ function addListeners() { const line = $element.parentNode; const state = +line.dataset.id; if (classList.contains("stateCapital")) stateChangeCapitalName(state, line, $element.value); - else if (classList.contains("cultureType")) stateChangeType(state, line, $element.value); - else if (classList.contains("statePower")) stateChangeExpansionism(state, line, $element.value); }); $body.on("change", function (ev) { @@ -173,6 +171,8 @@ function addListeners() { const line = $element.parentNode; const state = +line.dataset.id; if (classList.contains("stateCulture")) stateChangeCulture(state, line, $element.value); + else if (classList.contains("cultureType")) stateChangeType(state, line, $element.value); + else if (classList.contains("statePower")) stateChangeExpansionism(state, line, $element.value); }); } diff --git a/modules/ui/editors.js b/modules/ui/editors.js index 74ed5906..2d40c916 100644 --- a/modules/ui/editors.js +++ b/modules/ui/editors.js @@ -1176,13 +1176,13 @@ function refreshAllEditors() { // dynamically loaded editors async function editStates() { if (customization) return; - const Editor = await import("../dynamic/editors/states-editor.js?v=12062022"); + const Editor = await import("../dynamic/editors/states-editor.js?v=1.89.02"); Editor.open(); } async function editCultures() { if (customization) return; - const Editor = await import("../dynamic/editors/cultures-editor.js?v=1.88.06"); + const Editor = await import("../dynamic/editors/cultures-editor.js?v=1.89.02"); Editor.open(); } diff --git a/sw.js b/sw.js index ce803d5b..4e214e8e 100644 --- a/sw.js +++ b/sw.js @@ -8,7 +8,10 @@ const {ExpirationPlugin} = workbox.expiration; const DAY = 24 * 60 * 60; const getPolitics = ({entries, days}) => { - return [new CacheableResponsePlugin({statuses: [0, 200]}), new ExpirationPlugin({maxEntries: entries, maxAgeSeconds: days * DAY})]; + return [ + new CacheableResponsePlugin({statuses: [0, 200]}), + new ExpirationPlugin({maxEntries: entries, maxAgeSeconds: days * DAY}) + ]; }; registerRoute( @@ -21,7 +24,8 @@ registerRoute( ); registerRoute( - ({request, url}) => request.destination === "script" && !url.pathname.endsWith("min.js") && !url.pathname.includes("versioning.js"), + ({request, url}) => + request.destination === "script" && !url.pathname.endsWith("min.js") && !url.pathname.includes("versioning.js"), new CacheFirst({ cacheName: "fmg-scripts", plugins: getPolitics({entries: 100, days: 30}) diff --git a/versioning.js b/versioning.js index ae9969f2..b23bbd01 100644 --- a/versioning.js +++ b/versioning.js @@ -1,7 +1,7 @@ "use strict"; // version and caching control -const version = "1.89.01"; // generator version, update each time +const version = "1.89.02"; // generator version, update each time { document.title += " v" + version; From dd56c7e43387f4fa4751cf7f487e91a75942c59f Mon Sep 17 00:00:00 2001 From: Azgaar Date: Sun, 22 Jan 2023 23:39:24 +0400 Subject: [PATCH 04/28] doc: improve readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7369d7ed..efd8224e 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ # Fantasy Map Generator -Azgaar's _Fantasy Map Generator_ is a free web application generating interactive and highly customizable svg maps based on voronoi diagram. +Azgaar's _Fantasy Map Generator_ is a free web application that helps fantasy writers, game masters, and cartographers create and edit fantasy maps. -Project is under development, the current version is available on [Github Pages](https://azgaar.github.io/Fantasy-Map-Generator). +Link: [azgaar.github.io/Fantasy-Map-Generator](https://azgaar.github.io/Fantasy-Map-Generator). Refer to the [project wiki](https://github.com/Azgaar/Fantasy-Map-Generator/wiki) for guidance. The current progress is tracked in [Trello](https://trello.com/b/7x832DG4/fantasy-map-generator). Some details are covered in my old blog [_Fantasy Maps for fun and glory_](https://azgaar.wordpress.com). From 16e0aef2fb65799cf5c8e414c093b50c45409bf4 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Mon, 23 Jan 2023 00:11:20 +0400 Subject: [PATCH 05/28] fix: notes editor shows up halfway off the screen --- Readme.txt | 9 --------- index.css | 1 + index.html | 2 +- modules/ui/notes-editor.js | 3 +-- versioning.js | 2 +- 5 files changed, 4 insertions(+), 13 deletions(-) delete mode 100644 Readme.txt diff --git a/Readme.txt b/Readme.txt deleted file mode 100644 index 9d270a7c..00000000 --- a/Readme.txt +++ /dev/null @@ -1,9 +0,0 @@ -Azgaar's Fantasy Map Generator - -Developed by Azgaar (azgaar.fmg@yandex.com) and contributors - -Minsk, 2017-2021. MIT License - -https://github.com/Azgaar/Fantasy-Map-Generator - -To run the tool unzip ALL files and open index.html in browser \ No newline at end of file diff --git a/index.css b/index.css index bfd1ed67..945ccd04 100644 --- a/index.css +++ b/index.css @@ -2045,6 +2045,7 @@ div.textual span, } #notesLegend { + width: auto; height: 87%; outline: 0; overflow-y: auto; diff --git a/index.html b/index.html index 7684c476..4a97d69b 100644 --- a/index.html +++ b/index.html @@ -7886,7 +7886,7 @@ - + diff --git a/modules/ui/notes-editor.js b/modules/ui/notes-editor.js index 4362fb25..36b51671 100644 --- a/modules/ui/notes-editor.js +++ b/modules/ui/notes-editor.js @@ -42,12 +42,11 @@ function editNotes(id, name) { $("#notesEditor").dialog({ title: "Notes Editor", - width: "minmax(80vw, 540px)", + width: window.innerWidth * 0.8, height: window.innerHeight * 0.75, position: {my: "center", at: "center", of: "svg"}, close: removeEditor }); - $("[aria-describedby='notesEditor']").css("top", "10vh"); if (modules.editNotes) return; modules.editNotes = true; diff --git a/versioning.js b/versioning.js index b23bbd01..257b6cda 100644 --- a/versioning.js +++ b/versioning.js @@ -1,7 +1,7 @@ "use strict"; // version and caching control -const version = "1.89.02"; // generator version, update each time +const version = "1.89.03"; // generator version, update each time { document.title += " v" + version; From 7d500b1598bb846c3a8e0df6057ee756e25630e0 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Mon, 23 Jan 2023 00:17:00 +0400 Subject: [PATCH 06/28] feat: routes - increase space between contol points --- index.html | 2 +- modules/ui/routes-editor.js | 11 +++++++---- versioning.js | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/index.html b/index.html index 4a97d69b..1fb860ab 100644 --- a/index.html +++ b/index.html @@ -7876,7 +7876,7 @@ - + diff --git a/modules/ui/routes-editor.js b/modules/ui/routes-editor.js index 785c22a9..ca52b036 100644 --- a/modules/ui/routes-editor.js +++ b/modules/ui/routes-editor.js @@ -1,4 +1,7 @@ "use strict"; + +const CONTROL_POINST_DISTANCE = 10; + function editRoute(onClick) { if (customization) return; if (!onClick && elSelected && d3.event.target.id === elSelected.attr("id")) return; @@ -47,13 +50,13 @@ function editRoute(onClick) { } function drawControlPoints(node) { - const l = node.getTotalLength(); - const increment = l / Math.ceil(l / 4); - for (let i = 0; i <= l; i += increment) { + const totalLength = node.getTotalLength(); + const increment = totalLength / Math.ceil(totalLength / CONTROL_POINST_DISTANCE); + for (let i = 0; i <= totalLength; i += increment) { const point = node.getPointAtLength(i); addControlPoint([point.x, point.y]); } - routeLength.innerHTML = rn(l * distanceScaleInput.value) + " " + distanceUnitInput.value; + routeLength.innerHTML = rn(totalLength * distanceScaleInput.value) + " " + distanceUnitInput.value; } function addControlPoint(point, before = null) { diff --git a/versioning.js b/versioning.js index 257b6cda..734e3886 100644 --- a/versioning.js +++ b/versioning.js @@ -1,7 +1,7 @@ "use strict"; // version and caching control -const version = "1.89.03"; // generator version, update each time +const version = "1.89.04"; // generator version, update each time { document.title += " v" + version; From eb5d924cbd960abb7a36c72ee7a635f1b7cec4b0 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Sun, 5 Feb 2023 00:49:05 +0400 Subject: [PATCH 07/28] fix: state expansion to reset on re-generation --- index.html | 6 ++--- main.js | 1 - modules/burgs-and-states.js | 32 ++++++++++++++++-------- modules/dynamic/editors/states-editor.js | 1 - modules/ui/editors.js | 2 +- versioning.js | 2 +- 6 files changed, 26 insertions(+), 18 deletions(-) diff --git a/index.html b/index.html index 1fb860ab..baa5562b 100644 --- a/index.html +++ b/index.html @@ -7846,7 +7846,7 @@ - + @@ -7863,11 +7863,11 @@ - + - + diff --git a/main.js b/main.js index 69323e92..5cf0edc3 100644 --- a/main.js +++ b/main.js @@ -191,7 +191,6 @@ let populationRate = +document.getElementById("populationRateInput").value; let distanceScale = +document.getElementById("distanceScaleInput").value; let urbanization = +document.getElementById("urbanizationInput").value; let urbanDensity = +document.getElementById("urbanDensityInput").value; -let statesNeutral = 1; // statesEditor growth parameter applyStoredOptions(); diff --git a/modules/burgs-and-states.js b/modules/burgs-and-states.js index e9069e83..f0b13a05 100644 --- a/modules/burgs-and-states.js +++ b/modules/burgs-and-states.js @@ -367,18 +367,28 @@ window.BurgsAndStates = (function () { cells.state = cells.state || new Uint16Array(cells.i.length); const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); const cost = []; - const neutral = (cells.i.length / 5000) * 2500 * neutralInput.value * statesNeutral; // limit cost for state growth - states - .filter(s => s.i && !s.removed) - .forEach(s => { - const capitalCell = burgs[s.capital].cell; - cells.state[capitalCell] = s.i; - const cultureCenter = cultures[s.culture].center; - const b = cells.biome[cultureCenter]; // state native biome - queue.queue({e: s.center, p: 0, s: s.i, b}); - cost[s.center] = 1; - }); + const globalNeutralRate = byId("neutralInput")?.value || 1; + const statesNeutralRate = byId("statesNeutral")?.value || 1; + const neutral = (cells.i.length / 2) * globalNeutralRate * statesNeutralRate; // limit cost for state growth + + // remove state from all cells except of locked + for (const cellId of cells.i) { + const state = states[cells.state[cellId]]; + if (state.lock) continue; + cells.state[cellId] = 0; + } + + for (const state of states) { + if (!state.i || state.removed) continue; + + const capitalCell = burgs[state.capital].cell; + cells.state[capitalCell] = state.i; + const cultureCenter = cultures[state.culture].center; + const b = cells.biome[cultureCenter]; // state native biome + queue.queue({e: state.center, p: 0, s: state.i, b}); + cost[state.center] = 1; + } while (queue.length) { const next = queue.dequeue(); diff --git a/modules/dynamic/editors/states-editor.js b/modules/dynamic/editors/states-editor.js index 87592706..41241340 100644 --- a/modules/dynamic/editors/states-editor.js +++ b/modules/dynamic/editors/states-editor.js @@ -883,7 +883,6 @@ function changeStatesGrowthRate() { const growthRate = +this.value; byId("statesNeutral").value = growthRate; byId("statesNeutralNumber").value = growthRate; - statesNeutral = growthRate; tip("Growth rate: " + growthRate); recalculateStates(false); } diff --git a/modules/ui/editors.js b/modules/ui/editors.js index 2d40c916..54ab8300 100644 --- a/modules/ui/editors.js +++ b/modules/ui/editors.js @@ -1176,7 +1176,7 @@ function refreshAllEditors() { // dynamically loaded editors async function editStates() { if (customization) return; - const Editor = await import("../dynamic/editors/states-editor.js?v=1.89.02"); + const Editor = await import("../dynamic/editors/states-editor.js?v=1.89.05"); Editor.open(); } diff --git a/versioning.js b/versioning.js index 734e3886..ff5c3ef3 100644 --- a/versioning.js +++ b/versioning.js @@ -1,7 +1,7 @@ "use strict"; // version and caching control -const version = "1.89.04"; // generator version, update each time +const version = "1.89.05"; // generator version, update each time { document.title += " v" + version; From 994c240183a41a687a748395747e81670f5d5029 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Thu, 9 Feb 2023 13:12:37 -0800 Subject: [PATCH 08/28] doc: update readme --- heightmaps/import-rules.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/heightmaps/import-rules.txt b/heightmaps/import-rules.txt index 9478b35c..69499114 100644 --- a/heightmaps/import-rules.txt +++ b/heightmaps/import-rules.txt @@ -1,8 +1,8 @@ To get heightmap with correct height scale: -1. Open tangrams.github.io +1. Open https://tangrams.github.io/heightmapper 2. Toggle off auto-exposure 3. Set max elevation to 2000 4. Set min elevation to -500 5. Find region you like 6. Render image -7. Optionally rescale image to a smaller size (e.g. 500x300px) as high resolution is not used \ No newline at end of file +7. Optionally rescale image to a smaller size (e.g. 500x300px) as high resolution is not used From 8288335514ae384b4b213d03c12d836fae5098dc Mon Sep 17 00:00:00 2001 From: Azgaar Date: Sun, 19 Feb 2023 14:05:36 +0400 Subject: [PATCH 09/28] fix: erase data before regeneration on heightmap erase mode to avoid lock state, v1.89.06 --- index.html | 4 ++-- modules/cultures-generator.js | 12 ++++++------ modules/ui/heightmap-editor.js | 9 +++++++++ versioning.js | 2 +- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/index.html b/index.html index baa5562b..90594d0b 100644 --- a/index.html +++ b/index.html @@ -7845,7 +7845,7 @@ - + @@ -7870,7 +7870,7 @@ - + diff --git a/modules/cultures-generator.js b/modules/cultures-generator.js index 90f93d69..48090db3 100644 --- a/modules/cultures-generator.js +++ b/modules/cultures-generator.js @@ -116,14 +116,14 @@ window.Cultures = (function () { cultures.forEach(c => (c.base = c.base % nameBases.length)); - function selectCultures(c) { - let def = getDefault(c); - if (c === def.length) return def; - if (def.every(d => d.odd === 1)) return def.splice(0, c); + function selectCultures(culturesNumber) { + let def = getDefault(culturesNumber); + if (culturesNumber === def.length) return def; + if (def.every(d => d.odd === 1)) return def.splice(0, culturesNumber); + + const count = Math.min(culturesNumber, def.length); - const count = Math.min(c, def.length); const cultures = []; - pack.cultures?.forEach(function (culture) { if (culture.lock) cultures.push(culture); }); diff --git a/modules/ui/heightmap-editor.js b/modules/ui/heightmap-editor.js index 07216f90..bd41cefe 100644 --- a/modules/ui/heightmap-editor.js +++ b/modules/ui/heightmap-editor.js @@ -204,6 +204,13 @@ function editHeightmap(options) { INFO && console.group("Edit Heightmap"); TIME && console.time("regenerateErasedData"); + // remove data + pack.cultures = []; + pack.burgs = []; + pack.states = []; + pack.provinces = []; + pack.religions = []; + const erosionAllowed = allowErosion.checked; markFeatures(); markupGridOcean(); @@ -231,8 +238,10 @@ function editHeightmap(options) { Lakes.defineGroup(); defineBiomes(); rankCells(); + Cultures.generate(); Cultures.expand(); + BurgsAndStates.generate(); Religions.generate(); BurgsAndStates.defineStateForms(); diff --git a/versioning.js b/versioning.js index ff5c3ef3..e53ca4c2 100644 --- a/versioning.js +++ b/versioning.js @@ -1,7 +1,7 @@ "use strict"; // version and caching control -const version = "1.89.05"; // generator version, update each time +const version = "1.89.06"; // generator version, update each time { document.title += " v" + version; From d40cab2e284217140fd62d7eefb13418e2ee7a0c Mon Sep 17 00:00:00 2001 From: Azgaar Date: Sun, 19 Feb 2023 18:11:07 +0400 Subject: [PATCH 10/28] fix: clean cultures on regeneration, v1.89.07 --- index.html | 8 ++--- modules/burgs-and-states.js | 6 ++-- modules/cultures-generator.js | 39 +++++++++++++-------- modules/dynamic/editors/religions-editor.js | 4 +-- modules/religions-generator.js | 15 ++++---- modules/ui/editors.js | 2 +- versioning.js | 2 +- 7 files changed, 41 insertions(+), 35 deletions(-) diff --git a/index.html b/index.html index 90594d0b..84323f43 100644 --- a/index.html +++ b/index.html @@ -7845,10 +7845,10 @@ - - + + - + @@ -7867,7 +7867,7 @@ - + diff --git a/modules/burgs-and-states.js b/modules/burgs-and-states.js index f0b13a05..e360b45a 100644 --- a/modules/burgs-and-states.js +++ b/modules/burgs-and-states.js @@ -359,7 +359,7 @@ window.BurgsAndStates = (function () { TIME && console.timeEnd("drawBurgs"); }; - // growth algorithm to assign cells to states like we did for cultures + // expand cultures across the map (Dijkstra-like algorithm) const expandStates = function () { TIME && console.time("expandStates"); const {cells, states, cultures, burgs} = pack; @@ -368,8 +368,8 @@ window.BurgsAndStates = (function () { const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); const cost = []; - const globalNeutralRate = byId("neutralInput")?.value || 1; - const statesNeutralRate = byId("statesNeutral")?.value || 1; + const globalNeutralRate = byId("neutralInput")?.valueAsNumber || 1; + const statesNeutralRate = byId("statesNeutral")?.valueAsNumber || 1; const neutral = (cells.i.length / 2) * globalNeutralRate * statesNeutralRate; // limit cost for state growth // remove state from all cells except of locked diff --git a/modules/cultures-generator.js b/modules/cultures-generator.js index 48090db3..eeb09407 100644 --- a/modules/cultures-generator.js +++ b/modules/cultures-generator.js @@ -507,28 +507,37 @@ window.Cultures = (function () { // expand cultures across the map (Dijkstra-like algorithm) const expand = function () { TIME && console.time("expandCultures"); - cells = pack.cells; + const {cells, cultures} = pack; const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); - pack.cultures.forEach(function (c) { - if (!c.i || c.removed || c.lock) return; - queue.queue({e: c.center, p: 0, c: c.i}); - }); - - const neutral = (cells.i.length / 5000) * 3000 * neutralInput.value; // limit cost for culture growth const cost = []; + + const neutralRate = byId("neutralRate")?.valueAsNumber || 1; + const neutral = cells.i.length * 0.6 * neutralRate; // limit cost for culture growth + + // remove culture from all cells except of locked + for (const cellId of cells.i) { + const culture = cultures[cells.culture[cellId]]; + if (culture.lock) continue; + cells.culture[cellId] = 0; + } + + for (const culture of cultures) { + if (!culture.i || culture.removed) continue; + queue.queue({e: culture.center, p: 0, c: culture.i}); + } + while (queue.length) { - const next = queue.dequeue(), - n = next.e, - p = next.p, - c = next.c; - const type = pack.cultures[c].type; - cells.c[n].forEach(e => { - if (pack.cultures[cells.culture[e]]?.lock) return; + const {e, p, c} = queue.dequeue(); + const {type} = pack.cultures[c]; + + cells.c[e].forEach(e => { + const culture = cells.culture[e]; + if (culture?.lock) return; // do not overwrite cell of locked culture const biome = cells.biome[e]; const biomeCost = getBiomeCost(c, biome, type); - const biomeChangeCost = biome === cells.biome[n] ? 0 : 20; // penalty on biome change + const biomeChangeCost = biome === cells.biome[e] ? 0 : 20; // penalty on biome change const heightCost = getHeightCost(e, cells.h[e], type); const riverCost = getRiverCost(cells.r[e], e, type); const typeCost = getTypeCost(cells.t[e], type); diff --git a/modules/dynamic/editors/religions-editor.js b/modules/dynamic/editors/religions-editor.js index 4cf1903f..8a4abc5e 100644 --- a/modules/dynamic/editors/religions-editor.js +++ b/modules/dynamic/editors/religions-editor.js @@ -3,11 +3,11 @@ addListeners(); export function open() { closeDialogs("#religionsEditor, .stable"); - if (!layerIsOn("toggleReligions")) toggleCultures(); if (layerIsOn("toggleStates")) toggleStates(); if (layerIsOn("toggleBiomes")) toggleBiomes(); if (layerIsOn("toggleCultures")) toggleReligions(); if (layerIsOn("toggleProvinces")) toggleProvinces(); + if (!layerIsOn("toggleReligions")) toggleReligions(); refreshReligionsEditor(); drawReligionCenters(); @@ -214,7 +214,7 @@ function religionsEditorAddLines() {
${si(population)}
`; diff --git a/modules/religions-generator.js b/modules/religions-generator.js index 4d06d327..0ae9be91 100644 --- a/modules/religions-generator.js +++ b/modules/religions-generator.js @@ -350,9 +350,8 @@ window.Religions = (function () { const religions = []; // add folk religions - pack.cultures.forEach(c => { - const newId = c.i; - if (!newId) return religions.push({i: 0, name: "No religion"}); + cultures.forEach(c => { + if (!c.i) return religions.push({i: 0, name: "No religion"}); if (c.removed) { religions.push({ @@ -364,6 +363,8 @@ window.Religions = (function () { return; } + const newId = c.i; + if (pack.religions) { const lockedFolkReligion = pack.religions.find( r => r.culture === c.i && !r.removed && r.lock && r.type === "Folk" @@ -383,7 +384,7 @@ window.Religions = (function () { const form = rw(forms.Folk); const name = c.name + " " + rw(types[form]); const deity = form === "Animism" ? null : getDeityName(c.i); - const color = getMixedColor(c.color, 0.1, 0); // `url(#hatch${rand(8,13)})`; + const color = getMixedColor(c.color, 0.1, 0); religions.push({ i: newId, name, @@ -713,14 +714,10 @@ window.Religions = (function () { }; function updateCultures() { - TIME && console.time("updateCulturesForReligions"); pack.religions = pack.religions.map((religion, index) => { - if (index === 0) { - return religion; - } + if (index === 0) return religion; return {...religion, culture: pack.cells.culture[religion.center]}; }); - TIME && console.timeEnd("updateCulturesForReligions"); } // get supreme deity name diff --git a/modules/ui/editors.js b/modules/ui/editors.js index 54ab8300..23798861 100644 --- a/modules/ui/editors.js +++ b/modules/ui/editors.js @@ -1188,6 +1188,6 @@ async function editCultures() { async function editReligions() { if (customization) return; - const Editor = await import("../dynamic/editors/religions-editor.js?v=1.88.06"); + const Editor = await import("../dynamic/editors/religions-editor.js?v=1.88.07"); Editor.open(); } diff --git a/versioning.js b/versioning.js index e53ca4c2..ec4f6f9d 100644 --- a/versioning.js +++ b/versioning.js @@ -1,7 +1,7 @@ "use strict"; // version and caching control -const version = "1.89.06"; // generator version, update each time +const version = "1.89.07"; // generator version, update each time { document.title += " v" + version; From 89d61fda5f144dcd3776f67dd62eb96e0140f9f2 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Sat, 25 Feb 2023 13:36:44 +0400 Subject: [PATCH 11/28] chore: change meta description --- index.html | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index 84323f43..a4e25780 100644 --- a/index.html +++ b/index.html @@ -3,14 +3,33 @@ + Azgaar's Fantasy Map Generator - - + + + - + + + + + + + + + From f018256f7af0b293581d9ec52fcfcd21822fd41d Mon Sep 17 00:00:00 2001 From: Azgaar Date: Sat, 25 Feb 2023 16:01:22 +0400 Subject: [PATCH 12/28] fix(#906): ice to follow expected size, reduce ice elements in general --- index.html | 8 ++++---- modules/ui/ice-editor.js | 8 ++++---- modules/ui/layers.js | 34 ++++++++++++++++------------------ utils/numberUtils.js | 4 ++++ versioning.js | 2 +- 5 files changed, 29 insertions(+), 27 deletions(-) diff --git a/index.html b/index.html index a4e25780..19795b6a 100644 --- a/index.html +++ b/index.html @@ -2768,7 +2768,7 @@ `; @@ -109,6 +116,7 @@ function addListeners() { byId("religionsManuallyCancel").on("click", () => exitReligionsManualAssignment()); byId("religionsAdd").on("click", enterAddReligionMode); byId("religionsExport").on("click", downloadReligionsCsv); + byId("religionsRecalculate").on("click", () => recalculateReligions(true)); } function refreshReligionsEditor() { @@ -166,6 +174,7 @@ function religionsEditorAddLines() { data-type="" data-form="" data-deity="" + data-expansion="" data-expansionism="" > @@ -181,6 +190,9 @@ function religionsEditorAddLines() {
${si(area) + unit}
${si(population)}
+ + + `; continue; } @@ -195,6 +207,7 @@ function religionsEditorAddLines() { data-type="${r.type}" data-form="${r.form}" data-deity="${r.deity || ""}" + data-expansion="${r.expansion}" data-expansionism="${r.expansionism}" > @@ -212,8 +225,9 @@ function religionsEditorAddLines() {
${si(area) + unit}
${si(population)}
+ ${getExpansionColumns(r)} @@ -245,6 +259,8 @@ function religionsEditorAddLines() { $body.querySelectorAll("div > input.religionDeity").forEach(el => el.on("input", religionChangeDeity)); $body.querySelectorAll("div > span.icon-arrows-cw").forEach(el => el.on("click", regenerateDeity)); $body.querySelectorAll("div > div.religionPopulation").forEach(el => el.on("click", changePopulation)); + $body.querySelectorAll("div > select.religionExtent").forEach(el => el.on("change", religionChangeExtent)); + $body.querySelectorAll("div > input.religionExpan").forEach(el => el.on("change", religionChangeExpansionism)); $body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.on("click", religionRemovePrompt)); $body.querySelectorAll("div > span.icon-lock").forEach($el => $el.on("click", updateLockStatus)); $body.querySelectorAll("div > span.icon-lock-open").forEach($el => $el.on("click", updateLockStatus)); @@ -264,6 +280,36 @@ function getTypeOptions(type) { return options; } +function getExpansionColumns(r) { + if (r.type === "Folk") + return ` + culture + + + ` + else + return ` + + ` +} + +function getExtentOptions(type) { + let options = ""; + const types = ["global", "state", "culture"]; + types.forEach(t => (options += ``)); + return options; +} + const religionHighlightOn = debounce(event => { const religionId = Number(event.id || event.target.dataset.id); const $el = $body.querySelector(`div[data-id='${religionId}']`); @@ -434,6 +480,20 @@ function changePopulation() { } } +function religionChangeExtent() { + const religion = +this.parentNode.dataset.id; + this.parentNode.dataset.expansion = this.value; + pack.religions[religion].expansion = this.value; + recalculateReligions(); +} + +function religionChangeExpansionism() { + const religion = +this.parentNode.dataset.id; + this.parentNode.dataset.expansionism = this.value; + pack.religions[religion].expansionism = +this.value; + recalculateReligions(); +} + function religionRemovePrompt() { if (customization) return; @@ -475,7 +535,7 @@ function drawReligionCenters() { .attr("stroke", "#444444") .style("cursor", "move"); - const data = pack.religions.filter(r => r.i && r.center && r.cells && !r.removed); + const data = pack.religions.filter(r => r.i && r.center && !r.removed); religionCenters .selectAll("circle") .data(data) @@ -507,6 +567,7 @@ function religionCenterDrag() { const cell = findCell(x, y); if (pack.cells.h[cell] < 20) return; // ignore dragging on water pack.religions[religionId].center = cell; + recalculateReligions(); }); } @@ -584,7 +645,7 @@ function enterReligionsManualAssignent() { if (!layerIsOn("toggleReligions")) toggleReligions(); customization = 7; relig.append("g").attr("id", "temp"); - document.querySelectorAll("#religionsBottom > button").forEach(el => (el.style.display = "none")); + document.querySelectorAll("#religionsBottom > *").forEach(el => (el.style.display = "none")); byId("religionsManuallyButtons").style.display = "inline-block"; debug.select("#religionCenters").style("display", "none"); @@ -686,7 +747,7 @@ function exitReligionsManualAssignment(close) { customization = 0; relig.select("#temp").remove(); removeCircle(); - document.querySelectorAll("#religionsBottom > button").forEach(el => (el.style.display = "inline-block")); + document.querySelectorAll("#religionsBottom > *").forEach(el => (el.style.display = "inline-block")); byId("religionsManuallyButtons").style.display = "none"; byId("religionsEditor") @@ -740,15 +801,15 @@ function addReligion() { function downloadReligionsCsv() { const unit = getAreaUnit("2"); - const headers = `Id,Name,Color,Type,Form,Supreme Deity,Area ${unit},Believers,Origins`; + const headers = `Id,Name,Color,Type,Form,Supreme Deity,Area ${unit},Believers,Origins,Potential,Expansionism`; const lines = Array.from($body.querySelectorAll(":scope > div")); const data = lines.map($line => { - const {id, name, color, type, form, deity, area, population} = $line.dataset; + const {id, name, color, type, form, deity, area, population, expansion, expansionism} = $line.dataset; const deityText = '"' + deity + '"'; const {origins} = pack.religions[+id]; const originList = (origins || []).filter(origin => origin).map(origin => pack.religions[origin].name); const originText = '"' + originList.join(", ") + '"'; - return [id, name, color, type, form, deityText, area, population, originText].join(","); + return [id, name, color, type, form, deityText, area, population, originText, expansion, expansionism].join(","); }); const csvData = [headers].concat(data).join("\n"); @@ -773,3 +834,13 @@ function updateLockStatus() { classList.toggle("icon-lock-open"); classList.toggle("icon-lock"); } + +function recalculateReligions(must) { + if (!must && !religionsAutoChange.checked) return; + + Religions.recalculate(); + + drawReligions(); + refreshReligionsEditor(); + drawReligionCenters(); +} diff --git a/modules/religions-generator.js b/modules/religions-generator.js index 0ae9be91..bd3eb9a4 100644 --- a/modules/religions-generator.js +++ b/modules/religions-generator.js @@ -304,15 +304,34 @@ window.Religions = (function () { Heresy: {Heresy: 1} }; - const methods = { - "Random + type": 3, - "Random + ism": 1, - "Supreme + ism": 5, - "Faith of + Supreme": 5, - "Place + ism": 1, - "Culture + ism": 2, - "Place + ian + type": 6, - "Culture + type": 4 + const namingMethods = { + Folk: { + "Culture + type": 1 + }, + + Organized: { + "Random + type": 3, + "Random + ism": 1, + "Supreme + ism": 5, + "Faith of + Supreme": 5, + "Place + ism": 1, + "Culture + ism": 2, + "Place + ian + type": 6, + "Culture + type": 4 + }, + + Cult: { + "Burg + ian + type": 2, + "Random + ian + type": 1, + "Type + of the + meaning": 2 + }, + + Heresy: { + "Burg + ian + type": 3, + "Random + ism": 3, + "Random + ian + type": 2, + "Type + of the + meaning": 1 + } }; const types = { @@ -342,372 +361,405 @@ window.Religions = (function () { } }; - const generate = function () { + const expansionismMap = { + Folk: () => 0, + Organized: () => gauss(5, 3, 0, 10, 1), // was rand(3, 8) + Cult: () => gauss(0.5, 0.5, 0, 5, 1), // was gauss(1.1, 0.5, 0, 5) + Heresy: () => gauss(1, 0.5, 0, 5, 1) // was gauss(1.2, 0.5, 0, 5) + }; + + function generate() { TIME && console.time("generateReligions"); - const {cells, states, cultures} = pack; + // const {cells, states, cultures, burgs} = pack; - const religionIds = new Uint16Array(cells.culture); // cell religion; initially based on culture - const religions = []; + const lockedReligions = pack.religions?.filter(religion => religion.lock && !religion.removed) || []; - // add folk religions - cultures.forEach(c => { - if (!c.i) return religions.push({i: 0, name: "No religion"}); + const folkReligions = generateFolkReligions(); + const basicReligions = generateOrganizedReligions(+religionsInput.value, lockedReligions); - if (c.removed) { - religions.push({ - i: c.i, - name: "Extinct religion for " + c.name, - color: getMixedColor(c.color, 0.1, 0), - removed: true - }); - return; - } - - const newId = c.i; - - if (pack.religions) { - const lockedFolkReligion = pack.religions.find( - r => r.culture === c.i && !r.removed && r.lock && r.type === "Folk" - ); - - if (lockedFolkReligion) { - for (const i of cells.i) { - if (cells.religion[i] === lockedFolkReligion.i) religionIds[i] = newId; - } - - lockedFolkReligion.i = newId; - religions.push(lockedFolkReligion); - return; - } - } - - const form = rw(forms.Folk); - const name = c.name + " " + rw(types[form]); - const deity = form === "Animism" ? null : getDeityName(c.i); - const color = getMixedColor(c.color, 0.1, 0); - religions.push({ - i: newId, - name, - color, - culture: newId, - type: "Folk", - form, - deity, - center: c.center, - origins: [0] - }); - }); - - if (religionsInput.value == 0 || pack.cultures.length < 2) { - religions.filter(r => r.i).forEach(r => (r.code = abbreviate(r.name))); - cells.religion = religionIds; - pack.religions = religions; - return; - } - - const burgs = pack.burgs.filter(b => b.i && !b.removed); - const sorted = - burgs.length > +religionsInput.value - ? burgs.sort((a, b) => b.population - a.population).map(b => b.cell) - : cells.i.filter(i => cells.s[i] > 2).sort((a, b) => cells.s[b] - cells.s[a]); - - const religionsTree = d3.quadtree(); - const spacing = (graphWidth + graphHeight) / 6 / religionsInput.value; // base min distance between towns - const cultsCount = Math.floor((rand(10, 40) / 100) * religionsInput.value); - const count = +religionsInput.value - cultsCount + religions.length; - - function getReligionsInRadius({x, y, r, max}) { - if (max === 0) return [0]; - const cellsInRadius = findAll(x, y, r); - const religions = unique(cellsInRadius.map(i => religionIds[i]).filter(r => r)); - return religions.length ? religions.slice(0, max) : [0]; - } - - // restore locked non-folk religions - if (pack.religions) { - const lockedNonFolkReligions = pack.religions.filter(r => r.lock && !r.removed && r.type !== "Folk"); - for (const religion of lockedNonFolkReligions) { - const newId = religions.length; - for (const i of cells.i) { - if (cells.religion[i] === religion.i) religionIds[i] = newId; - } - - religion.i = newId; - religion.origins = religion.origins.filter(origin => origin < newId); - religionsTree.add(cells.p[religion.center]); - religions.push(religion); - } - } - - // generate organized religions - for (let i = 0; religions.length < count && i < 1000; i++) { - let center = sorted[biased(0, sorted.length - 1, 5)]; // religion center - const form = rw(forms.Organized); - const state = cells.state[center]; - const culture = cells.culture[center]; - - const deity = form === "Non-theism" ? null : getDeityName(culture); - let [name, expansion] = getReligionName(form, deity, center); - if (expansion === "state" && !state) expansion = "global"; - if (expansion === "culture" && !culture) expansion = "global"; - - if (expansion === "state" && Math.random() > 0.5) center = states[state].center; - if (expansion === "culture" && Math.random() > 0.5) center = cultures[culture].center; - - if (!cells.burg[center] && cells.c[center].some(c => cells.burg[c])) - center = cells.c[center].find(c => cells.burg[c]); - const [x, y] = cells.p[center]; - - const s = spacing * gauss(1, 0.3, 0.2, 2, 2); // randomize to make the placement not uniform - if (religionsTree.find(x, y, s) !== undefined) continue; // to close to existing religion - - // add "Old" to name of the folk religion on this culture - const isFolkBased = expansion === "culture" || P(0.5); - const folk = isFolkBased && religions.find(r => r.culture === culture && r.type === "Folk"); - if (folk && expansion === "culture" && folk.name.slice(0, 3) !== "Old") folk.name = "Old " + folk.name; - - const origins = folk ? [folk.i] : getReligionsInRadius({x, y, r: 150 / count, max: 2}); - const expansionism = rand(3, 8); - const baseColor = religions[culture]?.color || states[state]?.color || getRandomColor(); - const color = getMixedColor(baseColor, 0.3, 0); - - religions.push({ - i: religions.length, - name, - color, - culture, - type: "Organized", - form, - deity, - expansion, - expansionism, - center, - origins - }); - religionsTree.add([x, y]); - } - - // generate cults - for (let i = 0; religions.length < count + cultsCount && i < 1000; i++) { - const form = rw(forms.Cult); - let center = sorted[biased(0, sorted.length - 1, 1)]; // religion center - if (!cells.burg[center] && cells.c[center].some(c => cells.burg[c])) - center = cells.c[center].find(c => cells.burg[c]); - const [x, y] = cells.p[center]; - - const s = spacing * gauss(2, 0.3, 1, 3, 2); // randomize to make the placement not uniform - if (religionsTree.find(x, y, s) !== undefined) continue; // to close to existing religion - - const culture = cells.culture[center]; - const origins = getReligionsInRadius({x, y, r: 300 / count, max: rand(0, 4)}); - - const deity = getDeityName(culture); - const name = getCultName(form, center); - const expansionism = gauss(1.1, 0.5, 0, 5); - const color = getMixedColor(cultures[culture].color, 0.5, 0); // "url(#hatch7)"; - religions.push({ - i: religions.length, - name, - color, - culture, - type: "Cult", - form, - deity, - expansion: "global", - expansionism, - center, - origins - }); - religionsTree.add([x, y]); - } - - expandReligions(); - - // generate heresies - religions - .filter(r => r.type === "Organized") - .forEach(r => { - if (r.expansionism < 3) return; - const count = gauss(0, 1, 0, 3); - for (let i = 0; i < count; i++) { - let center = ra(cells.i.filter(i => religionIds[i] === r.i && cells.c[i].some(c => religionIds[c] !== r.i))); - if (!center) continue; - if (!cells.burg[center] && cells.c[center].some(c => cells.burg[c])) - center = cells.c[center].find(c => cells.burg[c]); - const [x, y] = cells.p[center]; - if (religionsTree.find(x, y, spacing / 10) !== undefined) continue; // to close to other - - const culture = cells.culture[center]; - const name = getCultName("Heresy", center); - const expansionism = gauss(1.2, 0.5, 0, 5); - const color = getMixedColor(r.color, 0.4, 0.2); // "url(#hatch6)"; - religions.push({ - i: religions.length, - name, - color, - culture, - type: "Heresy", - form: r.form, - deity: r.deity, - expansion: "global", - expansionism, - center, - origins: [r.i] - }); - religionsTree.add([x, y]); - } - }); - - expandHeresies(); + const namedReligions = specifyReligions([...folkReligions, ...basicReligions]); + const indexedReligions = combineReligions(namedReligions, lockedReligions); + const religionIds = expandReligions(indexedReligions); + const religions = defineOrigins(religionIds, indexedReligions); + + pack.religions = religions; + pack.cells.religion = religionIds; checkCenters(); - cells.religion = religionIds; - pack.religions = religions; - TIME && console.timeEnd("generateReligions"); - - // growth algorithm to assign cells to religions - function expandReligions() { - const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); - const cost = []; - - religions - .filter(r => !r.lock && (r.type === "Organized" || r.type === "Cult")) - .forEach(r => { - religionIds[r.center] = r.i; - queue.queue({e: r.center, p: 0, r: r.i, s: cells.state[r.center], c: r.culture}); - cost[r.center] = 1; - }); - - const neutral = (cells.i.length / 5000) * 200 * gauss(1, 0.3, 0.2, 2, 2) * neutralInput.value; // limit cost for organized religions growth - const popCost = d3.max(cells.pop) / 3; // enougth population to spered religion without penalty - - while (queue.length) { - const {e, p, r, c, s} = queue.dequeue(); - const expansion = religions[r].expansion; - - cells.c[e].forEach(nextCell => { - if (expansion === "culture" && c !== cells.culture[nextCell]) return; - if (expansion === "state" && s !== cells.state[nextCell]) return; - if (religions[religionIds[nextCell]]?.lock) return; - - const cultureCost = c !== cells.culture[nextCell] ? 10 : 0; - const stateCost = s !== cells.state[nextCell] ? 10 : 0; - const biomeCost = cells.road[nextCell] ? 1 : biomesData.cost[cells.biome[nextCell]]; - const populationCost = Math.max(rn(popCost - cells.pop[nextCell]), 0); - const heightCost = Math.max(cells.h[nextCell], 20) - 20; - const waterCost = cells.h[nextCell] < 20 ? (cells.road[nextCell] ? 50 : 1000) : 0; - const totalCost = - p + - (cultureCost + stateCost + biomeCost + populationCost + heightCost + waterCost) / religions[r].expansionism; - if (totalCost > neutral) return; - - if (!cost[nextCell] || totalCost < cost[nextCell]) { - if (cells.h[nextCell] >= 20 && cells.culture[nextCell]) religionIds[nextCell] = r; // assign religion to cell - cost[nextCell] = totalCost; - queue.queue({e: nextCell, p: totalCost, r, c, s}); - } - }); - } - } - - // growth algorithm to assign cells to heresies - function expandHeresies() { - const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); - const cost = []; - - religions - .filter(r => !r.lock && r.type === "Heresy") - .forEach(r => { - const b = religionIds[r.center]; // "base" religion id - religionIds[r.center] = r.i; // heresy id - queue.queue({e: r.center, p: 0, r: r.i, b}); - cost[r.center] = 1; - }); - - const neutral = (cells.i.length / 5000) * 500 * neutralInput.value; // limit cost for heresies growth - - while (queue.length) { - const {e, p, r, b} = queue.dequeue(); - - cells.c[e].forEach(nextCell => { - if (religions[religionIds[nextCell]]?.lock) return; - const religionCost = religionIds[nextCell] === b ? 0 : 2000; - const biomeCost = cells.road[nextCell] ? 0 : biomesData.cost[cells.biome[nextCell]]; - const heightCost = Math.max(cells.h[nextCell], 20) - 20; - const waterCost = cells.h[nextCell] < 20 ? (cells.road[nextCell] ? 50 : 1000) : 0; - const totalCost = - p + (religionCost + biomeCost + heightCost + waterCost) / Math.max(religions[r].expansionism, 0.1); - - if (totalCost > neutral) return; - - if (!cost[nextCell] || totalCost < cost[nextCell]) { - if (cells.h[nextCell] >= 20 && cells.culture[nextCell]) religionIds[nextCell] = r; // assign religion to cell - cost[nextCell] = totalCost; - queue.queue({e: nextCell, p: totalCost, r}); - } - }); - } - } - - function checkCenters() { - const codes = religions.map(r => r.code); - religions.forEach(r => { - if (!r.i) return; - r.code = abbreviate(r.name, codes); - - // move religion center if it's not within religion area after expansion - if (religionIds[r.center] === r.i) return; // in area - const firstCell = cells.i.find(i => religionIds[i] === r.i); - if (firstCell) r.center = firstCell; // move center, othervise it's an extinct religion - }); - } }; - const add = function (center) { - const {cells, religions} = pack; - const religionId = cells.religion[center]; + function generateFolkReligions() { + return pack.cultures.filter(c => c.i && !c.removed).map(culture => { + const {i: culutreId, center} = culture; + const form = rw(forms.Folk); - const culture = cells.culture[center]; - const color = getMixedColor(religions[religionId].color, 0.3, 0); + return {type:"Folk", form, culture: culutreId, center}; + }); + } - const type = - religions[religionId].type === "Organized" ? rw({Organized: 4, Cult: 1, Heresy: 2}) : rw({Organized: 5, Cult: 2}); - const form = rw(forms[type]); - const deity = - type === "Heresy" ? religions[religionId].deity : form === "Non-theism" ? null : getDeityName(culture); + function generateOrganizedReligions(desiredReligionNumber, lockedReligions) { + const cells = pack.cells; + const lockedReligionCount = lockedReligions.filter(({type}) => type !== "Folk").length || 0; + const requiredReligionsNumber = desiredReligionNumber - lockedReligionCount; + if (requiredReligionsNumber < 1) return []; - let name, expansion; - if (type === "Organized") [name, expansion] = getReligionName(form, deity, center); - else { - name = getCultName(form, center); - expansion = "global"; + const candidateCells = getCandidateCells(); + const religionCores = placeReligions(); + + const cultsCount = Math.floor((rand(1, 4) / 10) * religionCores.length); // 10 - 40% + const heresiesCount = Math.floor((rand(0, 2) / 10) * religionCores.length); // 0 - 20%, was gauss(0,1, 0,3) per organized with expansionism >= 3 + const organizedCount = religionCores.length - cultsCount - heresiesCount; + + const getType = (index) => { + if (index < organizedCount) return "Organized"; + if (index < organizedCount + cultsCount) return "Cult"; + return "Heresy"; + }; + + return religionCores.map((cellId, index) => { + const type = getType(index); + const form = rw(forms[type]); + const cultureId = cells.culture[cellId]; + + return {type, form, culture: cultureId, center: cellId}; + }); + + function placeReligions() { + const religionCells = []; + const religionsTree = d3.quadtree(); + + // pre-populate with locked centers + lockedReligions.forEach(({center}) => religionsTree.add(cells.p[center])); + + // min distance between religion inceptions + const spacing = (graphWidth + graphHeight) / 2 / desiredReligionNumber; // was major gauss(1,0.3, 0.2,2, 2) / 6; cult gauss(2,0.3, 1,3, 2) /6; heresy /60 + + for (const cellId of candidateCells) { // was biased random major ^5, cult ^1 + const [x, y] = cells.p[cellId]; + + if (religionsTree.find(x, y, spacing) === undefined) { + religionCells.push(cellId); + religionsTree.add([x,y]); + + if (religionCells.length === requiredReligionsNumber) return religionCells; + } + } + + WARN && console.warn(`Placed only ${religionCells.length} of ${requiredReligionsNumber} religions`); + return religionCells; } + function getCandidateCells() { + const validBurgs = pack.burgs.filter(b => b.i && !b.removed); + + if (validBurgs.length >= requiredReligionsNumber) + return validBurgs.sort((a, b) => b.population - a.population).map(burg => burg.cell); + return cells.i.filter(i=> cells.s[i] > 2).sort((a, b) => cells.s[b] - cells.s[a]); + } + } + + function specifyReligions(newReligions) { + const {cells, cultures} = pack; + + const rawReligions = newReligions.map(({type, form, culture: cultureId, center}) => { + const supreme = getDeityName(cultureId); + const deity = form === "Non-theism" || form === "Animism" ? null : supreme; + + const stateId = cells.state[center]; + + let [name, expansion] = generateReligionName(type, form, supreme, center); + if (expansion === "state" && !stateId) expansion = "global"; + + const expansionism = expansionismMap[type](); + + const color = getReligionColor(cultures[cultureId], type); + + return {name, type, form, culture: cultureId, center, deity, expansion, expansionism, color}; + }); + + return rawReligions; + + function getReligionColor(culture, type) { + if (!culture.i) ERROR && console.error(`Culture ${culture.i} is not a valid culture`); + + if (type === "Folk") return culture.color; + if (type === "Heresy") return getMixedColor(culture.color, 0.35, 0.2); + if (type === "Cult") return getMixedColor(culture.color, 0.5, 0); + return getMixedColor(culture.color, 0.25, 0.4); + } + } + + // indexes, conditionally renames, and abbreviates religions + function combineReligions(namedReligions, lockedReligions) { + const noReligion = {i: 0, name: "No religion"}; + const indexedReligions = [noReligion]; + + const {lockedReligionQueue, highestLockedIndex, codes, numberLockedFolk} = parseLockedReligions(); + const maxIndex = Math.max(highestLockedIndex, namedReligions.length + lockedReligions.length + 1 - numberLockedFolk); + + for (let index = 1, progress = 0; index < maxIndex; index = indexedReligions.length) { + // place locked religion back at its old index + if (index === lockedReligionQueue[0]?.i) { + const nextReligion = lockedReligionQueue.shift(); + indexedReligions.push(nextReligion); + continue; + } + // slot the new religions + if (progress < namedReligions.length) { + const nextReligion = namedReligions[progress]; + progress++; + if (nextReligion.type === "Folk" && lockedReligions.some( + ({type, culture}) => type === "Folk" && culture === nextReligion.culture + )) continue; // when there is a locked Folk religion for this culture discard duplicate + + const newName = renameOld(nextReligion); + const code = abbreviate(newName, codes); + codes.push(code); + indexedReligions.push({...nextReligion, i: index, name: newName, code}); + continue; + } + indexedReligions.push({i: index, type: "Folk", culture: 0, name: "Padding", removed: true}); + } + return indexedReligions; + + function parseLockedReligions() { + // copy and sort the locked religions list + const lockedReligionQueue = lockedReligions.map(religion => { + // and filter their origins to locked religions + let newOrigin = religion.origins.filter(n => lockedReligions.some(({i: index}) => index === n)); + if (newOrigin === []) newOrigin = [0]; + return {...religion, origins: newOrigin}; + }).sort((a, b) => a.i - b.i); + + const highestLockedIndex = Math.max(...(lockedReligions.map(r => r.i))); + const codes = lockedReligions.length > 0 ? lockedReligions.map(r => r.code) : []; + const numberLockedFolk = lockedReligions.filter(({type}) => type === "Folk").length; + + return {lockedReligionQueue, highestLockedIndex, codes, numberLockedFolk}; + } + + // prepend 'Old' to names of folk religions which have organized competitors + function renameOld({name, type, culture: cultureId}) { + if (type !== "Folk") return name; + + const haveOrganized = namedReligions.some( + ({type, culture, expansion}) => culture === cultureId && type === "Organized" && expansion === "culture") + || lockedReligions.some(({type, culture, expansion}) => culture === cultureId && type === "Organized" && expansion === "culture"); + if (haveOrganized && name.slice(0, 3) !== "Old") return `Old ${name}`; + return name; + } + } + + // finally generate and stores origins trees + function defineOrigins(religionIds, indexedReligions) { + const religionOriginsParamsMap = { + Organized: {clusterSize: 100, maxReligions: 2}, // was 150/count, 2 + Cult: {clusterSize: 50, maxReligions: 3}, // was 300/count, rand(0,4) + Heresy: {clusterSize: 50, maxReligions: 4} + }; + + const origins = indexedReligions.map((religion, index) => { + if (religion.type === "Folk") return [0]; + if (index === 0) return null; + + const {i, type, culture: cultureId, expansion, center} = religion; + + const folkReligion = indexedReligions.find(({culture, type}) => type === "Folk" && culture === cultureId); + const isFolkBased = folkReligion && cultureId && expansion === "culture" && each(2)(center); // P(0.5) -> isEven cellId + + if (isFolkBased) return [folkReligion.i]; + + const {clusterSize, maxReligions} = religionOriginsParamsMap[type]; + const origins = getReligionsInRadius(pack.cells.c, center, religionIds, i, clusterSize, maxReligions); + + if (origins === [0]) return [folkReligion.i]; // hegemony has local roots + return origins; + }); + + return indexedReligions.map((religion, index) => ({ + ...religion, + origins: origins[index] + })); + } + + function getReligionsInRadius(neighbors, center, religionIds, religionId, clusterSize, maxReligions) { + const foundReligions = new Set(); + const queue = [center]; + const checked = {}; + + for (let size = 0; queue.length && size < clusterSize; size++) { + const cellId = queue.shift(); + checked[cellId] = true; + + for (const neibId of neighbors[cellId]) { + if (checked[neibId]) continue; + checked[neibId] = true; + + const neibReligion = religionIds[neibId]; + if (neibReligion && neibReligion !== religionId) foundReligions.add(neibReligion); + queue.push(neibId); + } + } + + return foundReligions.size ? [...foundReligions].slice(0, maxReligions) : [0]; + } + + // growth algorithm to assign cells to religions + function expandReligions(religions) { + const cells = pack.cells; + const religionIds = spreadFolkReligions(religions); + + const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); + const cost = []; + + const maxExpansionCost = (cells.i.length / 20) * neutralInput.value; // limit cost for organized religions growth (was /25, heresy /10) + + const biomePassageCost = (cellId) => biomesData.cost[cells.biome[cellId]]; + + religions + .filter(r => r.i && !r.lock && r.type !== "Folk" && !r.removed) + .forEach(r => { + religionIds[r.center] = r.i; + queue.queue({e: r.center, p: 0, r: r.i, s: cells.state[r.center]}); + cost[r.center] = 1; + }); + + const religionsMap = new Map(religions.map(r => [r.i, r])); + + const isMainRoad = (cellId) => (cells.road[cellId] - cells.crossroad[cellId]) > 4; + const isTrail = (cellId) => cells.h[cellId] > 19 && (cells.road[cellId] - cells.crossroad[cellId]) === 1; + const isSeaRoute = (cellId) => cells.h[cellId] < 20 && cells.road[cellId]; + const isWater = (cellId) => cells.h[cellId] < 20; + // const popCost = d3.max(cells.pop) / 3; // enougth population to spered religion without penalty + + while (queue.length) { + const {e: cellId, p, r, s: state} = queue.dequeue(); + const {culture, expansion, expansionism} = religionsMap.get(r); + + cells.c[cellId].forEach(nextCell => { + if (expansion === "culture" && culture !== cells.culture[nextCell]) return; + if (expansion === "state" && state !== cells.state[nextCell]) return; + if (religionsMap.get(religionIds[nextCell])?.lock) return; + + const cultureCost = culture !== cells.culture[nextCell] ? 10 : 0; + const stateCost = state !== cells.state[nextCell] ? 10 : 0; + const passageCost = getPassageCost(nextCell); + // const populationCost = Math.max(rn(popCost - cells.pop[nextCell]), 0); + // const heightCost = Math.max(cells.h[nextCell], 20) - 20; + + const cellCost = cultureCost + stateCost + passageCost; + const totalCost = p + 10 + cellCost / expansionism; + if (totalCost > maxExpansionCost) return; + + if (!cost[nextCell] || totalCost < cost[nextCell]) { + if (cells.culture[nextCell]) religionIds[nextCell] = r; // assign religion to cell + cost[nextCell] = totalCost; + + queue.queue({e: nextCell, p: totalCost, r, s: state}); + } + }); + } + + return religionIds; + + function getPassageCost(cellId) { + if (isWater(cellId)) return isSeaRoute ? 50 : 500; // was 50 : 1000 + if (isMainRoad(cellId)) return 1; + const biomeCost = biomePassageCost(cellId); + return (isTrail(cellId)) ? biomeCost / 1.5 : biomeCost; // was same as main road + } + } + + // folk religions initially get all cells of their culture, and locked religions are retained + function spreadFolkReligions(religions) { + const cells = pack.cells; + const hasPrior = cells.religion && true; + const religionIds = new Uint16Array(cells.i.length); + + const folkReligions = religions.filter(religion => religion.type === "Folk" && !religion.removed); + const cultureToReligionMap = new Map(folkReligions.map(({i, culture}) => [culture, i])); + + for (const cellId of cells.i) { + const oldId = (hasPrior && cells.religion[cellId]) || 0; + if (oldId && religions[oldId]?.lock && !religions[oldId]?.removed) { + religionIds[cellId] = oldId; + continue; + } + const cultureId = cells.culture[cellId]; + religionIds[cellId] = cultureToReligionMap.get(cultureId) || 0; + } + + return religionIds; + } + + function checkCenters() { + const cells = pack.cells; + pack.religions.forEach(r => { + if (!r.i) return; + // move religion center if it's not within religion area after expansion + if (cells.religion[r.center] === r.i) return; // in area + const firstCell = cells.i.find(i => cells.religion[i] === r.i); + const cultureHome = pack.cultures[r.culture]?.center; + if (firstCell) r.center = firstCell; // move center, othervise it's an extinct religion + else if (r.type === "Folk" && cultureHome) r.center = cultureHome; // reset extinct culture centers + }); + } + + function recalculate() { + const newReligionIds = expandReligions(pack.religions); + pack.cells.religion = newReligionIds; + + checkCenters(); + } + + const add = function (center) { + const {cells, cultures, religions} = pack; + const religionId = cells.religion[center]; + + const cultureId = cells.culture[center]; + const missingFolk = cultureId !== 0 && !religions.some(({type, culture, removed}) => type === "Folk" && culture === cultureId && !removed); + const color = missingFolk ? cultures[cultureId].color + : getMixedColor(religions[religionId].color, 0.3, 0); + + const type = + missingFolk ? "Folk" : + religions[religionId].type === "Organized" ? rw({Organized: 4, Cult: 1, Heresy: 2}) + : rw({Organized: 5, Cult: 2}); + const form = rw(forms[type]); + const deity = + type === "Heresy" ? religions[religionId].deity : + (form === "Non-theism" || form === "Animism") ? null + : getDeityName(cultureId); + + const [name, expansion] = generateReligionName(type, form, deity, center); + const formName = type === "Heresy" ? religions[religionId].form : form; const code = abbreviate( name, religions.map(r => r.code) ); + const influences = getReligionsInRadius(cells.c, center, cells.religion, 0, 25, 3); + const origins = type === "Folk" ? [0] : influences; const i = religions.length; religions.push({ i, name, color, - culture, + culture: cultureId, type, form: formName, deity, expansion, - expansionism: 0, + expansionism: expansionismMap[type](), center, cells: 0, area: 0, rural: 0, urban: 0, - origins: [religionId], + origins, code }); cells.religion[center] = i; @@ -736,22 +788,24 @@ window.Religions = (function () { if (a === "Number") return ra(base.number); if (a === "Being") return ra(base.being); if (a === "Adjective") return ra(base.adjective); - if (a === "Color + Animal") return ra(base.color) + " " + ra(base.animal); - if (a === "Adjective + Animal") return ra(base.adjective) + " " + ra(base.animal); - if (a === "Adjective + Being") return ra(base.adjective) + " " + ra(base.being); - if (a === "Adjective + Genitive") return ra(base.adjective) + " " + ra(base.genitive); - if (a === "Color + Being") return ra(base.color) + " " + ra(base.being); - if (a === "Color + Genitive") return ra(base.color) + " " + ra(base.genitive); - if (a === "Being + of + Genitive") return ra(base.being) + " of " + ra(base.genitive); - if (a === "Being + of the + Genitive") return ra(base.being) + " of the " + ra(base.theGenitive); - if (a === "Animal + of + Genitive") return ra(base.animal) + " of " + ra(base.genitive); + if (a === "Color + Animal") return `${ra(base.color)} ${ra(base.animal)}`; + if (a === "Adjective + Animal") return `${ra(base.adjective)} ${ra(base.animal)}`; + if (a === "Adjective + Being") return `${ra(base.adjective)} ${ra(base.being)}`; + if (a === "Adjective + Genitive") return `${ra(base.adjective)} ${ra(base.genitive)}`; + if (a === "Color + Being") return `${ra(base.color)} ${ra(base.being)}`; + if (a === "Color + Genitive") return `${ra(base.color)} ${ra(base.genitive)}`; + if (a === "Being + of + Genitive") return `${ra(base.being)} of ${ra(base.genitive)}`; + if (a === "Being + of the + Genitive") return `${ra(base.being)} of the ${ra(base.theGenitive)}`; + if (a === "Animal + of + Genitive") return `${ra(base.animal)} of ${ra(base.genitive)}`; if (a === "Adjective + Being + of + Genitive") - return ra(base.adjective) + " " + ra(base.being) + " of " + ra(base.genitive); + return `${ra(base.adjective)} ${ra(base.being)} of ${ra(base.genitive)}`; if (a === "Adjective + Animal + of + Genitive") - return ra(base.adjective) + " " + ra(base.animal) + " of " + ra(base.genitive); + return `${ra(base.adjective)} ${ra(base.animal)} of ${ra(base.genitive)}`; + + ERROR && console.error("Unkown generation approach"); } - function getReligionName(form, deity, center) { + function generateReligionName(variety, form, deity, center) { const {cells, cultures, burgs, states} = pack; const random = () => Names.getCulture(cells.culture[center], null, null, "", 0); @@ -767,7 +821,7 @@ window.Religions = (function () { return adj ? getAdjective(name) : name; }; - const m = rw(methods); + const m = rw(namingMethods[variety]); if (m === "Random + type") return [random() + " " + type(), "global"]; if (m === "Random + ism") return [trimVowels(random()) + "ism", "global"]; if (m === "Supreme + ism" && deity) return [trimVowels(supreme()) + "ism", "global"]; @@ -777,24 +831,11 @@ window.Religions = (function () { if (m === "Culture + ism") return [trimVowels(culture()) + "ism", "culture"]; if (m === "Place + ian + type") return [place("adj") + " " + type(), "state"]; if (m === "Culture + type") return [culture() + " " + type(), "culture"]; + if (m === "Burg + ian + type") return [`${place("adj")} ${type()}`, "global"]; + if (m === "Random + ian + type") return [`${getAdjective(random())} ${type()}`, "global"]; + if (m === "Type + of the + meaning") return [`${type()} of the ${generateMeaning()}`, "global"]; return [trimVowels(random()) + "ism", "global"]; // else } - function getCultName(form, center) { - const cells = pack.cells; - const type = function () { - return rw(types[form]); - }; - const random = function () { - return trimVowels(Names.getCulture(cells.culture[center], null, null, "", 0).split(/[ ,]+/)[0]); - }; - const burg = function () { - return trimVowels(pack.burgs[cells.burg[center]].name.split(/[ ,]+/)[0]); - }; - if (cells.burg[center]) return burg() + "ian " + type(); - if (Math.random() > 0.5) return random() + "ian " + type(); - return type() + " of the " + generateMeaning(); - } - - return {generate, add, getDeityName, updateCultures}; + return {generate, add, getDeityName, updateCultures, recalculate}; })(); diff --git a/modules/ui/editors.js b/modules/ui/editors.js index 9b167f57..49a525eb 100644 --- a/modules/ui/editors.js +++ b/modules/ui/editors.js @@ -1188,6 +1188,6 @@ async function editCultures() { async function editReligions() { if (customization) return; - const Editor = await import("../dynamic/editors/religions-editor.js?v=1.88.07"); + const Editor = await import("../dynamic/editors/religions-editor.js?v=1.89.10"); Editor.open(); } diff --git a/versioning.js b/versioning.js index 70a25bdb..9e10745c 100644 --- a/versioning.js +++ b/versioning.js @@ -1,7 +1,7 @@ "use strict"; // version and caching control -const version = "1.89.11"; // generator version, update each time +const version = "1.89.12"; // generator version, update each time { document.title += " v" + version; @@ -28,6 +28,7 @@ const version = "1.89.11"; // generator version, update each time
    Latest changes: +
  • Religions can be edited and redrawn like cultures
  • Lock states, provinces, cultures, and religions from regeneration
  • Heightmap brushes: linear edit option
  • Data Charts screen
  • From 13e247ed9c25b7171e3e6d130e92c0ce63964ad3 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Sun, 19 Mar 2023 14:09:38 +0400 Subject: [PATCH 18/28] feat(religions editor): sync religion center circle style with culture one --- main.js | 3 +-- modules/dynamic/editors/religions-editor.js | 20 +++++++++----------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/main.js b/main.js index 49b0f001..a7756124 100644 --- a/main.js +++ b/main.js @@ -684,7 +684,6 @@ async function generate(options) { const timeStart = performance.now(); const {seed: precreatedSeed, graph: precreatedGraph} = options || {}; - pack = {}; invokeActiveZooming(); setSeed(precreatedSeed); INFO && console.group("Generated Map " + seed); @@ -695,7 +694,7 @@ async function generate(options) { if (shouldRegenerateGrid(grid, precreatedSeed)) grid = precreatedGraph || generateGrid(); else delete grid.cells.h; grid.cells.h = await HeightmapGenerator.generate(grid); - pack = {}; + pack = {}; // reset pack markFeatures(); markupGridOcean(); diff --git a/modules/dynamic/editors/religions-editor.js b/modules/dynamic/editors/religions-editor.js index d6c60dfb..94abd84e 100644 --- a/modules/dynamic/editors/religions-editor.js +++ b/modules/dynamic/editors/religions-editor.js @@ -286,7 +286,7 @@ function getExpansionColumns(r) { culture - ` + `; else return ` -
    ${si(area) + unit}
    +
    ${si(area) + unit}
    -
    ${si(population)}
    - - - +
    ${si( + population + )}
    `; continue; } @@ -222,14 +221,13 @@ function religionsEditorAddLines() { -
    ${si(area) + unit}
    +
    ${si(area) + unit}
    -
    ${si(population)}
    +
    ${si( + population + )}
    ${getExpansionColumns(r)} - + `; } @@ -260,7 +258,7 @@ function religionsEditorAddLines() { $body.querySelectorAll("div > span.icon-arrows-cw").forEach(el => el.on("click", regenerateDeity)); $body.querySelectorAll("div > div.religionPopulation").forEach(el => el.on("click", changePopulation)); $body.querySelectorAll("div > select.religionExtent").forEach(el => el.on("change", religionChangeExtent)); - $body.querySelectorAll("div > input.religionExpan").forEach(el => el.on("change", religionChangeExpansionism)); + $body.querySelectorAll("div > input.religionExpantion").forEach(el => el.on("change", religionChangeExpansionism)); $body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.on("click", religionRemovePrompt)); $body.querySelectorAll("div > span.icon-lock").forEach($el => $el.on("click", updateLockStatus)); $body.querySelectorAll("div > span.icon-lock-open").forEach($el => $el.on("click", updateLockStatus)); @@ -282,25 +280,27 @@ function getTypeOptions(type) { function getExpansionColumns(r) { if (r.type === "Folk") - return ` - culture - - - `; - else - return ` - - `; + return /* html */ ` + + culture + + `; + + return /* html */ ` + + + + `; } function getExtentOptions(type) { From 218887b435cf48838ed6d7b02c50f778082cdaaf Mon Sep 17 00:00:00 2001 From: Azgaar Date: Sun, 19 Mar 2023 15:46:53 +0400 Subject: [PATCH 20/28] feat(religions editor): debouce center dragging --- modules/dynamic/editors/cultures-editor.js | 15 ++++++++++---- modules/dynamic/editors/religions-editor.js | 22 ++++++++++++++++----- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/modules/dynamic/editors/cultures-editor.js b/modules/dynamic/editors/cultures-editor.js index 15fe1f32..5e66941d 100644 --- a/modules/dynamic/editors/cultures-editor.js +++ b/modules/dynamic/editors/cultures-editor.js @@ -590,16 +590,23 @@ function drawCultureCenters() { } function cultureCenterDrag() { - const $el = d3.select(this); const cultureId = +this.id.slice(13); - d3.event.on("drag", () => { + const tr = parseTransform(this.getAttribute("transform")); + const x0 = +tr[0] - d3.event.x; + const y0 = +tr[1] - d3.event.y; + + function handleDrag() { const {x, y} = d3.event; - $el.attr("cx", x).attr("cy", y); + this.setAttribute("transform", `translate(${x0 + x},${y0 + y})`); const cell = findCell(x, y); if (pack.cells.h[cell] < 20) return; // ignore dragging on water + pack.cultures[cultureId].center = cell; recalculateCultures(); - }); + } + + const dragDebounced = debounce(handleDrag, 50); + d3.event.on("drag", dragDebounced); } function toggleLegend() { diff --git a/modules/dynamic/editors/religions-editor.js b/modules/dynamic/editors/religions-editor.js index 46e50f24..a441c793 100644 --- a/modules/dynamic/editors/religions-editor.js +++ b/modules/dynamic/editors/religions-editor.js @@ -267,6 +267,7 @@ function religionsEditorAddLines() { $body.dataset.type = "absolute"; togglePercentageMode(); } + applySorting(religionsHeader); $("#religionsEditor").dialog({width: fitContent()}); } @@ -533,7 +534,10 @@ function drawReligionCenters() { .attr("stroke", "#444444") .style("cursor", "move"); - const data = pack.religions.filter(r => r.i && r.center && !r.removed); + let data = pack.religions.filter(r => r.i && r.center && !r.removed); + const showExtinct = $body.dataset.extinct === "show"; + if (!showExtinct) data = data.filter(r => r.cells > 0); + religionCenters .selectAll("circle") .data(data) @@ -557,16 +561,23 @@ function drawReligionCenters() { } function religionCenterDrag() { - const $el = d3.select(this); const religionId = +this.dataset.id; - d3.event.on("drag", () => { + const tr = parseTransform(this.getAttribute("transform")); + const x0 = +tr[0] - d3.event.x; + const y0 = +tr[1] - d3.event.y; + + function handleDrag() { const {x, y} = d3.event; - $el.attr("cx", x).attr("cy", y); + this.setAttribute("transform", `translate(${x0 + x},${y0 + y})`); const cell = findCell(x, y); if (pack.cells.h[cell] < 20) return; // ignore dragging on water + pack.religions[religionId].center = cell; recalculateReligions(); - }); + } + + const dragDebounced = debounce(handleDrag, 50); + d3.event.on("drag", dragDebounced); } function toggleLegend() { @@ -637,6 +648,7 @@ async function showHierarchy() { function toggleExtinct() { $body.dataset.extinct = $body.dataset.extinct !== "show" ? "show" : "hide"; religionsEditorAddLines(); + drawReligionCenters(); } function enterReligionsManualAssignent() { From 12fad8fd8fe034cd4a52a46a9ca87aa390f9a8db Mon Sep 17 00:00:00 2001 From: Azgaar Date: Tue, 21 Mar 2023 22:41:50 +0400 Subject: [PATCH 21/28] feat(religions): editor UX update and cleanup, increase religions number to generate --- modules/dynamic/editors/religions-editor.js | 17 +- modules/religions-generator.js | 163 ++++++++++---------- modules/ui/options.js | 4 +- 3 files changed, 96 insertions(+), 88 deletions(-) diff --git a/modules/dynamic/editors/religions-editor.js b/modules/dynamic/editors/religions-editor.js index a441c793..b6b2b5a0 100644 --- a/modules/dynamic/editors/religions-editor.js +++ b/modules/dynamic/editors/religions-editor.js @@ -30,7 +30,7 @@ function insertEditorHtml() {
    Supreme Deity 
    Area 
    Believers 
    -
    Potential 
    +
    Potential 
    Expansion 
    @@ -280,15 +280,18 @@ function getTypeOptions(type) { } function getExpansionColumns(r) { - if (r.type === "Folk") + if (r.type === "Folk") { + const tip = + "Folk religions are not competitive and do not expand. Initially they cover all cells of their parent culture, but get ousted by organized religions when they expand"; return /* html */ ` - - culture - - `; + + culture + + `; + } return /* html */ ` - + diff --git a/modules/religions-generator.js b/modules/religions-generator.js index bd3eb9a4..c3140324 100644 --- a/modules/religions-generator.js +++ b/modules/religions-generator.js @@ -363,40 +363,35 @@ window.Religions = (function () { const expansionismMap = { Folk: () => 0, - Organized: () => gauss(5, 3, 0, 10, 1), // was rand(3, 8) - Cult: () => gauss(0.5, 0.5, 0, 5, 1), // was gauss(1.1, 0.5, 0, 5) - Heresy: () => gauss(1, 0.5, 0, 5, 1) // was gauss(1.2, 0.5, 0, 5) + Organized: () => gauss(5, 3, 0, 10, 1), + Cult: () => gauss(0.5, 0.5, 0, 5, 1), + Heresy: () => gauss(1, 0.5, 0, 5, 1) }; function generate() { TIME && console.time("generateReligions"); - // const {cells, states, cultures, burgs} = pack; - - const lockedReligions = pack.religions?.filter(religion => religion.lock && !religion.removed) || []; + const lockedReligions = pack.religions?.filter(religion => r.i && religion.lock && !religion.removed) || []; const folkReligions = generateFolkReligions(); - const basicReligions = generateOrganizedReligions(+religionsInput.value, lockedReligions); + const organizedReligions = generateOrganizedReligions(+religionsInput.value, lockedReligions); - const namedReligions = specifyReligions([...folkReligions, ...basicReligions]); + const namedReligions = specifyReligions([...folkReligions, ...organizedReligions]); const indexedReligions = combineReligions(namedReligions, lockedReligions); const religionIds = expandReligions(indexedReligions); const religions = defineOrigins(religionIds, indexedReligions); - + pack.religions = religions; pack.cells.religion = religionIds; checkCenters(); TIME && console.timeEnd("generateReligions"); - }; + } function generateFolkReligions() { - return pack.cultures.filter(c => c.i && !c.removed).map(culture => { - const {i: culutreId, center} = culture; - const form = rw(forms.Folk); - - return {type:"Folk", form, culture: culutreId, center}; - }); + return pack.cultures + .filter(c => c.i && !c.removed) + .map(culture => ({type: "Folk", form: rw(forms.Folk), culture: culture.i, center: culture.center})); } function generateOrganizedReligions(desiredReligionNumber, lockedReligions) { @@ -408,11 +403,11 @@ window.Religions = (function () { const candidateCells = getCandidateCells(); const religionCores = placeReligions(); - const cultsCount = Math.floor((rand(1, 4) / 10) * religionCores.length); // 10 - 40% - const heresiesCount = Math.floor((rand(0, 2) / 10) * religionCores.length); // 0 - 20%, was gauss(0,1, 0,3) per organized with expansionism >= 3 + const cultsCount = Math.floor((rand(1, 4) / 10) * religionCores.length); // 10-40% + const heresiesCount = Math.floor((rand(0, 3) / 10) * religionCores.length); // 0-30% const organizedCount = religionCores.length - cultsCount - heresiesCount; - const getType = (index) => { + const getType = index => { if (index < organizedCount) return "Organized"; if (index < organizedCount + cultsCount) return "Cult"; return "Heresy"; @@ -434,14 +429,14 @@ window.Religions = (function () { lockedReligions.forEach(({center}) => religionsTree.add(cells.p[center])); // min distance between religion inceptions - const spacing = (graphWidth + graphHeight) / 2 / desiredReligionNumber; // was major gauss(1,0.3, 0.2,2, 2) / 6; cult gauss(2,0.3, 1,3, 2) /6; heresy /60 + const spacing = (graphWidth + graphHeight) / 2 / desiredReligionNumber; - for (const cellId of candidateCells) { // was biased random major ^5, cult ^1 + for (const cellId of candidateCells) { const [x, y] = cells.p[cellId]; if (religionsTree.find(x, y, spacing) === undefined) { religionCells.push(cellId); - religionsTree.add([x,y]); + religionsTree.add([x, y]); if (religionCells.length === requiredReligionsNumber) return religionCells; } @@ -456,7 +451,7 @@ window.Religions = (function () { if (validBurgs.length >= requiredReligionsNumber) return validBurgs.sort((a, b) => b.population - a.population).map(burg => burg.cell); - return cells.i.filter(i=> cells.s[i] > 2).sort((a, b) => cells.s[b] - cells.s[a]); + return cells.i.filter(i => cells.s[i] > 2).sort((a, b) => cells.s[b] - cells.s[a]); } } @@ -493,11 +488,13 @@ window.Religions = (function () { // indexes, conditionally renames, and abbreviates religions function combineReligions(namedReligions, lockedReligions) { - const noReligion = {i: 0, name: "No religion"}; - const indexedReligions = [noReligion]; + const indexedReligions = [{name: "No religion", i: 0}]; const {lockedReligionQueue, highestLockedIndex, codes, numberLockedFolk} = parseLockedReligions(); - const maxIndex = Math.max(highestLockedIndex, namedReligions.length + lockedReligions.length + 1 - numberLockedFolk); + const maxIndex = Math.max( + highestLockedIndex, + namedReligions.length + lockedReligions.length + 1 - numberLockedFolk + ); for (let index = 1, progress = 0; index < maxIndex; index = indexedReligions.length) { // place locked religion back at its old index @@ -506,13 +503,17 @@ window.Religions = (function () { indexedReligions.push(nextReligion); continue; } + // slot the new religions if (progress < namedReligions.length) { const nextReligion = namedReligions[progress]; progress++; - if (nextReligion.type === "Folk" && lockedReligions.some( - ({type, culture}) => type === "Folk" && culture === nextReligion.culture - )) continue; // when there is a locked Folk religion for this culture discard duplicate + + if ( + nextReligion.type === "Folk" && + lockedReligions.some(({type, culture}) => type === "Folk" && culture === nextReligion.culture) + ) + continue; // when there is a locked Folk religion for this culture discard duplicate const newName = renameOld(nextReligion); const code = abbreviate(newName, codes); @@ -520,20 +521,23 @@ window.Religions = (function () { indexedReligions.push({...nextReligion, i: index, name: newName, code}); continue; } - indexedReligions.push({i: index, type: "Folk", culture: 0, name: "Padding", removed: true}); + + indexedReligions.push({i: index, type: "Folk", culture: 0, name: "Removed religion", removed: true}); } return indexedReligions; function parseLockedReligions() { // copy and sort the locked religions list - const lockedReligionQueue = lockedReligions.map(religion => { - // and filter their origins to locked religions - let newOrigin = religion.origins.filter(n => lockedReligions.some(({i: index}) => index === n)); - if (newOrigin === []) newOrigin = [0]; - return {...religion, origins: newOrigin}; - }).sort((a, b) => a.i - b.i); + const lockedReligionQueue = lockedReligions + .map(religion => { + // and filter their origins to locked religions + let newOrigin = religion.origins.filter(n => lockedReligions.some(({i: index}) => index === n)); + if (newOrigin === []) newOrigin = [0]; + return {...religion, origins: newOrigin}; + }) + .sort((a, b) => a.i - b.i); - const highestLockedIndex = Math.max(...(lockedReligions.map(r => r.i))); + const highestLockedIndex = Math.max(...lockedReligions.map(r => r.i)); const codes = lockedReligions.length > 0 ? lockedReligions.map(r => r.code) : []; const numberLockedFolk = lockedReligions.filter(({type}) => type === "Folk").length; @@ -543,10 +547,14 @@ window.Religions = (function () { // prepend 'Old' to names of folk religions which have organized competitors function renameOld({name, type, culture: cultureId}) { if (type !== "Folk") return name; - - const haveOrganized = namedReligions.some( - ({type, culture, expansion}) => culture === cultureId && type === "Organized" && expansion === "culture") - || lockedReligions.some(({type, culture, expansion}) => culture === cultureId && type === "Organized" && expansion === "culture"); + + const haveOrganized = + namedReligions.some( + ({type, culture, expansion}) => culture === cultureId && type === "Organized" && expansion === "culture" + ) || + lockedReligions.some( + ({type, culture, expansion}) => culture === cultureId && type === "Organized" && expansion === "culture" + ); if (haveOrganized && name.slice(0, 3) !== "Old") return `Old ${name}`; return name; } @@ -555,36 +563,28 @@ window.Religions = (function () { // finally generate and stores origins trees function defineOrigins(religionIds, indexedReligions) { const religionOriginsParamsMap = { - Organized: {clusterSize: 100, maxReligions: 2}, // was 150/count, 2 - Cult: {clusterSize: 50, maxReligions: 3}, // was 300/count, rand(0,4) + Organized: {clusterSize: 100, maxReligions: 2}, + Cult: {clusterSize: 50, maxReligions: 3}, Heresy: {clusterSize: 50, maxReligions: 4} }; - const origins = indexedReligions.map((religion, index) => { - if (religion.type === "Folk") return [0]; - if (index === 0) return null; - - const {i, type, culture: cultureId, expansion, center} = religion; + const origins = indexedReligions.map(({i, type, culture: cultureId, expansion, center}) => { + if (i === 0) return null; // no religion + if (type === "Folk") return [0]; // folk religions originate from its parent culture only const folkReligion = indexedReligions.find(({culture, type}) => type === "Folk" && culture === cultureId); - const isFolkBased = folkReligion && cultureId && expansion === "culture" && each(2)(center); // P(0.5) -> isEven cellId - + const isFolkBased = folkReligion && cultureId && expansion === "culture" && each(2)(center); if (isFolkBased) return [folkReligion.i]; const {clusterSize, maxReligions} = religionOriginsParamsMap[type]; - const origins = getReligionsInRadius(pack.cells.c, center, religionIds, i, clusterSize, maxReligions); - - if (origins === [0]) return [folkReligion.i]; // hegemony has local roots - return origins; + const fallbackOrigin = folkReligion?.i || 0; + return getReligionsInRadius(pack.cells.c, center, religionIds, i, clusterSize, maxReligions, fallbackOrigin); }); - return indexedReligions.map((religion, index) => ({ - ...religion, - origins: origins[index] - })); + return indexedReligions.map((religion, index) => ({...religion, origins: origins[index]})); } - function getReligionsInRadius(neighbors, center, religionIds, religionId, clusterSize, maxReligions) { + function getReligionsInRadius(neighbors, center, religionIds, religionId, clusterSize, maxReligions, fallbackOrigin) { const foundReligions = new Set(); const queue = [center]; const checked = {}; @@ -599,11 +599,12 @@ window.Religions = (function () { const neibReligion = religionIds[neibId]; if (neibReligion && neibReligion !== religionId) foundReligions.add(neibReligion); + if (foundReligions.size >= maxReligions) return [...foundReligions]; queue.push(neibId); } } - return foundReligions.size ? [...foundReligions].slice(0, maxReligions) : [0]; + return foundReligions.size ? [...foundReligions] : [fallbackOrigin]; } // growth algorithm to assign cells to religions @@ -616,7 +617,7 @@ window.Religions = (function () { const maxExpansionCost = (cells.i.length / 20) * neutralInput.value; // limit cost for organized religions growth (was /25, heresy /10) - const biomePassageCost = (cellId) => biomesData.cost[cells.biome[cellId]]; + const biomePassageCost = cellId => biomesData.cost[cells.biome[cellId]]; religions .filter(r => r.i && !r.lock && r.type !== "Folk" && !r.removed) @@ -625,13 +626,13 @@ window.Religions = (function () { queue.queue({e: r.center, p: 0, r: r.i, s: cells.state[r.center]}); cost[r.center] = 1; }); - + const religionsMap = new Map(religions.map(r => [r.i, r])); - const isMainRoad = (cellId) => (cells.road[cellId] - cells.crossroad[cellId]) > 4; - const isTrail = (cellId) => cells.h[cellId] > 19 && (cells.road[cellId] - cells.crossroad[cellId]) === 1; - const isSeaRoute = (cellId) => cells.h[cellId] < 20 && cells.road[cellId]; - const isWater = (cellId) => cells.h[cellId] < 20; + const isMainRoad = cellId => cells.road[cellId] - cells.crossroad[cellId] > 4; + const isTrail = cellId => cells.h[cellId] > 19 && cells.road[cellId] - cells.crossroad[cellId] === 1; + const isSeaRoute = cellId => cells.h[cellId] < 20 && cells.road[cellId]; + const isWater = cellId => cells.h[cellId] < 20; // const popCost = d3.max(cells.pop) / 3; // enougth population to spered religion without penalty while (queue.length) { @@ -668,7 +669,7 @@ window.Religions = (function () { if (isWater(cellId)) return isSeaRoute ? 50 : 500; // was 50 : 1000 if (isMainRoad(cellId)) return 1; const biomeCost = biomePassageCost(cellId); - return (isTrail(cellId)) ? biomeCost / 1.5 : biomeCost; // was same as main road + return isTrail(cellId) ? biomeCost / 1.5 : biomeCost; // was same as main road } } @@ -713,34 +714,38 @@ window.Religions = (function () { checkCenters(); } - + const add = function (center) { const {cells, cultures, religions} = pack; const religionId = cells.religion[center]; const cultureId = cells.culture[center]; - const missingFolk = cultureId !== 0 && !religions.some(({type, culture, removed}) => type === "Folk" && culture === cultureId && !removed); - const color = missingFolk ? cultures[cultureId].color - : getMixedColor(religions[religionId].color, 0.3, 0); + const missingFolk = + cultureId !== 0 && + !religions.some(({type, culture, removed}) => type === "Folk" && culture === cultureId && !removed); + const color = missingFolk ? cultures[cultureId].color : getMixedColor(religions[religionId].color, 0.3, 0); - const type = - missingFolk ? "Folk" : - religions[religionId].type === "Organized" ? rw({Organized: 4, Cult: 1, Heresy: 2}) + const type = missingFolk + ? "Folk" + : religions[religionId].type === "Organized" + ? rw({Organized: 4, Cult: 1, Heresy: 2}) : rw({Organized: 5, Cult: 2}); const form = rw(forms[type]); const deity = - type === "Heresy" ? religions[religionId].deity : - (form === "Non-theism" || form === "Animism") ? null - : getDeityName(cultureId); + type === "Heresy" + ? religions[religionId].deity + : form === "Non-theism" || form === "Animism" + ? null + : getDeityName(cultureId); const [name, expansion] = generateReligionName(type, form, deity, center); - + const formName = type === "Heresy" ? religions[religionId].form : form; const code = abbreviate( name, religions.map(r => r.code) ); - const influences = getReligionsInRadius(cells.c, center, cells.religion, 0, 25, 3); + const influences = getReligionsInRadius(cells.c, center, cells.religion, 0, 25, 3, 0); const origins = type === "Folk" ? [0] : influences; const i = religions.length; diff --git a/modules/ui/options.js b/modules/ui/options.js index 403315cd..aa3875e2 100644 --- a/modules/ui/options.js +++ b/modules/ui/options.js @@ -537,7 +537,7 @@ function applyStoredOptions() { options.stateLabelsMode = stateLabelsModeInput.value; } -// randomize options if randomization is allowed (not locked or options='default') +// randomize options if randomization is allowed (not locked or queryParam options='default') function randomizeOptions() { const randomize = new URL(window.location.href).searchParams.get("options") === "default"; // ignore stored options @@ -549,7 +549,7 @@ function randomizeOptions() { manorsInput.value = 1000; manorsOutput.value = "auto"; } - if (randomize || !locked("religions")) religionsInput.value = religionsOutput.value = gauss(5, 2, 2, 10); + if (randomize || !locked("religions")) religionsInput.value = religionsOutput.value = gauss(6, 3, 2, 10); if (randomize || !locked("power")) powerInput.value = powerOutput.value = gauss(4, 2, 0, 10, 2); if (randomize || !locked("neutral")) neutralInput.value = neutralOutput.value = rn(1 + Math.random(), 1); if (randomize || !locked("cultures")) culturesInput.value = culturesOutput.value = gauss(12, 3, 5, 30); From 440430e4154289692bcc1707dfceff1251777ae6 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Tue, 21 Mar 2023 23:15:17 +0400 Subject: [PATCH 22/28] fix(religions): don't throw error if religion culture is 0 (happens if culture is removed) --- index.html | 2 +- modules/religions-generator.js | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/index.html b/index.html index 132b256a..6909d1cb 100644 --- a/index.html +++ b/index.html @@ -5617,7 +5617,7 @@