refactor(#902): regenerate religions

This commit is contained in:
Azgaar 2023-01-02 01:12:18 +03:00
parent 788ce5f1db
commit 2595ab9408
3 changed files with 157 additions and 253 deletions

View file

@ -106,7 +106,7 @@ window.Markers = (function () {
let candidates = Array.from(list(pack)); let candidates = Array.from(list(pack));
let quantity = getQuantity(candidates, min, each, multiplier); let quantity = getQuantity(candidates, min, each, multiplier);
// uncomment for debugging: // uncomment for debugging:
// console.log(`${icon} ${type}: each ${each} of ${candidates.length}, min ${min} candidates. Got ${quantity}`); // console.info(`${icon} ${type}: each ${each} of ${candidates.length}, min ${min} candidates. Got ${quantity}`);
while (quantity && candidates.length) { while (quantity && candidates.length) {
const [cell] = extractAnyElement(candidates); const [cell] = extractAnyElement(candidates);

View file

@ -344,66 +344,15 @@ window.Religions = (function () {
const generate = function () { const generate = function () {
TIME && console.time("generateReligions"); TIME && console.time("generateReligions");
const cells = pack.cells, const {cells, states, cultures} = pack;
states = pack.states,
cultures = pack.cultures;
// Keep a map of the previous religions per cell for referencing when we restore locked religions const religionIds = new Uint16Array(cells.culture); // cell religion; initially based on culture
const prevReligions = {}; const religions = [];
if (cells.religion) {
cells.religion.forEach((rId, index) => {
prevReligions[index] = rId;
})
}
cells.religion = new Uint16Array(cells.culture); // cell religion; initially based on culture
const religionsToRestore = [];
const folkToRestore = [];
const restoredCells = [];
const restoredHeresyCells = [];
// Restore locked religions to their existing cells
if (pack.religions) {
pack.religions.forEach( (religion) => {
// Keep any locked religions, we will reassign it to a folk or organized religion later
if (!religion.lock) return;
// Add all religions we restore after the base cults to keep the index correct
// Keep folk religion at the same index since we should restore them during the culture parsing
const id = religion.type === 'Folk' ? religion.i : pack.cultures.length + religionsToRestore.length;
Object.entries(prevReligions).forEach(([index, rId]) => {
if (rId !== religion.i) return;
if (religion.type === "Heresy") {
restoredHeresyCells.push(parseInt(index));
religion.old_i = religion.i;
} else {
restoredCells.push(parseInt(index));
cells.religion[index] = id;
}
});
if (religion.type === 'Folk') {
folkToRestore.push(religion);
} else {
religionsToRestore.push(religion);
religion.i = id;
}
});
}
const religions = (pack.religions = []);
// add folk religions // add folk religions
pack.cultures.forEach(c => { pack.cultures.forEach(c => {
// If a restored religion exists for this culture, move it to this position const newId = c.i;
const existingFolkReligion = folkToRestore.find(r => r.culture === c.i); if (!newId) return religions.push({i: 0, name: "No religion"});
if (existingFolkReligion !== undefined) {
religions.push(existingFolkReligion);
return;
}
// We already preadded the "no religion" religion
if (!c.i) return religions.push({i: 0, name: "No religion"});
if (c.removed) { if (c.removed) {
religions.push({ religions.push({
@ -415,13 +364,38 @@ window.Religions = (function () {
return; return;
} }
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 form = rw(forms.Folk);
const name = c.name + " " + rw(types[form]); const name = c.name + " " + rw(types[form]);
const deity = form === "Animism" ? null : getDeityName(c.i); const deity = form === "Animism" ? null : getDeityName(c.i);
const color = getMixedColor(c.color, 0.1, 0); // `url(#hatch${rand(8,13)})`; const color = getMixedColor(c.color, 0.1, 0); // `url(#hatch${rand(8,13)})`;
religions.push({i: c.i, name, color, culture: c.i, type: "Folk", form, deity, center: c.center, origins: [0]}); religions.push({
i: newId,
name,
color,
culture: newId,
type: "Folk",
form,
deity,
center: c.center,
origins: [0]
});
}); });
religions.push(...religionsToRestore);
if (religionsInput.value == 0 || pack.cultures.length < 2) if (religionsInput.value == 0 || pack.cultures.length < 2)
return religions.filter(r => r.i).forEach(r => (r.code = abbreviate(r.name))); return religions.filter(r => r.i).forEach(r => (r.code = abbreviate(r.name)));
@ -431,7 +405,7 @@ window.Religions = (function () {
burgs.length > +religionsInput.value burgs.length > +religionsInput.value
? burgs.sort((a, b) => b.population - a.population).map(b => b.cell) ? 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]); : cells.i.filter(i => cells.s[i] > 2).sort((a, b) => cells.s[b] - cells.s[a]);
const available = [...sorted].filter(cellI => !restoredCells.includes(cellI));
const religionsTree = d3.quadtree(); const religionsTree = d3.quadtree();
const spacing = (graphWidth + graphHeight) / 6 / religionsInput.value; // base min distance between towns const spacing = (graphWidth + graphHeight) / 6 / religionsInput.value; // base min distance between towns
const cultsCount = Math.floor((rand(10, 40) / 100) * religionsInput.value); const cultsCount = Math.floor((rand(10, 40) / 100) * religionsInput.value);
@ -440,39 +414,29 @@ window.Religions = (function () {
function getReligionsInRadius({x, y, r, max}) { function getReligionsInRadius({x, y, r, max}) {
if (max === 0) return [0]; if (max === 0) return [0];
const cellsInRadius = findAll(x, y, r); const cellsInRadius = findAll(x, y, r);
const religions = unique(cellsInRadius.map(i => cells.religion[i]).filter(r => r)); const religions = unique(cellsInRadius.map(i => religionIds[i]).filter(r => r));
return religions.length ? religions.slice(0, max) : [0]; return religions.length ? religions.slice(0, max) : [0];
} }
// Restore the origins of any organized religion that was locked // restore locked non-folk religions
pack.religions.forEach(religion => { if (pack.religions) {
// Ignore if the religion is not locked or not organized const lockedNonFolkReligions = pack.religions.filter(r => r.lock && !r.removed && r.type !== "Folk");
if (religion.type !== "Organized" || !religion.lock) return; for (const religion of lockedNonFolkReligions) {
// Ignore if the religion already has a valid origin const newId = religions.length;
if (pack.religions.find(r => r.i !== religion.i && religion.origins.includes(r.i) && r.lock) !== undefined) for (const i of cells.i) {
return; if (cells.religion[i] === religion.i) religionIds[i] = newId;
}
// Select a random folk religion for the religion religion.i = newId;
const culture = cells.culture[religion.center]; religion.origins = religion.origins.filter(origin => origin < newId);
const [x, y] = cells.p[religion.center]; religionsTree.add(cells.p[religion.center]);
const isFolkBased = religion.expansion === "culture" || P(0.5); religions.push(religion);
const folk = isFolkBased && religions.find(r => r.i !== religion.i && r.culture === culture && r.type === "Folk"); }
if (folk && religion.expansion === "culture" && folk.name.slice(0, 3) !== "Old") folk.name = "Old " + folk.name; }
// have a counter here to adjust the search range and make sure we can find a religion
let runs = 1;
do {
// Run the search until we have at least one source religion that is not the current religion.
religion.origins = folk ? [folk.i] : getReligionsInRadius({x, y, r: (150 * runs) / count, max: 2})
.filter(r => r !== religion.i);
runs++;
} while (!religion.origins.length)
religionsTree.add([x, y]);
});
// generate organized religions // generate organized religions
for (let i = 0; religions.length < count && i < 1000; i++) { for (let i = 0; religions.length < count && i < 1000; i++) {
let center = available[biased(0, available.length - 1, 5)]; // religion center let center = sorted[biased(0, sorted.length - 1, 5)]; // religion center
const form = rw(forms.Organized); const form = rw(forms.Organized);
const state = cells.state[center]; const state = cells.state[center];
const culture = cells.culture[center]; const culture = cells.culture[center];
@ -518,30 +482,10 @@ window.Religions = (function () {
religionsTree.add([x, y]); religionsTree.add([x, y]);
} }
// Restore the origins of any cult that was locked
pack.religions.forEach(religion => {
// Ignore if the religion is not locked or not organized
if (religion.type !== "Cult" || !religion.lock) return;
// Ignore if the religion already has a valid origin
if (pack.religions.find(r => r.i !== religion.i && religion.origins.includes(r.i) && r.lock) !== undefined)
return;
const [x, y] = cells.p[religion.center];
// have a counter here to adjust the search range and make sure we can find a religion
let runs = 1;
do {
// Run the search until we have at least one source religion that is not the current religion.
religion.origins = getReligionsInRadius({x, y, r: (300 * runs) / count, max: rand(0, 4)})
.filter(r => r !== religion.i);
runs++;
} while (!religion.origins.length)
religionsTree.add([x, y]);
});
// generate cults // generate cults
for (let i = 0; religions.length < count + cultsCount && i < 1000; i++) { for (let i = 0; religions.length < count + cultsCount && i < 1000; i++) {
const form = rw(forms.Cult); const form = rw(forms.Cult);
let center = available[biased(0, available.length - 1, 1)]; // religion center let center = sorted[biased(0, sorted.length - 1, 1)]; // religion center
if (!cells.burg[center] && cells.c[center].some(c => cells.burg[c])) if (!cells.burg[center] && cells.c[center].some(c => cells.burg[c]))
center = cells.c[center].find(c => cells.burg[c]); center = cells.c[center].find(c => cells.burg[c]);
const [x, y] = cells.p[center]; const [x, y] = cells.p[center];
@ -572,32 +516,7 @@ window.Religions = (function () {
religionsTree.add([x, y]); religionsTree.add([x, y]);
} }
expandReligions(restoredCells); expandReligions();
// Restore the origins of any heresy that was locked
pack.religions.forEach(religion => {
// Ignore if the religion is not locked or not organized
if (religion.type !== "Heresy" || !religion.lock) return;
const originReligion = cells.religion[religion.center];
// Restore the cells now that all other religions have been processed
Object.entries(prevReligions).forEach(([index, rId]) => {
if (rId !== religion.old_i) return;
cells.religion[parseInt(index)] = religion.i;
});
delete religion.old_i;
// Ignore if the religion already has a valid origin
if (pack.religions.find(r => r.i !== religion.i && religion.origins.includes(r.i) && r.lock) !== undefined)
return;
const [x, y] = cells.p[religion.center];
// Use the religion from the expanded cells, we'll restore the heresies later
religion.origins = [originReligion];
religionsTree.add([x, y]);
});
// generate heresies // generate heresies
religions religions
@ -606,9 +525,7 @@ window.Religions = (function () {
if (r.expansionism < 3) return; if (r.expansionism < 3) return;
const count = gauss(0, 1, 0, 3); const count = gauss(0, 1, 0, 3);
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
let center = ra( let center = ra(cells.i.filter(i => religionIds[i] === r.i && cells.c[i].some(c => religionIds[c] !== r.i)));
cells.i.filter(i => cells.religion[i] === r.i && cells.c[i].some(c => cells.religion[c] !== r.i))
);
if (!center) continue; if (!center) continue;
if (!cells.burg[center] && cells.c[center].some(c => cells.burg[c])) if (!cells.burg[center] && cells.c[center].some(c => cells.burg[c]))
center = cells.c[center].find(c => cells.burg[c]); center = cells.c[center].find(c => cells.burg[c]);
@ -636,10 +553,111 @@ window.Religions = (function () {
} }
}); });
expandHeresies([...restoredCells, ...restoredHeresyCells]); expandHeresies();
checkCenters(); checkCenters();
cells.religion = religionIds;
pack.religions = religions;
TIME && console.timeEnd("generateReligions"); 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 add = function (center) {
@ -690,120 +708,6 @@ window.Religions = (function () {
cells.religion[center] = i; cells.religion[center] = i;
}; };
// growth algorithm to assign cells to religions
const expandReligions = function (restoredCells) {
const cells = pack.cells,
religions = pack.religions;
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 => {
cells.religion[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 next = queue.dequeue(),
n = next.e,
p = next.p,
r = next.r,
c = next.c,
s = next.s;
const expansion = religions[r].expansion;
cells.c[n].forEach(function (e) {
if (expansion === "culture" && c !== cells.culture[e]) return;
if (expansion === "state" && s !== cells.state[e]) return;
if (restoredCells.includes(e)) return;
const cultureCost = c !== cells.culture[e] ? 10 : 0;
const stateCost = s !== cells.state[e] ? 10 : 0;
const biomeCost = cells.road[e] ? 1 : biomesData.cost[cells.biome[e]];
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;
if (totalCost > neutral) return;
if (!cost[e] || totalCost < cost[e]) {
if (cells.h[e] >= 20 && cells.culture[e]) cells.religion[e] = r; // assign religion to cell
cost[e] = totalCost;
queue.queue({e, p: totalCost, r, c, s});
}
});
}
};
// growth algorithm to assign cells to heresies
const expandHeresies = function (restoredCells) {
const cells = pack.cells,
religions = pack.religions;
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 = cells.religion[r.center]; // "base" religion id
cells.religion[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 next = queue.dequeue(),
n = next.e,
p = next.p,
r = next.r,
b = next.b;
cells.c[n].forEach(function (e) {
if (restoredCells.includes(e)) return;
const religionCost = cells.religion[e] === b ? 0 : 2000;
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);
if (totalCost > neutral) return;
if (!cost[e] || totalCost < cost[e]) {
if (cells.h[e] >= 20 && cells.culture[e]) cells.religion[e] = r; // assign religion to cell
cost[e] = totalCost;
queue.queue({e, p: totalCost, r});
}
});
}
};
function checkCenters() {
const {cells, religions} = pack;
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 (cells.religion[r.center] === r.i) return; // in area
const religCells = cells.i.filter(i => cells.religion[i] === r.i);
if (!religCells.length) return; // extinct religion
r.center = religCells.sort((a, b) => cells.pop[b] - cells.pop[a])[0];
});
}
function updateCultures() { function updateCultures() {
TIME && console.time("updateCulturesForReligions"); TIME && console.time("updateCulturesForReligions");
pack.religions = pack.religions.map((religion, index) => { pack.religions = pack.religions.map((religion, index) => {
@ -891,5 +795,5 @@ window.Religions = (function () {
return type() + " of the " + generateMeaning(); return type() + " of the " + generateMeaning();
} }
return {generate, add, getDeityName, expandReligions, updateCultures}; return {generate, add, getDeityName, updateCultures};
})(); })();

View file

@ -210,7 +210,7 @@ function showMapTooltip(point, e, i, g) {
const r = pack.religions[religion]; const r = pack.religions[religion];
const type = r.type === "Cult" || r.type == "Heresy" ? r.type : r.type + " religion"; const type = r.type === "Cult" || r.type == "Heresy" ? r.type : r.type + " religion";
tip(type + ": " + r.name); tip(type + ": " + r.name);
if (religionsEditor?.offsetParent) highlightEditorLine(religionsEditor, religion); if (byId("religionsEditor")?.offsetParent) highlightEditorLine(religionsEditor, religion);
} else if (pack.cells.state[i] && (layerIsOn("toggleProvinces") || layerIsOn("toggleStates"))) { } else if (pack.cells.state[i] && (layerIsOn("toggleProvinces") || layerIsOn("toggleStates"))) {
const state = pack.cells.state[i]; const state = pack.cells.state[i];
const stateName = pack.states[state].fullName; const stateName = pack.states[state].fullName;