From ff1d3566c434b3beddc54e29c826dd2dc815ebc7 Mon Sep 17 00:00:00 2001
From: Azgaar
Date: Sun, 5 Jun 2022 18:26:28 +0300
Subject: [PATCH] feat: multi-parental tree for religions
---
modules/dynamic/auto-update.js | 8 +-
modules/dynamic/editors/cultures-editor.js | 30 +-
modules/religions-generator.js | 172 +++++++---
modules/ui/religions-editor.js | 356 ++++++++++++---------
4 files changed, 346 insertions(+), 220 deletions(-)
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 */ `
+
+
+ .
+ Abbreviation:
+
+
+
+
`;
+
// 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";