"use strict"; window.Religions = (function () { // name generation approach and relative chance to be selected const approach = { Number: 1, Being: 3, Adjective: 5, "Color + Animal": 5, "Adjective + Animal": 5, "Adjective + Being": 5, "Adjective + Genitive": 1, "Color + Being": 3, "Color + Genitive": 3, "Being + of + Genitive": 2, "Being + of the + Genitive": 1, "Animal + of + Genitive": 1, "Adjective + Being + of + Genitive": 2, "Adjective + Animal + of + Genitive": 2 }; // turn weighted array into simple array const approaches = []; for (const a in approach) { for (let j = 0; j < approach[a]; j++) { approaches.push(a); } } const base = { number: ["One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve"], being: [ "Ancestor", "Ancient", "Avatar", "Brother", "Champion", "Chief", "Council", "Creator", "Deity", "Divine One", "Elder", "Enlightened Being", "Father", "Forebear", "Forefather", "Giver", "God", "Goddess", "Guardian", "Guide", "Hierach", "Lady", "Lord", "Maker", "Master", "Mother", "Numen", "Oracle", "Overlord", "Protector", "Reaper", "Ruler", "Sage", "Seer", "Sister", "Spirit", "Supreme Being", "Transcendent", "Virgin" ], animal: [ "Antelope", "Ape", "Badger", "Basilisk", "Bear", "Beaver", "Bison", "Boar", "Buffalo", "Camel", "Cat", "Centaur", "Cerberus", "Chimera", "Cobra", "Cockatrice", "Crane", "Crocodile", "Crow", "Cyclope", "Deer", "Dog", "Direwolf", "Drake", "Dragon", "Eagle", "Elephant", "Elk", "Falcon", "Fox", "Goat", "Goose", "Gorgon", "Gryphon", "Hare", "Hawk", "Heron", "Hippogriff", "Horse", "Hound", "Hyena", "Ibis", "Jackal", "Jaguar", "Kitsune", "Kraken", "Lark", "Leopard", "Lion", "Manticore", "Mantis", "Marten", "Minotaur", "Moose", "Mule", "Narwhal", "Owl", "Ox", "Panther", "Pegasus", "Phoenix", "Python", "Rat", "Raven", "Roc", "Rook", "Scorpion", "Serpent", "Shark", "Sheep", "Snake", "Sphinx", "Spider", "Swan", "Tiger", "Turtle", "Unicorn", "Viper", "Vulture", "Walrus", "Wolf", "Wolverine", "Worm", "Wyvern", "Yeti" ], adjective: [ "Aggressive", "Almighty", "Ancient", "Beautiful", "Benevolent", "Big", "Blind", "Blond", "Bloody", "Brave", "Broken", "Brutal", "Burning", "Calm", "Celestial", "Cheerful", "Crazy", "Cruel", "Dead", "Deadly", "Devastating", "Distant", "Disturbing", "Divine", "Dying", "Eternal", "Ethernal", "Empyreal", "Enigmatic", "Enlightened", "Evil", "Explicit", "Fair", "Far", "Fat", "Fatal", "Favorable", "Flying", "Friendly", "Frozen", "Giant", "Good", "Grateful", "Great", "Happy", "High", "Holy", "Honest", "Huge", "Hungry", "Illustrious", "Immutable", "Ineffable", "Infallible", "Inherent", "Last", "Latter", "Lost", "Loud", "Lucky", "Mad", "Magical", "Main", "Major", "Marine", "Mythical", "Mystical", "Naval", "New", "Noble", "Old", "Otherworldly", "Patient", "Peaceful", "Pregnant", "Prime", "Proud", "Pure", "Radiant", "Resplendent", "Sacred", "Sacrosanct", "Sad", "Scary", "Secret", "Selected", "Serene", "Severe", "Silent", "Sleeping", "Slumbering", "Sovereign", "Strong", "Sunny", "Superior", "Supernatural", "Sustainable", "Transcendent", "Transcendental", "Troubled", "Unearthly", "Unfathomable", "Unhappy", "Unknown", "Unseen", "Waking", "Wild", "Wise", "Worried", "Young" ], genitive: [ "Cold", "Day", "Death", "Doom", "Fate", "Fire", "Fog", "Frost", "Gates", "Heaven", "Home", "Ice", "Justice", "Life", "Light", "Lightning", "Love", "Nature", "Night", "Pain", "Snow", "Springs", "Summer", "Thunder", "Time", "Victory", "War", "Winter" ], theGenitive: [ "Abyss", "Blood", "Dawn", "Earth", "East", "Eclipse", "Fall", "Harvest", "Moon", "North", "Peak", "Rainbow", "Sea", "Sky", "South", "Stars", "Storm", "Sun", "Tree", "Underworld", "West", "Wild", "Word", "World" ], color: [ "Amber", "Black", "Blue", "Bright", "Bronze", "Brown", "Coral", "Crimson", "Dark", "Emerald", "Golden", "Green", "Grey", "Indigo", "Lavender", "Light", "Magenta", "Maroon", "Orange", "Pink", "Plum", "Purple", "Red", "Ruby", "Sapphire", "Teal", "Turquoise", "White", "Yellow" ] }; const forms = { Folk: { Shamanism: 4, Animism: 4, Polytheism: 4, "Ancestor Worship": 2, "Nature Worship": 1, Totemism: 1 }, Organized: { Polytheism: 7, Monotheism: 7, Dualism: 3, Pantheism: 2, "Non-theism": 2 }, Cult: { Cult: 5, "Dark Cult": 5, Sect: 1 }, Heresy: { Heresy: 1 } }; 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 = { Shamanism: {Beliefs: 3, Shamanism: 2, Druidism: 1, Spirits: 1}, Animism: {Spirits: 3, Beliefs: 1}, Polytheism: {Deities: 3, Faith: 1, Gods: 1, Pantheon: 1}, "Ancestor Worship": {Beliefs: 1, Forefathers: 2, Ancestors: 2}, "Nature Worship": {Beliefs: 3, Druids: 1}, Totemism: {Beliefs: 2, Totems: 2, Idols: 1}, Monotheism: {Religion: 2, Church: 3, Faith: 1}, Dualism: {Religion: 3, Faith: 1, Cult: 1}, Pantheism: {Religion: 1, Faith: 1}, "Non-theism": {Beliefs: 3, Spirits: 1}, Cult: {Cult: 4, Sect: 2, Arcanum: 1, Order: 1, Worship: 1}, "Dark Cult": {Cult: 2, Blasphemy: 1, Circle: 1, Coven: 1, Idols: 1, Occultism: 1}, Sect: {Sect: 3, Society: 1}, Heresy: { Heresy: 3, Sect: 2, Apostates: 1, Brotherhood: 1, Circle: 1, Dissent: 1, Dissenters: 1, Iconoclasm: 1, Schism: 1, Society: 1 } }; const expansionismMap = { Folk: () => 0, Organized: () => gauss(5, 3, 0, 10, 1), Cult: () => gauss(0.5, 0.5, 0, 5, 1), Heresy: () => gauss(1, 0.5, 0, 5, 1) }; function generate() { TIME && console.time("generateReligions"); const lockedReligions = pack.religions?.filter(r => r.i && r.lock && !r.removed) || []; const folkReligions = generateFolkReligions(); const organizedReligions = generateOrganizedReligions(+religionsNumber.value, lockedReligions); const namedReligions = specifyReligions([...folkReligions, ...organizedReligions]); const indexedReligions = combineReligions(namedReligions, lockedReligions); const religionIds = expandReligions(indexedReligions); const religions = defineOrigins(religionIds, indexedReligions); pack.religions = religions; pack.cells.religion = religionIds; checkCenters(); TIME && console.timeEnd("generateReligions"); } function generateFolkReligions() { return pack.cultures .filter(c => c.i && !c.removed) .map(culture => ({type: "Folk", form: rw(forms.Folk), culture: culture.i, center: culture.center})); } 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 []; const candidateCells = getCandidateCells(); const religionCores = placeReligions(); const cultsCount = Math.floor((rand(1, 4) / 10) * religionCores.length); // 10-40% const heresiesCount = Math.floor((rand(0, 3) / 10) * religionCores.length); // 0-30% 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; for (const cellId of candidateCells) { 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) return getRandomColor(); 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 indexedReligions = [{name: "No religion", i: 0}]; 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: "Removed religion", 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}, Cult: {clusterSize: 50, maxReligions: 3}, Heresy: {clusterSize: 50, maxReligions: 4} }; const origins = indexedReligions.map(({i, type, culture: cultureId, expansion, center}) => { if (i === 0) return null; // no religion if (type === "Folk") return [0]; // folk religions originate from its parent culture only const folkReligion = indexedReligions.find(({culture, type}) => type === "Folk" && culture === cultureId); const isFolkBased = folkReligion && cultureId && expansion === "culture" && each(2)(center); if (isFolkBased) return [folkReligion.i]; const {clusterSize, maxReligions} = religionOriginsParamsMap[type]; const fallbackOrigin = folkReligion?.i || 0; return getReligionsInRadius(pack.cells.c, center, religionIds, i, clusterSize, maxReligions, fallbackOrigin); }); return indexedReligions.map((religion, index) => ({...religion, origins: origins[index]})); } function getReligionsInRadius(neighbors, center, religionIds, religionId, clusterSize, maxReligions, fallbackOrigin) { 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); if (foundReligions.size >= maxReligions) return [...foundReligions]; queue.push(neibId); } } return foundReligions.size ? [...foundReligions] : [fallbackOrigin]; } // growth algorithm to assign cells to religions function expandReligions(religions) { const {cells, routes} = pack; const religionIds = spreadFolkReligions(religions); const queue = new FlatQueue(); const cost = []; // limit cost for organized religions growth const maxExpansionCost = (cells.i.length / 20) * byId("growthRate").valueAsNumber; religions .filter(r => r.i && !r.lock && r.type !== "Folk" && !r.removed) .forEach(r => { religionIds[r.center] = r.i; queue.push({e: r.center, p: 0, r: r.i, s: cells.state[r.center]}, 0); cost[r.center] = 1; }); const religionsMap = new Map(religions.map(r => [r.i, r])); while (queue.length) { const {e: cellId, p, r, s: state} = queue.pop(); 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(cellId, nextCell); 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.push({e: nextCell, p: totalCost, r, s: state}, totalCost); } }); } return religionIds; function getPassageCost(cellId, nextCellId) { const route = Routes.getRoute(cellId, nextCellId); if (isWater(cellId)) return route ? 50 : 500; const biomePassageCost = biomesData.cost[cells.biome[nextCellId]]; if (route) { if (route.group === "roads") return 1; return biomePassageCost / 3; // trails and other routes } return biomePassageCost; } } // 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 i = religions.length; 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, i, 25, 3, 0); const origins = type === "Folk" ? [0] : influences; religions.push({ i, name, color, culture: cultureId, type, form: formName, deity, expansion, expansionism: expansionismMap[type](), center, cells: 0, area: 0, rural: 0, urban: 0, origins, code }); cells.religion[center] = i; }; // get supreme deity name const getDeityName = function (culture) { if (culture === undefined) { ERROR && console.error("Please define a culture"); return; } const meaning = generateMeaning(); const cultureName = Names.getCulture(culture, null, null, "", 0.8); return cultureName + ", The " + meaning; }; function generateMeaning() { const a = ra(approaches); // select generation approach 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 === "Adjective + Being + of + Genitive") return `${ra(base.adjective)} ${ra(base.being)} of ${ra(base.genitive)}`; if (a === "Adjective + Animal + of + Genitive") return `${ra(base.adjective)} ${ra(base.animal)} of ${ra(base.genitive)}`; ERROR && console.error("Unkown generation approach"); } function generateReligionName(variety, form, deity, center) { const {cells, cultures, burgs, states} = pack; const random = () => Names.getCulture(cells.culture[center], null, null, "", 0); const type = rw(types[form]); const supreme = deity.split(/[ ,]+/)[0]; const culture = cultures[cells.culture[center]].name; const place = adj => { const burgId = cells.burg[center]; const stateId = cells.state[center]; const base = burgId ? burgs[burgId].name : states[stateId].name; let name = trimVowels(base.split(/[ ,]+/)[0]); return adj ? getAdjective(name) : name; }; const 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"]; if (m === "Faith of + Supreme" && deity) return [ra(["Faith", "Way", "Path", "Word", "Witnesses"]) + " of " + supreme, "global"]; if (m === "Place + ism") return [place() + "ism", "state"]; if (m === "Culture + ism") return [trimVowels(culture) + "ism", "culture"]; if (m === "Place + ian + type") return [place("adj") + " " + type, "state"]; 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 } return {generate, add, getDeityName, recalculate}; })();