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 */ `
-
+
Religion 
Type 
Form 
Supreme Deity 
Area 
Believers 
+
Potential 
+
Expansion 
@@ -88,6 +90,11 @@ function insertEditorHtml() {
+ + + + +
`; @@ -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