diff --git a/index.html b/index.html
index d7a60bc6..132b256a 100644
--- a/index.html
+++ b/index.html
@@ -7860,10 +7860,10 @@
-
+
-
+
@@ -7877,12 +7877,12 @@
-
-
+
+
-
+
diff --git a/main.js b/main.js
index 5cf0edc3..49b0f001 100644
--- a/main.js
+++ b/main.js
@@ -684,6 +684,7 @@ async function generate(options) {
const timeStart = performance.now();
const {seed: precreatedSeed, graph: precreatedGraph} = options || {};
+ pack = {};
invokeActiveZooming();
setSeed(precreatedSeed);
INFO && console.group("Generated Map " + seed);
@@ -694,6 +695,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 = {};
markFeatures();
markupGridOcean();
diff --git a/modules/cultures-generator.js b/modules/cultures-generator.js
index eeb09407..1c0e07a7 100644
--- a/modules/cultures-generator.js
+++ b/modules/cultures-generator.js
@@ -118,21 +118,23 @@ window.Cultures = (function () {
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 cultures = [];
+
pack.cultures?.forEach(function (culture) {
if (culture.lock) cultures.push(culture);
});
+
+ if (!cultures.length) {
+ if (culturesNumber === def.length) return def;
+ if (def.every(d => d.odd === 1)) return def.splice(0, culturesNumber);
+ }
- for (let culture, rnd, i = 0; cultures.length < count && i < 200; i++) {
+ for (let culture, rnd, i = 0; cultures.length < culturesNumber && def.length > 0;) {
do {
rnd = rand(def.length - 1);
culture = def[rnd];
- } while (!P(culture.odd));
+ i++;
+ } while (i < 200 && !P(culture.odd));
cultures.push(culture);
def.splice(rnd, 1);
}
diff --git a/modules/dynamic/editors/religions-editor.js b/modules/dynamic/editors/religions-editor.js
index 8a4abc5e..d6c60dfb 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")) toggleReligions();
if (layerIsOn("toggleStates")) toggleStates();
if (layerIsOn("toggleBiomes")) toggleBiomes();
- if (layerIsOn("toggleCultures")) toggleReligions();
+ if (layerIsOn("toggleCultures")) toggleCultures();
if (layerIsOn("toggleProvinces")) toggleProvinces();
- if (!layerIsOn("toggleReligions")) toggleReligions();
refreshReligionsEditor();
drawReligionCenters();
@@ -23,13 +23,15 @@ export function open() {
function insertEditorHtml() {
const editorHtml = /* html */ `
-
+
+
+
+
+
`;
@@ -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