Fantasy-Map-Generator/modules/religions-generator.js
2021-08-14 11:56:40 +03:00

660 lines
21 KiB
JavaScript

'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: [
'God',
'Goddess',
'Lord',
'Lady',
'Deity',
'Creator',
'Maker',
'Overlord',
'Ruler',
'Chief',
'Master',
'Spirit',
'Ancestor',
'Father',
'Forebear',
'Forefather',
'Mother',
'Brother',
'Sister',
'Elder',
'Numen',
'Ancient',
'Virgin',
'Giver',
'Council',
'Guardian',
'Reaper'
],
animal: [
'Dragon',
'Wyvern',
'Phoenix',
'Unicorn',
'Sphinx',
'Centaur',
'Pegasus',
'Kraken',
'Basilisk',
'Chimera',
'Cyclope',
'Antelope',
'Ape',
'Badger',
'Bear',
'Beaver',
'Bison',
'Boar',
'Buffalo',
'Cat',
'Cobra',
'Crane',
'Crocodile',
'Crow',
'Deer',
'Dog',
'Eagle',
'Elk',
'Fox',
'Goat',
'Goose',
'Hare',
'Hawk',
'Heron',
'Horse',
'Hyena',
'Ibis',
'Jackal',
'Jaguar',
'Lark',
'Leopard',
'Lion',
'Mantis',
'Marten',
'Moose',
'Mule',
'Narwhal',
'Owl',
'Panther',
'Rat',
'Raven',
'Rook',
'Scorpion',
'Shark',
'Sheep',
'Snake',
'Spider',
'Swan',
'Tiger',
'Turtle',
'Viper',
'Vulture',
'Walrus',
'Wolf',
'Wolverine',
'Worm',
'Camel',
'Falcon',
'Hound',
'Ox',
'Serpent'
],
adjective: [
'New',
'Good',
'High',
'Old',
'Great',
'Big',
'Young',
'Major',
'Strong',
'Happy',
'Last',
'Main',
'Huge',
'Far',
'Beautiful',
'Wild',
'Fair',
'Prime',
'Crazy',
'Ancient',
'Proud',
'Secret',
'Lucky',
'Sad',
'Silent',
'Latter',
'Severe',
'Fat',
'Holy',
'Pure',
'Aggressive',
'Honest',
'Giant',
'Mad',
'Pregnant',
'Distant',
'Lost',
'Broken',
'Blind',
'Friendly',
'Unknown',
'Sleeping',
'Slumbering',
'Loud',
'Hungry',
'Wise',
'Worried',
'Sacred',
'Magical',
'Superior',
'Patient',
'Dead',
'Deadly',
'Peaceful',
'Grateful',
'Frozen',
'Evil',
'Scary',
'Burning',
'Divine',
'Bloody',
'Dying',
'Waking',
'Brutal',
'Unhappy',
'Calm',
'Cruel',
'Favorable',
'Blond',
'Explicit',
'Disturbing',
'Devastating',
'Brave',
'Sunny',
'Troubled',
'Flying',
'Sustainable',
'Marine',
'Fatal',
'Inherent',
'Selected',
'Naval',
'Cheerful',
'Almighty',
'Benevolent',
'Eternal',
'Immutable',
'Infallible'
],
genitive: [
'Day',
'Life',
'Death',
'Night',
'Home',
'Fog',
'Snow',
'Winter',
'Summer',
'Cold',
'Springs',
'Gates',
'Nature',
'Thunder',
'Lightning',
'War',
'Ice',
'Frost',
'Fire',
'Doom',
'Fate',
'Pain',
'Heaven',
'Justice',
'Light',
'Love',
'Time',
'Victory'
],
theGenitive: [
'World',
'Word',
'South',
'West',
'North',
'East',
'Sun',
'Moon',
'Peak',
'Fall',
'Dawn',
'Eclipse',
'Abyss',
'Blood',
'Tree',
'Earth',
'Harvest',
'Rainbow',
'Sea',
'Sky',
'Stars',
'Storm',
'Underworld',
'Wild'
],
color: ['Dark', 'Light', 'Bright', 'Golden', 'White', 'Black', 'Red', 'Pink', 'Purple', 'Blue', 'Green', 'Yellow', 'Amber', 'Orange', 'Brown', 'Grey']
};
const forms = {
Folk: {Shamanism: 2, Animism: 2, 'Ancestor worship': 1, Polytheism: 2},
Organized: {Polytheism: 5, Dualism: 1, Monotheism: 4, 'Non-theism': 1},
Cult: {Cult: 1, 'Dark Cult': 1},
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 types = {
Shamanism: {Beliefs: 3, Shamanism: 2, Spirits: 1},
Animism: {Spirits: 1, Beliefs: 1},
'Ancestor worship': {Beliefs: 1, Forefathers: 2, Ancestors: 2},
Polytheism: {Deities: 3, Faith: 1, Gods: 1, Pantheon: 1},
Dualism: {Religion: 3, Faith: 1, Cult: 1},
Monotheism: {Religion: 1, Church: 1},
'Non-theism': {Beliefs: 3, Spirits: 1},
Cult: {Cult: 4, Sect: 4, Worship: 1, Orden: 1, Coterie: 1, Arcanum: 1},
'Dark Cult': {Cult: 2, Sect: 2, Occultism: 1, Idols: 1, Coven: 1, Circle: 1, Blasphemy: 1},
Heresy: {Heresy: 3, Sect: 2, Schism: 1, Dissenters: 1, Circle: 1, Brotherhood: 1, Society: 1, Iconoclasm: 1, Dissent: 1, Apostates: 1}
};
const generate = function () {
TIME && console.time('generateReligions');
const cells = pack.cells,
states = pack.states,
cultures = pack.cultures;
const religions = (pack.religions = []);
cells.religion = new Uint16Array(cells.culture); // cell religion; initially based on culture
// add folk religions
pack.cultures.forEach((c) => {
if (!c.i) {
religions.push({i: 0, name: 'No religion'});
return;
}
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 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, origin: 0});
});
if (religionsInput.value == 0 || pack.cultures.length < 2) {
religions.filter((r) => r.i).forEach((r) => (r.code = abbreviate(r.name)));
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;
// 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 = cells.p[center][0],
y = cells.p[center][1];
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 folk = 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 origin = folk ? folk.i : 0;
const expansionism = rand(3, 8);
const color = getMixedColor(religions[origin].color, 0.3, 0); // `url(#hatch${rand(0,5)})`;
religions.push({i: religions.length, name, color, culture, type: 'Organized', form, deity, expansion, expansionism, center, origin});
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 = cells.p[center][0],
y = cells.p[center][1];
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 folk = religions.find((r) => r.culture === culture && r.type === 'Folk');
const origin = folk ? folk.i : 0;
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, origin});
religionsTree.add([x, y]);
//debug.append("circle").attr("cx", x).attr("cy", y).attr("r", 2).attr("fill", "red");
}
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) => cells.religion[i] === r.i && cells.c[i].some((c) => cells.religion[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 = cells.p[center][0],
y = cells.p[center][1];
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, origin: r.i});
religionsTree.add([x, y]);
//debug.append("circle").attr("cx", x).attr("cy", y).attr("r", 2).attr("fill", "green");
}
});
expandHeresies();
checkCenters();
TIME && console.timeEnd('generateReligions');
};
const add = function (center) {
const cells = pack.cells,
religions = pack.religions;
const r = cells.religion[center];
const i = religions.length;
const culture = cells.culture[center];
const color = getMixedColor(religions[r].color, 0.3, 0);
const type = religions[r].type === 'Organized' ? rw({Organized: 4, Cult: 1, Heresy: 2}) : rw({Organized: 5, Cult: 2});
const form = rw(forms[type]);
const deity = type === 'Heresy' ? religions[r].deity : form === 'Non-theism' ? null : getDeityName(culture);
let name, expansion;
if (type === 'Organized') [name, expansion] = getReligionName(form, deity, center);
else {
name = getCultName(form, center);
expansion = 'global';
}
const formName = type === 'Heresy' ? religions[r].form : form;
const code = abbreviate(
name,
religions.map((r) => r.code)
);
religions.push({i, name, color, culture, type, form: formName, deity, expansion, expansionism: 0, center, cells: 0, area: 0, rural: 0, urban: 0, origin: r, code});
cells.religion[center] = i;
};
// growth algorithm to assign cells to religions
const expandReligions = function () {
const cells = pack.cells,
religions = pack.religions;
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const cost = [];
religions
.filter((r) => 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;
const cultureCost = c !== cells.culture[e] ? 10 : 0;
const stateCost = s !== cells.state[e] ? 10 : 0;
const biomeCost = 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 ? 500 : 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 () {
const cells = pack.cells,
religions = pack.religions;
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const cost = [];
religions
.filter((r) => 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) {
const religionCost = cells.religion[e] === b ? 0 : 2000;
const biomeCost = biomesData.cost[cells.biome[e]];
const heightCost = Math.max(cells.h[e], 20) - 20;
const waterCost = cells.h[e] < 20 ? 500 : 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 = pack.cells,
religions = pack.religions;
const codes = religions.map((r) => r.code);
religions
.filter((r) => r.i)
.forEach((r) => {
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) => b.pop - a.pop)[0];
});
}
function updateCultures() {
TIME && console.time('updateCulturesForReligions');
pack.religions = pack.religions.map((religion, index) => {
if (index === 0) {
return religion;
}
return {...religion, culture: pack.cells.culture[religion.center]};
});
TIME && console.timeEnd('updateCulturesForReligions');
}
// 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);
}
function getReligionName(form, deity, center) {
const cells = pack.cells;
const random = function () {
return Names.getCulture(cells.culture[center], null, null, '', 0);
};
const type = function () {
return rw(types[form]);
};
const supreme = function () {
return deity.split(/[ ,]+/)[0];
};
const place = function (adj) {
const base = cells.burg[center] ? pack.burgs[cells.burg[center]].name : pack.states[cells.state[center]].name;
let name = trimVowels(base.split(/[ ,]+/)[0]);
return adj ? getAdjective(name) : name;
};
const culture = function () {
return pack.cultures[cells.culture[center]].name;
};
const m = rw(methods);
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'];
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, expandReligions, updateCultures};
})();