diff --git a/modules/markers-generator.js b/modules/markers-generator.js index ac66cc630..b42ad2b33 100644 --- a/modules/markers-generator.js +++ b/modules/markers-generator.js @@ -106,7 +106,7 @@ window.Markers = (function () { let candidates = Array.from(list(pack)); let quantity = getQuantity(candidates, min, each, multiplier); // 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) { const [cell] = extractAnyElement(candidates); diff --git a/modules/religions-generator.js b/modules/religions-generator.js index d2ea9c0e3..d823b4c62 100644 --- a/modules/religions-generator.js +++ b/modules/religions-generator.js @@ -344,66 +344,15 @@ window.Religions = (function () { const generate = function () { TIME && console.time("generateReligions"); - const cells = pack.cells, - states = pack.states, - cultures = pack.cultures; - - // Keep a map of the previous religions per cell for referencing when we restore locked religions - const prevReligions = {}; - if (cells.religion) { - cells.religion.forEach((rId, index) => { - prevReligions[index] = rId; - }) - } - cells.religion = new Uint16Array(cells.culture); // cell religion; initially based on culture + const {cells, states, cultures} = pack; - 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 = []); + const religionIds = new Uint16Array(cells.culture); // cell religion; initially based on culture + const religions = []; // add folk religions pack.cultures.forEach(c => { - // If a restored religion exists for this culture, move it to this position - const existingFolkReligion = folkToRestore.find(r => r.culture === c.i); - 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"}); + const newId = c.i; + if (!newId) return religions.push({i: 0, name: "No religion"}); if (c.removed) { religions.push({ @@ -415,13 +364,38 @@ window.Religions = (function () { 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 name = c.name + " " + rw(types[form]); const deity = form === "Animism" ? null : getDeityName(c.i); const color = getMixedColor(c.color, 0.1, 0); // `url(#hatch${rand(8,13)})`; - religions.push({i: c.i, name, color, culture: c.i, type: "Folk", form, deity, center: c.center, 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) 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.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 available = [...sorted].filter(cellI => !restoredCells.includes(cellI)); + 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); @@ -440,39 +414,29 @@ window.Religions = (function () { function getReligionsInRadius({x, y, r, max}) { if (max === 0) return [0]; 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]; } - // Restore the origins of any organized religion that was locked - pack.religions.forEach(religion => { - // Ignore if the religion is not locked or not organized - if (religion.type !== "Organized" || !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; + // 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; + } - // Select a random folk religion for the religion - const culture = cells.culture[religion.center]; - const [x, y] = cells.p[religion.center]; - const isFolkBased = religion.expansion === "culture" || P(0.5); - 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]); - }); + 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 = 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 state = cells.state[center]; const culture = cells.culture[center]; @@ -518,30 +482,10 @@ window.Religions = (function () { 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 for (let i = 0; religions.length < count + cultsCount && i < 1000; i++) { 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])) center = cells.c[center].find(c => cells.burg[c]); const [x, y] = cells.p[center]; @@ -572,32 +516,7 @@ window.Religions = (function () { religionsTree.add([x, y]); } - expandReligions(restoredCells); - - // 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]); - }); + expandReligions(); // generate heresies religions @@ -606,9 +525,7 @@ window.Religions = (function () { if (r.expansionism < 3) return; const count = gauss(0, 1, 0, 3); for (let i = 0; i < count; i++) { - let center = ra( - cells.i.filter(i => cells.religion[i] === r.i && cells.c[i].some(c => cells.religion[c] !== r.i)) - ); + let center = ra(cells.i.filter(i => 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]); @@ -636,10 +553,111 @@ window.Religions = (function () { } }); - expandHeresies([...restoredCells, ...restoredHeresyCells]); + expandHeresies(); + 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) { @@ -690,120 +708,6 @@ window.Religions = (function () { 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() { TIME && console.time("updateCulturesForReligions"); pack.religions = pack.religions.map((religion, index) => { @@ -891,5 +795,5 @@ window.Religions = (function () { return type() + " of the " + generateMeaning(); } - return {generate, add, getDeityName, expandReligions, updateCultures}; + return {generate, add, getDeityName, updateCultures}; })(); diff --git a/modules/ui/general.js b/modules/ui/general.js index 238dd2f1a..62be2ab5f 100644 --- a/modules/ui/general.js +++ b/modules/ui/general.js @@ -210,7 +210,7 @@ function showMapTooltip(point, e, i, g) { const r = pack.religions[religion]; const type = r.type === "Cult" || r.type == "Heresy" ? r.type : r.type + " religion"; 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"))) { const state = pack.cells.state[i]; const stateName = pack.states[state].fullName;