diff --git a/modules/dynamic/auto-update.js b/modules/dynamic/auto-update.js index da5f0250..fdb758de 100644 --- a/modules/dynamic/auto-update.js +++ b/modules/dynamic/auto-update.js @@ -618,17 +618,15 @@ export function resolveVersionConflicts(version) { } if (version < 1.86) { - // v1.86.0 added support of multi-origin culture and religion hierarchy trees + // v1.86.0 added multi-origin culture and religion hierarchy trees for (const culture of pack.cultures) { - const origin = culture.origin; + culture.origins = [culture.origin]; delete culture.origin; - culture.origins = [origin]; } for (const religion of pack.religions) { - const origin = religion.origin; + religion.origins = [religion.origin]; delete religion.origin; - religion.origins = [origin]; } } } diff --git a/modules/dynamic/editors/cultures-editor.js b/modules/dynamic/editors/cultures-editor.js index 154fe9fe..212c2130 100644 --- a/modules/dynamic/editors/cultures-editor.js +++ b/modules/dynamic/editors/cultures-editor.js @@ -295,15 +295,14 @@ function getShapeOptions(selectShape, selected) { } function cultureHighlightOn(event) { - const culture = Number(event.id || event.target.dataset.id); + const cultureId = Number(event.id || event.target.dataset.id); const $info = byId("cultureInfo"); if ($info) { - d3.select("#hierarchy").select(`g[data-id='${culture}']`).classed("selected", 1); - const c = pack.cultures[culture]; - const rural = c.rural * populationRate; - const urban = c.urban * populationRate * urbanization; - const population = rural + urban > 0 ? si(rn(rural + urban)) + " people" : "Extinct"; - $info.innerHTML = `${c.name} culture. ${c.type}. ${population}`; + d3.select("#hierarchy").select(`g[data-id='${cultureId}']`).classed("selected", 1); + const {name, type, rural, urban} = pack.cultures[cultureId]; + const population = rural * populationRate + urban * populationRate * urbanization; + const populationText = population > 0 ? si(rn(population)) + " people" : "Extinct"; + $info.innerHTML = `${name} culture. ${type}. ${populationText}`; tip("Drag to other node to add parent, click to edit"); } @@ -312,13 +311,13 @@ function cultureHighlightOn(event) { const animate = d3.transition().duration(2000).ease(d3.easeSinIn); cults - .select("#culture" + culture) + .select("#culture" + cultureId) .raise() .transition(animate) .attr("stroke-width", 2.5) .attr("stroke", "#d0240f"); debug - .select("#cultureCenter" + culture) + .select("#cultureCenter" + cultureId) .raise() .transition(animate) .attr("r", 8) @@ -326,23 +325,23 @@ function cultureHighlightOn(event) { } function cultureHighlightOff(event) { - const culture = Number(event.id || event.target.dataset.id); + const cultureId = Number(event.id || event.target.dataset.id); const $info = byId("cultureInfo"); if ($info) { - d3.select("#hierarchy").select(`g[data-id='${culture}']`).classed("selected", 0); + d3.select("#hierarchy").select(`g[data-id='${cultureId}']`).classed("selected", 0); $info.innerHTML = "‍"; tip(""); } if (!layerIsOn("toggleCultures")) return; cults - .select("#culture" + culture) + .select("#culture" + cultureId) .transition() .attr("stroke-width", null) .attr("stroke", null); debug - .select("#cultureCenter" + culture) + .select("#cultureCenter" + cultureId) .transition() .attr("r", 6) .attr("stroke", null); @@ -769,7 +768,6 @@ function showHierarchy() { $("#alert").dialog({ title: "Cultures tree", width: fitContent(), - minWidth: "20vw", resizable: false, position: {my: "left center", at: "left+10 center", of: "svg"}, buttons: null, @@ -822,10 +820,10 @@ function showHierarchy() { if (!selected.size()) return; const cultureId = d.data.i; - const newOrigin = Number(selected.datum().data.i); + const newOrigin = selected.datum().data.i; if (cultureId === newOrigin) return; // dragged to itself if (d.data.origins.includes(newOrigin)) return; // already a child of the selected node - if (newOrigin && d.descendants().some(node => node.id === newOrigin)) return; // cannot be a child of its own child + if (d.descendants().some(node => node.data.i === newOrigin)) return; // cannot be a child of its own child const culture = pack.cultures[cultureId]; if (culture.origins[0] === 0) culture.origins = []; diff --git a/modules/religions-generator.js b/modules/religions-generator.js index abe168ef..bc820384 100644 --- a/modules/religions-generator.js +++ b/modules/religions-generator.js @@ -277,7 +277,24 @@ window.Religions = (function () { "Word", "World" ], - color: ["Amber", "Black", "Blue", "Bright", "Brown", "Dark", "Golden", "Green", "Grey", "Light", "Orange", "Pink", "Purple", "Red", "White", "Yellow"] + color: [ + "Amber", + "Black", + "Blue", + "Bright", + "Brown", + "Dark", + "Golden", + "Green", + "Grey", + "Light", + "Orange", + "Pink", + "Purple", + "Red", + "White", + "Yellow" + ] }; const forms = { @@ -311,7 +328,18 @@ window.Religions = (function () { Cult: {Cult: 4, Sect: 4, Arcanum: 1, Coterie: 1, Order: 1, Worship: 1}, "Dark Cult": {Cult: 2, Sect: 2, Blasphemy: 1, Circle: 1, Coven: 1, Idols: 1, Occultism: 1}, - Heresy: {Heresy: 3, Sect: 2, Apostates: 1, Brotherhood: 1, Circle: 1, Dissent: 1, Dissenters: 1, Iconoclasm: 1, Schism: 1, Society: 1} + Heresy: { + Heresy: 3, + Sect: 2, + Apostates: 1, + Brotherhood: 1, + Circle: 1, + Dissent: 1, + Dissenters: 1, + Iconoclasm: 1, + Schism: 1, + Society: 1 + } }; const generate = function () { @@ -324,25 +352,27 @@ window.Religions = (function () { // add folk religions pack.cultures.forEach(c => { - if (!c.i) { - religions.push({i: 0, name: "No religion"}); - return; - } + if (!c.i) return religions.push({i: 0, name: "No religion"}); + if (c.removed) { - religions.push({i: c.i, name: "Extinct religion for " + c.name, color: getMixedColor(c.color, 0.1, 0), removed: true}); + religions.push({ + i: c.i, + name: "Extinct religion for " + c.name, + color: getMixedColor(c.color, 0.1, 0), + removed: true + }); 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); // `url(#hatch${rand(8,13)})`; - religions.push({i: c.i, name, color, culture: c.i, type: "Folk", form, deity, center: c.center, origin: 0}); + religions.push({i: c.i, name, color, culture: c.i, 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))); - return; - } + if (religionsInput.value == 0 || pack.cultures.length < 2) + return religions.filter(r => r.i).forEach(r => (r.code = abbreviate(r.name))); const burgs = pack.burgs.filter(b => b.i && !b.removed); const sorted = @@ -354,6 +384,12 @@ window.Religions = (function () { const cultsCount = Math.floor((rand(10, 40) / 100) * religionsInput.value); const count = +religionsInput.value - cultsCount + religions.length; + function getReligionsInRadius({x, y, r, max}) { + const cellsInRadius = findAll(x, y, r); + const religions = unique(cellsInRadius.map(i => cells.religion[i]).filter(r => r)); + return religions.length ? religions.slice(0, max) : [0]; + } + // generate organized religions for (let i = 0; religions.length < count && i < 1000; i++) { let center = sorted[biased(0, sorted.length - 1, 5)]; // religion center @@ -369,21 +405,35 @@ window.Religions = (function () { 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 = cells.p[center][0], - y = cells.p[center][1]; + 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 folk = religions.find(r => r.culture === culture && r.type === "Folk"); + 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 origin = folk ? folk.i : 0; + const origins = folk ? [folk.i] : getReligionsInRadius({x, y, r: 30, max: 2}); const expansionism = rand(3, 8); - const color = getMixedColor(religions[origin].color, 0.3, 0); // `url(#hatch${rand(0,5)})`; - religions.push({i: religions.length, name, color, culture, type: "Organized", form, deity, expansion, expansionism, center, origin}); + 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]); } @@ -391,23 +441,34 @@ window.Religions = (function () { 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 = cells.p[center][0], - y = cells.p[center][1]; + 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 folk = religions.find(r => r.culture === culture && r.type === "Folk"); - const origin = folk ? folk.i : 0; + const origins = getReligionsInRadius({x, y, r: 75, 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, origin}); + religions.push({ + i: religions.length, + name, + color, + culture, + type: "Cult", + form, + deity, + expansion: "global", + expansionism, + center, + origins + }); religionsTree.add([x, y]); - //debug.append("circle").attr("cx", x).attr("cy", y).attr("r", 2).attr("fill", "red"); } expandReligions(); @@ -419,11 +480,13 @@ window.Religions = (function () { 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 => cells.religion[i] === r.i && cells.c[i].some(c => cells.religion[c] !== r.i))); + let center = ra( + cells.i.filter(i => cells.religion[i] === r.i && cells.c[i].some(c => cells.religion[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 = cells.p[center][0], - y = cells.p[center][1]; + 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]; @@ -441,7 +504,7 @@ window.Religions = (function () { expansion: "global", expansionism, center, - origin: r.i + origins: [r.i] }); religionsTree.add([x, y]); } @@ -461,7 +524,8 @@ window.Religions = (function () { const culture = cells.culture[center]; const color = getMixedColor(religions[r].color, 0.3, 0); - const type = religions[r].type === "Organized" ? rw({Organized: 4, Cult: 1, Heresy: 2}) : rw({Organized: 5, Cult: 2}); + const type = + religions[r].type === "Organized" ? rw({Organized: 4, Cult: 1, Heresy: 2}) : rw({Organized: 5, Cult: 2}); const form = rw(forms[type]); const deity = type === "Heresy" ? religions[r].deity : form === "Non-theism" ? null : getDeityName(culture); @@ -491,7 +555,7 @@ window.Religions = (function () { area: 0, rural: 0, urban: 0, - origin: r, + origins: [r], code }); cells.religion[center] = i; @@ -534,7 +598,9 @@ window.Religions = (function () { const populationCost = Math.max(rn(popCost - cells.pop[e]), 0); const heightCost = Math.max(cells.h[e], 20) - 20; const waterCost = cells.h[e] < 20 ? (cells.road[e] ? 50 : 1000) : 0; - const totalCost = p + (cultureCost + stateCost + biomeCost + populationCost + heightCost + waterCost) / religions[r].expansionism; + const totalCost = + p + + (cultureCost + stateCost + biomeCost + populationCost + heightCost + waterCost) / religions[r].expansionism; if (totalCost > neutral) return; if (!cost[e] || totalCost < cost[e]) { @@ -576,7 +642,8 @@ window.Religions = (function () { const biomeCost = cells.road[e] ? 0 : biomesData.cost[cells.biome[e]]; const heightCost = Math.max(cells.h[e], 20) - 20; const waterCost = cells.h[e] < 20 ? (cells.road[e] ? 50 : 1000) : 0; - const totalCost = p + (religionCost + biomeCost + heightCost + waterCost) / Math.max(religions[r].expansionism, 0.1); + const totalCost = + p + (religionCost + biomeCost + heightCost + waterCost) / Math.max(religions[r].expansionism, 0.1); if (totalCost > neutral) return; @@ -641,35 +708,34 @@ window.Religions = (function () { 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); - if (a === "Adjective + Animal + of + Genitive") return ra(base.adjective) + " " + ra(base.animal) + " of " + ra(base.genitive); + if (a === "Adjective + Being + of + 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); } function getReligionName(form, deity, center) { - const cells = pack.cells; - const random = function () { - return Names.getCulture(cells.culture[center], null, null, "", 0); - }; - const type = function () { - return rw(types[form]); - }; - const supreme = function () { - return deity.split(/[ ,]+/)[0]; - }; - const place = function (adj) { - const base = cells.burg[center] ? pack.burgs[cells.burg[center]].name : pack.states[cells.state[center]].name; + const {cells, cultures, burgs, states} = pack; + + const random = () => Names.getCulture(cells.culture[center], null, null, "", 0); + const type = () => rw(types[form]); + const supreme = () => deity.split(/[ ,]+/)[0]; + const culture = () => cultures[cells.culture[center]].name; + const place = adj => { + const burgId = cells.burg[center]; + const stateId = cells.state[center]; + + const base = burgId ? burgs[burgId].name : states[stateId].name; let name = trimVowels(base.split(/[ ,]+/)[0]); return adj ? getAdjective(name) : name; }; - const culture = function () { - return pack.cultures[cells.culture[center]].name; - }; const m = rw(methods); 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"]; - if (m === "Faith of + Supreme" && deity) return [ra(["Faith", "Way", "Path", "Word", "Witnesses"]) + " of " + supreme(), "global"]; + if (m === "Faith of + Supreme" && deity) + return [ra(["Faith", "Way", "Path", "Word", "Witnesses"]) + " of " + supreme(), "global"]; if (m === "Place + ism") return [place() + "ism", "state"]; if (m === "Culture + ism") return [trimVowels(culture()) + "ism", "culture"]; if (m === "Place + ian + type") return [place("adj") + " " + type(), "state"]; diff --git a/modules/ui/religions-editor.js b/modules/ui/religions-editor.js index e4e9d174..8fbec3ac 100644 --- a/modules/ui/religions-editor.js +++ b/modules/ui/religions-editor.js @@ -8,7 +8,7 @@ function editReligions() { if (layerIsOn("toggleBiomes")) toggleBiomes(); if (layerIsOn("toggleProvinces")) toggleProvinces(); - const body = document.getElementById("religionsBody"); + const body = byId("religionsBody"); const animate = d3.transition().duration(1500).ease(d3.easeSinIn); refreshReligionsEditor(); drawReligionCenters(); @@ -25,17 +25,17 @@ function editReligions() { }); // add listeners - document.getElementById("religionsEditorRefresh").addEventListener("click", refreshReligionsEditor); - document.getElementById("religionsEditStyle").addEventListener("click", () => editStyle("relig")); - document.getElementById("religionsLegend").addEventListener("click", toggleLegend); - document.getElementById("religionsPercentage").addEventListener("click", togglePercentageMode); - document.getElementById("religionsHeirarchy").addEventListener("click", showHierarchy); - document.getElementById("religionsExtinct").addEventListener("click", toggleExtinct); - document.getElementById("religionsManually").addEventListener("click", enterReligionsManualAssignent); - document.getElementById("religionsManuallyApply").addEventListener("click", applyReligionsManualAssignent); - document.getElementById("religionsManuallyCancel").addEventListener("click", () => exitReligionsManualAssignment()); - document.getElementById("religionsAdd").addEventListener("click", enterAddReligionMode); - document.getElementById("religionsExport").addEventListener("click", downloadReligionsData); + byId("religionsEditorRefresh").on("click", refreshReligionsEditor); + byId("religionsEditStyle").on("click", () => editStyle("relig")); + byId("religionsLegend").on("click", toggleLegend); + byId("religionsPercentage").on("click", togglePercentageMode); + byId("religionsHeirarchy").on("click", showHierarchy); + byId("religionsExtinct").on("click", toggleExtinct); + byId("religionsManually").on("click", enterReligionsManualAssignent); + byId("religionsManuallyApply").on("click", applyReligionsManualAssignent); + byId("religionsManuallyCancel").on("click", () => exitReligionsManualAssignment()); + byId("religionsAdd").on("click", enterAddReligionMode); + byId("religionsExport").on("click", downloadReligionsData); function refreshReligionsEditor() { religionsCollectStatistics(); @@ -72,7 +72,9 @@ function editReligions() { const urban = r.urban * populationRate * urbanization; const population = rn(rural + urban); if (r.i && !r.cells && body.dataset.extinct !== "show") continue; // hide extinct religions - const populationTip = `Believers: ${si(population)}; Rural areas: ${si(rural)}; Urban areas: ${si(urban)}. Click to change`; + const populationTip = `Believers: ${si(population)}; Rural areas: ${si(rural)}; Urban areas: ${si( + urban + )}. Click to change`; totalArea += area; totalPopulation += population; @@ -90,13 +92,19 @@ function editReligions() { data-expansionism=${r.expansionism} > - + - + - +
${si(area) + unit}
@@ -118,7 +126,9 @@ function editReligions() { data-expansionism="" > - + @@ -146,17 +156,17 @@ function editReligions() { religionsFooterPopulation.dataset.population = totalPopulation; // add listeners - body.querySelectorAll("div.religions").forEach(el => el.addEventListener("mouseenter", ev => religionHighlightOn(ev))); - body.querySelectorAll("div.religions").forEach(el => el.addEventListener("mouseleave", ev => religionHighlightOff(ev))); - body.querySelectorAll("div.states").forEach(el => el.addEventListener("click", selectReligionOnLineClick)); - body.querySelectorAll("fill-box").forEach(el => el.addEventListener("click", religionChangeColor)); - body.querySelectorAll("div > input.religionName").forEach(el => el.addEventListener("input", religionChangeName)); - body.querySelectorAll("div > select.religionType").forEach(el => el.addEventListener("change", religionChangeType)); - body.querySelectorAll("div > input.religionForm").forEach(el => el.addEventListener("input", religionChangeForm)); - body.querySelectorAll("div > input.religionDeity").forEach(el => el.addEventListener("input", religionChangeDeity)); - body.querySelectorAll("div > span.icon-arrows-cw").forEach(el => el.addEventListener("click", regenerateDeity)); - body.querySelectorAll("div > div.culturePopulation").forEach(el => el.addEventListener("click", changePopulation)); - body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.addEventListener("click", religionRemove)); + body.querySelectorAll("div.religions").forEach(el => el.on("mouseenter", religionHighlightOn)); + body.querySelectorAll("div.religions").forEach(el => el.on("mouseleave", religionHighlightOff)); + body.querySelectorAll("div.states").forEach(el => el.on("click", selectReligionOnLineClick)); + body.querySelectorAll("fill-box").forEach(el => el.on("click", religionChangeColor)); + body.querySelectorAll("div > input.religionName").forEach(el => el.on("input", religionChangeName)); + body.querySelectorAll("div > select.religionType").forEach(el => el.on("change", religionChangeType)); + body.querySelectorAll("div > input.religionForm").forEach(el => el.on("input", religionChangeForm)); + 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.culturePopulation").forEach(el => el.on("click", changePopulation)); + body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.on("click", religionRemove)); if (body.dataset.type === "percentage") { body.dataset.type = "absolute"; @@ -174,35 +184,39 @@ function editReligions() { } function religionHighlightOn(event) { - const religion = +event.target.dataset.id; - const info = document.getElementById("religionInfo"); - if (info) { - d3.select("#hierarchy") - .select("g[data-id='" + religion + "'] > path") - .classed("selected", 1); - const r = pack.religions[religion]; - const type = r.name.includes(r.type) ? "" : r.type === "Folk" || r.type === "Organized" ? ". " + r.type + " religion" : ". " + r.type; - const form = r.form === r.type || r.name.includes(r.form) ? "" : ". " + r.form; - const rural = r.rural * populationRate; - const urban = r.urban * populationRate * urbanization; - const population = rural + urban > 0 ? ". " + si(rn(rural + urban)) + " believers" : ". Extinct"; - info.innerHTML = /* html */ `${r.name}${type}${form}${population}`; - tip("Drag to change parent, drag to itself to move to the top level. Hold CTRL and click to change abbreviation"); + const religionId = Number(event.id || event.target.dataset.id); + const $info = byId("religionInfo"); + if ($info) { + d3.select("#hierarchy").select(`g[data-id='${religionId}']`).classed("selected", 1); + const {name, type, form, rural, urban} = pack.religions[religionId]; + + const getTypeText = () => { + if (name.includes(type)) return ""; + if (form.includes(type)) return ""; + if (type === "Folk" || type === "Organized") return `. ${type} religion`; + return `. ${type}`; + }; + const formText = form === type ? "" : ". " + form; + const population = rural * populationRate + urban * populationRate * urbanization; + const populationText = population > 0 ? si(rn(population)) + " people" : "Extinct"; + + $info.innerHTML = `${name}${getTypeText()}${formText}. ${populationText}`; + tip("Drag to other node to add parent, click to edit"); } - const el = body.querySelector(`div[data-id='${religion}']`); + const el = body.querySelector(`div[data-id='${religionId}']`); if (el) el.classList.add("active"); if (!layerIsOn("toggleReligions")) return; if (customization) return; relig - .select("#religion" + religion) + .select("#religion" + religionId) .raise() .transition(animate) .attr("stroke-width", 2.5) .attr("stroke", "#c13119"); debug - .select("#religionsCenter" + religion) + .select("#religionsCenter" + religionId) .raise() .transition(animate) .attr("r", 8) @@ -211,26 +225,24 @@ function editReligions() { } function religionHighlightOff(event) { - const religion = +event.target.dataset.id; - const info = document.getElementById("religionInfo"); - if (info) { - d3.select("#hierarchy") - .select("g[data-id='" + religion + "'] > path") - .classed("selected", 0); - info.innerHTML = "‍"; + const religionId = Number(event.id || event.target.dataset.id); + const $info = byId("religionInfo"); + if ($info) { + d3.select("#hierarchy").select(`g[data-id='${religionId}']`).classed("selected", 0); + $info.innerHTML = "‍"; tip(""); } - const el = body.querySelector(`div[data-id='${religion}']`); + const el = body.querySelector(`div[data-id='${religionId}']`); if (el) el.classList.remove("active"); relig - .select("#religion" + religion) + .select("#religion" + religionId) .transition() .attr("stroke-width", null) .attr("stroke", null); debug - .select("#religionsCenter" + religion) + .select("#religionsCenter" + religionId) .transition() .attr("r", 4) .attr("stroke-width", 1.2) @@ -309,8 +321,12 @@ function editReligions() { >

Rural: Urban: - -

Total believers: ${l(total)} ⇒ ${l(total)} (100%)

`; + +

Total believers: ${l(total)} ⇒ ${l( + total + )} (100%)

`; const update = function () { const totalNew = ruralPop.valueAsNumber + urbanPop.valueAsNumber; @@ -367,7 +383,7 @@ function editReligions() { function religionRemove() { if (customization) return; - const religion = +this.parentNode.dataset.id; + const religionId = +this.parentNode.dataset.id; alertMessage.innerHTML = "Are you sure you want to remove the religion?
This action cannot be reverted"; $("#alert").dialog({ @@ -375,18 +391,21 @@ function editReligions() { title: "Remove religion", buttons: { Remove: function () { - relig.select("#religion" + religion).remove(); - relig.select("#religion-gap" + religion).remove(); - debug.select("#religionsCenter" + religion).remove(); + relig.select("#religion" + religionId).remove(); + relig.select("#religion-gap" + religionId).remove(); + debug.select("#religionsCenter" + religionId).remove(); pack.cells.religion.forEach((r, i) => { - if (r === religion) pack.cells.religion[i] = 0; - }); - pack.religions[religion].removed = true; - const origin = pack.religions[religion].origin; - pack.religions.forEach(r => { - if (r.origin === religion) r.origin = origin; + if (r === religionId) pack.cells.religion[i] = 0; }); + pack.religions[religionId].removed = true; + + pack.religions + .filter(r => r.i && !r.removed) + .forEach(r => { + r.origins = r.origins.filter(origin => origin !== religionId); + if (!r.origins.length) r.origins = [0]; + }); refreshReligionsEditor(); $(this).dialog("close"); @@ -400,7 +419,12 @@ function editReligions() { function drawReligionCenters() { debug.select("#religionCenters").remove(); - const religionCenters = debug.append("g").attr("id", "religionCenters").attr("stroke-width", 1.2).attr("stroke", "#444444").style("cursor", "move"); + const religionCenters = debug + .append("g") + .attr("id", "religionCenters") + .attr("stroke-width", 1.2) + .attr("stroke", "#444444") + .style("cursor", "move"); const data = pack.religions.filter(r => r.i && r.center && r.cells && !r.removed); religionCenters @@ -466,67 +490,90 @@ function editReligions() { function showHierarchy() { // build hierarchy tree - pack.religions[0].origin = null; - const religions = pack.religions.filter(r => !r.removed); - if (religions.length < 3) { - tip("Not enough religions to show hierarchy", false, "error"); - return; - } + pack.religions[0].origins = [null]; + const validReligions = pack.religions.filter(r => !r.removed); + if (validReligions.length < 3) return tip("Not enough religions to show hierarchy", false, "error"); + const root = d3 .stratify() .id(d => d.i) - .parentId(d => d.origin)(religions); + .parentId(d => d.origins[0])(validReligions); const treeWidth = root.leaves().length; const treeHeight = root.height; - const width = treeWidth * 40, - height = treeHeight * 60; + const width = Math.max(treeWidth * 40, 300); + const height = treeHeight * 60; const margin = {top: 10, right: 10, bottom: -5, left: 10}; const w = width - margin.left - margin.right; const h = height + 30 - margin.top - margin.bottom; const treeLayout = d3.tree().size([w, h]); + alertMessage.innerHTML = /* html */ `
+
+ +
`; + // prepare svg - alertMessage.innerHTML = "
"; const svg = d3 .select("#alertMessage") - .insert("svg", "#religionInfo") + .insert("svg", "#religionChartDetails") .attr("id", "hierarchy") .attr("width", width) .attr("height", height) .style("text-anchor", "middle"); const graph = svg.append("g").attr("transform", `translate(10, -45)`); const links = graph.append("g").attr("fill", "none").attr("stroke", "#aaaaaa"); + const primaryLinks = links.append("g"); + const secondaryLinks = links.append("g").attr("stroke-dasharray", 1); const nodes = graph.append("g"); + // render helper functions + const getLinkPath = d => { + const { + source: {x: sx, y: sy}, + target: {x: tx, y: ty} + } = d; + return `M${sx},${sy} C${sx},${(sy * 3 + ty) / 4} ${tx},${(sy * 2 + ty) / 3} ${tx},${ty}`; + }; + + const getSecondaryLinks = root => { + const nodes = root.descendants(); + const links = []; + + for (const node of nodes) { + const origins = node.data.origins; + if (node.depth < 2) continue; + + for (let i = 1; i < origins.length; i++) { + const source = nodes.find(n => n.data.i === origins[i]); + if (source) links.push({source, target: node}); + } + } + + return links; + }; + + const nodePathMap = { + undefined: "M5,0A5,5,0,1,1,-5,0A5,5,0,1,1,5,0", // small circle + Folk: "M11.3,0A11.3,11.3,0,1,1,-11.3,0A11.3,11.3,0,1,1,11.3,0", // circle + Organized: "M-11,-11h22v22h-22Z", // square + Cult: "M-6.5,-11.26l13,0l6.5,11.26l-6.5,11.26l-13,0l-6.5,-11.26Z", // hexagon + Heresy: "M0,-14L14,0L0,14L-14,0Z" // diamond + }; + + const getNodePath = d => nodePathMap[d.data.type]; + renderTree(); function renderTree() { treeLayout(root); - links - .selectAll("path") - .data(root.links()) - .enter() - .append("path") - .attr("d", d => { - return ( - "M" + - d.source.x + - "," + - d.source.y + - "C" + - d.source.x + - "," + - (d.source.y * 3 + d.target.y) / 4 + - " " + - d.target.x + - "," + - (d.source.y * 2 + d.target.y) / 3 + - " " + - d.target.x + - "," + - d.target.y - ); - }); + + primaryLinks.selectAll("path").data(root.links()).enter().append("path").attr("d", getLinkPath); + secondaryLinks.selectAll("path").data(getSecondaryLinks(root)).enter().append("path").attr("d", getLinkPath); const node = nodes .selectAll("g") @@ -536,30 +583,21 @@ function editReligions() { .attr("data-id", d => d.data.i) .attr("stroke", "#333333") .attr("transform", d => `translate(${d.x}, ${d.y})`) - .on("mouseenter", () => religionHighlightOn(event)) - .on("mouseleave", () => religionHighlightOff(event)) - .call(d3.drag().on("start", d => dragToReorigin(d))); + .on("mouseenter", religionHighlightOn) + .on("mouseleave", religionHighlightOff) + .on("click", religionSelect) + .call(d3.drag().on("start", dragToReorigin)); node .append("path") - .attr("d", d => { - if (d.data.type === "Folk") return "M11.3,0A11.3,11.3,0,1,1,-11.3,0A11.3,11.3,0,1,1,11.3,0"; - // circle - else if (d.data.type === "Heresy") return "M0,-14L14,0L0,14L-14,0Z"; - // diamond - else if (d.data.type === "Cult") return "M-6.5,-11.26l13,0l6.5,11.26l-6.5,11.26l-13,0l-6.5,-11.26Z"; - // hex - else if (!d.data.i) return "M5,0A5,5,0,1,1,-5,0A5,5,0,1,1,5,0"; - // small circle - else return "M-11,-11h22v22h-22Z"; // square - }) - .attr("fill", d => (d.data.i ? d.data.color : "#ffffff")) + .attr("d", getNodePath) + .attr("fill", d => d.data.color || "#ffffff") .attr("stroke-dasharray", d => (d.data.cells ? "null" : "1")); node .append("text") .attr("dy", ".35em") - .text(d => (d.data.i ? d.data.code : "")); + .text(d => d.data.code || ""); } $("#alert").dialog({ @@ -573,12 +611,38 @@ function editReligions() { } }); - function dragToReorigin(d) { - if (isCtrlClick(d3.event.sourceEvent)) { - changeCode(d); - return; - } + function religionSelect(d) { + d3.event.stopPropagation(); + nodes.selectAll("g").style("outline", "none"); + this.style.outline = "1px solid #c13119"; + byId("religionSelected").style.display = "block"; + byId("religionInfo").style.display = "none"; + + const religion = d.data; + byId("religionSelectedName").innerText = religion.name; + byId("religionSelectedCode").value = religion.code; + + byId("religionSelectedCode").onchange = function () { + if (this.value.length > 3) return tip("Abbreviation must be 3 characters or less", false, "error", 3000); + if (!this.value.length) return tip("Abbreviation cannot be empty", false, "error", 3000); + nodes.select(`g[data-id="${d.id}"] > text`).text(this.value); + religion.code = this.value; + }; + + byId("religionSelectedClear").onclick = () => { + religion.origins = [0]; + showHierarchy(); + }; + + byId("religionSelectedClose").onclick = () => { + this.style.outline = "none"; + byId("religionSelected").style.display = "none"; + byId("religionInfo").style.display = "block"; + }; + } + + function dragToReorigin(d) { const originLine = graph.append("path").attr("class", "dragLine").attr("d", `M${d.x},${d.y}L${d.x},${d.y}`); d3.event.on("drag", () => { @@ -587,26 +651,20 @@ function editReligions() { d3.event.on("end", () => { originLine.remove(); - const selected = graph.select("path.selected"); + const selected = graph.select("g.selected"); if (!selected.size()) return; - const religion = d.data.i; - const oldOrigin = d.data.origin; - let newOrigin = selected.datum().data.i; - if (newOrigin == oldOrigin) return; // already a child of the selected node - if (newOrigin == religion) newOrigin = 0; // move to top - if (newOrigin && d.descendants().some(node => node.id == newOrigin)) return; // cannot be a child of its own child - pack.religions[religion].origin = d.data.origin = newOrigin; // change data - showHierarchy(); // update hierarchy - }); - } - function changeCode(d) { - prompt(`Please provide an abbreviation for ${d.data.name}`, {default: d.data.code}, v => { - pack.religions[d.data.i].code = v; - nodes - .select("g[data-id='" + d.data.i + "']") - .select("text") - .text(v); + const religionId = d.data.i; + const newOrigin = selected.datum().data.i; + if (religionId === newOrigin) return; // dragged to itself + if (d.data.origins.includes(newOrigin)) return; // already a child of the selected node + if (d.descendants().some(node => node.data.i === newOrigin)) return; // cannot be a child of its own child + + const religion = pack.religions[religionId]; + if (religion.origins[0] === 0) religion.origins = []; + religion.origins.push(newOrigin); + + showHierarchy(); }); } } @@ -621,7 +679,7 @@ function editReligions() { customization = 7; relig.append("g").attr("id", "temp"); document.querySelectorAll("#religionsBottom > button").forEach(el => (el.style.display = "none")); - document.getElementById("religionsManuallyButtons").style.display = "inline-block"; + byId("religionsManuallyButtons").style.display = "inline-block"; debug.select("#religionCenters").style("display", "none"); religionsEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden")); @@ -685,7 +743,13 @@ function editReligions() { // change of append new element if (exists.size()) exists.attr("data-religion", r).attr("fill", color); - else temp.append("polygon").attr("data-cell", i).attr("data-religion", r).attr("points", getPackPolygon(i)).attr("fill", color); + else + temp + .append("polygon") + .attr("data-cell", i) + .attr("data-religion", r) + .attr("points", getPackPolygon(i)) + .attr("fill", color); }); } @@ -717,7 +781,7 @@ function editReligions() { relig.select("#temp").remove(); removeCircle(); document.querySelectorAll("#religionsBottom > button").forEach(el => (el.style.display = "inline-block")); - document.getElementById("religionsManuallyButtons").style.display = "none"; + byId("religionsManuallyButtons").style.display = "none"; religionsEditor.querySelectorAll(".hide").forEach(el => el.classList.remove("hidden")); religionsFooter.style.display = "block";