refactor: generate organized religions

This commit is contained in:
max 2022-08-25 00:01:42 +03:00
parent 70e8296c48
commit cce374da24
8 changed files with 342 additions and 90 deletions

View file

@ -53,13 +53,13 @@ function placeTowns(townsNumber: number, scoredCellIds: UintArray, points: TPoin
const townCells: number[] = []; const townCells: number[] = [];
const townsQuadtree = d3.quadtree(); const townsQuadtree = d3.quadtree();
const randomizeScaping = (spacing: number) => spacing * gauss(1, 0.3, 0.2, 2, 2); const randomizeSpacing = (spacing: number) => spacing * gauss(1, 0.3, 0.2, 2, 2);
for (const cellId of scoredCellIds) { for (const cellId of scoredCellIds) {
const [x, y] = points[cellId]; const [x, y] = points[cellId];
// randomize min spacing a bit to make placement not that uniform // randomize min spacing a bit to make placement not that uniform
const currentSpacing = randomizeScaping(spacing); const currentSpacing = randomizeSpacing(spacing);
if (townsQuadtree.find(x, y, currentSpacing) === undefined) { if (townsQuadtree.find(x, y, currentSpacing) === undefined) {
townCells.push(cellId); townCells.push(cellId);

View file

@ -6,6 +6,7 @@ import {minmax} from "utils/numberUtils";
import type {createCapitals} from "./createCapitals"; import type {createCapitals} from "./createCapitals";
import type {createStates} from "./createStates"; import type {createStates} from "./createStates";
import {ELEVATION, FOREST_BIOMES, MIN_LAND_HEIGHT, DISTANCE_FIELD} from "config/generation"; import {ELEVATION, FOREST_BIOMES, MIN_LAND_HEIGHT, DISTANCE_FIELD} from "config/generation";
import {isState} from "utils/typeUtils";
type TCapitals = ReturnType<typeof createCapitals>; type TCapitals = ReturnType<typeof createCapitals>;
type TStates = ReturnType<typeof createStates>; type TStates = ReturnType<typeof createStates>;
@ -104,13 +105,9 @@ export function expandStates(
return normalizeStates(stateIds, capitalCells, cells.c, cells.h); return normalizeStates(stateIds, capitalCells, cells.c, cells.h);
function isNeutrals(state: Entry<TStates>): state is TNeutrals {
return state.i === 0;
}
function getState(stateId: number) { function getState(stateId: number) {
const state = states[stateId]; const state = states[stateId];
if (isNeutrals(state)) throw new Error("Neutrals cannot expand"); if (!isState(state)) throw new Error("Neutrals cannot expand");
return state; return state;
} }

View file

@ -18,6 +18,7 @@ import {minmax, rn} from "utils/numberUtils";
import {biased, P, rand} from "utils/probabilityUtils"; import {biased, P, rand} from "utils/probabilityUtils";
import {byId} from "utils/shorthands"; import {byId} from "utils/shorthands";
import {defaultNameBases} from "config/namebases"; import {defaultNameBases} from "config/namebases";
import {isCulture} from "utils/typeUtils";
const {COA} = window; const {COA} = window;
@ -284,9 +285,7 @@ export const expandCultures = function (
const cultureIds = new Uint16Array(cells.h.length); // cell cultures const cultureIds = new Uint16Array(cells.h.length); // cell cultures
const queue = new FlatQueue<{cellId: number; cultureId: number}>(); const queue = new FlatQueue<{cellId: number; cultureId: number}>();
const isWilderness = (culture: ICulture | TWilderness): culture is TWilderness => culture.i === 0; cultures.filter(isCulture).forEach(culture => {
cultures.forEach(culture => {
if (isWilderness(culture) || culture.removed) return;
queue.push({cellId: culture.center, cultureId: culture.i}, 0); queue.push({cellId: culture.center, cultureId: culture.i}, 0);
}); });
@ -323,7 +322,7 @@ export const expandCultures = function (
function getCulture(cultureId: number) { function getCulture(cultureId: number) {
const culture = cultures[cultureId]; const culture = cultures[cultureId];
if (isWilderness(culture)) throw new Error("Wilderness culture cannot expand"); if (!isCulture(culture)) throw new Error("Wilderness cannot expand");
return culture; return culture;
} }

View file

@ -1,70 +0,0 @@
import {TIME} from "config/logging";
import {religionsData} from "config/religionsData";
import {getMixedColor} from "utils/colorUtils";
import {ra, rw} from "utils/probabilityUtils";
type TCellsData = Pick<IPack["cells"], "c" | "p" | "g" | "h" | "t" | "biome" | "burg">;
const {Names} = window;
const {approaches, base, forms, methods, types} = religionsData;
export function generateReligions(states: TStates, cultures: TCultures, cells: TCellsData) {
TIME && console.time("generateReligions");
const religionIds = new Uint16Array(cells.c.length);
const folkReligions = generateFolkReligions(cultures, cells);
console.log(folkReligions);
TIME && console.timeEnd("generateReligions");
return {religionIds};
}
function generateFolkReligions(cultures: TCultures, cells: TCellsData) {
const isValidCulture = (culture: TWilderness | ICulture): culture is ICulture =>
culture.i !== 0 && !(culture as ICulture).removed;
return cultures.filter(isValidCulture).map((culture, index) => {
const {i: cultureId, name: cultureName, center} = culture;
const id = index + 1;
const form = rw(forms.Folk);
const type: {[key: string]: number} = types[form];
const name = cultureName + " " + rw(type);
const deity = form === "Animism" ? null : getDeityName(cultures, cultureId);
const color = getMixedColor(culture.color, 0.1, 0);
return {i: id, name, color, culture: cultureId, type: "Folk", form, deity, center: center, origins: [0]};
});
}
function getDeityName(cultures: TCultures, cultureId: number) {
if (cultureId === undefined) throw "CultureId is undefined";
const meaning = generateMeaning();
const base = cultures[cultureId].base;
const cultureName = Names.getBase(base);
return cultureName + ", The " + meaning;
}
function generateMeaning() {
const approach = ra(approaches);
if (approach === "Number") return ra(base.number);
if (approach === "Being") return ra(base.being);
if (approach === "Adjective") return ra(base.adjective);
if (approach === "Color + Animal") return `${ra(base.color)} ${ra(base.animal)}`;
if (approach === "Adjective + Animal") return `${ra(base.adjective)} ${ra(base.animal)}`;
if (approach === "Adjective + Being") return `${ra(base.adjective)} ${ra(base.being)}`;
if (approach === "Adjective + Genitive") return `${ra(base.adjective)} ${ra(base.genitive)}`;
if (approach === "Color + Being") return `${ra(base.color)} ${ra(base.being)}`;
if (approach === "Color + Genitive") return `${ra(base.color)} ${ra(base.genitive)}`;
if (approach === "Being + of + Genitive") return `${ra(base.being)} of ${ra(base.genitive)}`;
if (approach === "Being + of the + Genitive") return `${ra(base.being)} of the ${ra(base.theGenitive)}`;
if (approach === "Animal + of + Genitive") return `${ra(base.animal)} of ${ra(base.genitive)}`;
if (approach === "Adjective + Being + of + Genitive")
return `${ra(base.adjective)} ${ra(base.being)} of ${ra(base.genitive)}`;
if (approach === "Adjective + Animal + of + Genitive")
return `${ra(base.adjective)} ${ra(base.animal)} of ${ra(base.genitive)}`;
throw "Unknown generation approach";
}

View file

@ -13,7 +13,7 @@ import {generateCultures, expandCultures} from "./cultures";
import {generateRivers} from "./rivers"; import {generateRivers} from "./rivers";
import {generateBurgsAndStates} from "./burgsAndStates/generateBurgsAndStates"; import {generateBurgsAndStates} from "./burgsAndStates/generateBurgsAndStates";
import {generateRoutes} from "./generateRoutes"; import {generateRoutes} from "./generateRoutes";
import {generateReligions} from "./generateReligions"; import {generateReligions} from "./religions/generateReligions";
const {LAND_COAST, WATER_COAST, DEEPER_WATER} = DISTANCE_FIELD; const {LAND_COAST, WATER_COAST, DEEPER_WATER} = DISTANCE_FIELD;
const {Biomes} = window; const {Biomes} = window;
@ -128,14 +128,24 @@ export function createPack(grid: IGrid): IPack {
burg: burgIds burg: burgIds
}); });
const {religionIds} = generateReligions(states, cultures, { const {religionIds} = generateReligions({
states,
cultures,
burgs,
cultureIds,
stateIds,
burgIds,
cells: {
i: cells.i,
c: cells.c, c: cells.c,
p: cells.p, p: cells.p,
g: cells.g, g: cells.g,
h: heights, h: heights,
t: distanceField, t: distanceField,
biome, biome,
pop: population,
burg: burgIds burg: burgIds
}
}); });
// Religions.generate(); // Religions.generate();

View file

@ -0,0 +1,112 @@
import {religionsData} from "config/religionsData";
import {trimVowels, getAdjective} from "utils/languageUtils";
import {rw, ra} from "utils/probabilityUtils";
import {isCulture, isState} from "utils/typeUtils";
const {Names} = window;
const {methods, types} = religionsData;
interface IContext {
cultureId: number;
stateId: number;
burgId: number;
cultures: TCultures;
states: TStates;
burgs: TBurgs;
center: number;
form: keyof typeof types;
deity: string;
}
const context = {
data: {} as IContext,
// data setter
set current(data: IContext) {
this.data = data;
},
// data getters
get culture() {
return this.data.cultures[this.data.cultureId];
},
get state() {
return this.data.states[this.data.stateId];
},
get burg() {
return this.data.burgs[this.data.burgId];
},
get form() {
return this.data.form;
},
get deity() {
return this.data.deity;
},
// generation methods
get random() {
return Names.getBase(this.culture.base);
},
get type() {
return rw(types[this.form] as {[key: string]: number});
},
get supreme() {
return this.deity.split(/[ ,]+/)[0];
},
get cultureName() {
return this.culture.name;
},
get place() {
const base = this.burg.name || this.state.name;
return trimVowels(base.split(/[ ,]+/)[0]);
}
};
const nameMethodsMap = {
"Random + type": {getName: () => `${context.random} ${context.type}`, expansion: "global"},
"Random + ism": {getName: () => `${trimVowels(context.random)}ism`, expansion: "global"},
"Supreme + ism": {getName: () => `${trimVowels(context.supreme)}ism`, expansion: "global"},
"Faith of + Supreme": {
getName: () => `${ra(["Faith", "Way", "Path", "Word", "Witnesses"])} of ${context.supreme}`,
expansion: "global"
},
"Place + ism": {getName: () => `${context.place}ism`, expansion: "state"},
"Culture + ism": {getName: () => `${trimVowels(context.cultureName)}ism`, expansion: "culture"},
"Place + ian + type": {
getName: () => `${getAdjective(context.place)} ${context.type}`,
expansion: "state"
},
"Culture + type": {getName: () => `${context.cultureName} ${context.type}`, expansion: "culture"}
};
export function generateReligionName(data: IContext) {
context.current = data;
const {stateId, cultureId, states, cultures, center} = data;
const method = nameMethodsMap[rw(methods)];
const name = method.getName();
let expansion = method.expansion;
if (expansion === "state" && !stateId) expansion = "global";
if (expansion === "culture" && !cultureId) expansion = "global";
if (expansion === "state" && Math.random() > 0.5) {
const state = states[stateId];
if (isState(state)) return {name, expansion, center: state.center};
}
if (expansion === "culture" && Math.random() > 0.5) {
const culture = cultures[cultureId];
if (isCulture(culture)) return {name, expansion, center: culture.center};
}
return {name, expansion, center};
}

View file

@ -0,0 +1,198 @@
import * as d3 from "d3";
import {TIME, WARN} from "config/logging";
import {religionsData} from "config/religionsData";
import {unique} from "utils/arrayUtils";
import {getMixedColor, getRandomColor} from "utils/colorUtils";
import {findAll} from "utils/graphUtils";
import {getInputNumber} from "utils/nodeUtils";
import {ra, rand, rw} from "utils/probabilityUtils";
import {isBurg} from "utils/typeUtils";
import {generateReligionName} from "./generateReligionName";
const {Names} = window;
const {approaches, base, forms, types} = religionsData;
type TCellsData = Pick<IPack["cells"], "i" | "c" | "p" | "g" | "h" | "t" | "biome" | "pop" | "burg">;
export function generateReligions({
states,
cultures,
burgs,
cultureIds,
stateIds,
burgIds,
cells
}: {
states: TStates;
cultures: TCultures;
burgs: TBurgs;
cultureIds: Uint16Array;
stateIds: Uint16Array;
burgIds: Uint16Array;
cells: TCellsData;
}) {
TIME && console.time("generateReligions");
const religionIds = new Uint16Array(cells.c.length);
const folkReligions = generateFolkReligions(cultures);
const basicReligions = generateOrganizedReligionsAndCults(
states,
cultures,
burgs,
cultureIds,
stateIds,
burgIds,
folkReligions,
{
i: cells.i,
p: cells.p,
pop: cells.pop
}
);
console.log(folkReligions, basicReligions);
TIME && console.timeEnd("generateReligions");
return {religionIds};
}
function generateFolkReligions(cultures: TCultures) {
const isValidCulture = (culture: TWilderness | ICulture): culture is ICulture =>
culture.i !== 0 && !(culture as ICulture).removed;
return cultures.filter(isValidCulture).map(culture => {
const {i: cultureId, name: cultureName, center} = culture;
const form = rw(forms.Folk);
const type: {[key: string]: number} = types[form];
const name = cultureName + " " + rw(type);
const deity = form === "Animism" ? null : getDeityName(cultures, cultureId);
const color = getMixedColor(culture.color, 0.1, 0);
return {name, type: "Folk", form, deity, color, culture: cultureId, center, origins: [0]};
});
}
function generateOrganizedReligionsAndCults(
states: TStates,
cultures: TCultures,
burgs: TBurgs,
cultureIds: Uint16Array,
stateIds: Uint16Array,
burgIds: Uint16Array,
folkReligions: ReturnType<typeof generateFolkReligions>,
cells: Pick<IPack["cells"], "i" | "p" | "pop">
) {
const religionsNumber = getInputNumber("religionsInput");
if (religionsNumber === 0) return [];
const cultsNumber = Math.floor((rand(1, 4) / 10) * religionsNumber); // 10-40%
const organizedNumber = religionsNumber - cultsNumber;
const canditateCells = getCandidateCells();
const religionCells = placeReligions();
return religionCells.map((cellId, index) => {
const cultureId = cultureIds[cellId];
const stateId = stateIds[cellId];
const burgId = burgIds[cellId];
const type = index < organizedNumber ? "Organized" : "Cult";
const form = rw(forms[type] as {[key in keyof typeof types]: number});
const deityName = getDeityName(cultures, cultureId);
const deity = form === "Non-theism" ? null : deityName;
const {name, expansion, center} = generateReligionName({
cultureId,
stateId,
burgId,
cultures,
states,
burgs,
center: cellId,
form,
deity: deityName
});
const folkReligion = folkReligions.find(({culture}) => culture === cultureId);
const baseColor = folkReligion?.color || getRandomColor();
const color = getMixedColor(baseColor, 0.3, 0);
return {name, type, form, deity, color, culture: cultureId, center, expansion};
});
function placeReligions() {
const religionCells = [];
const religionsTree = d3.quadtree();
// initial min distance between religions
let spacing = (graphWidth + graphHeight) / 4 / religionsNumber;
for (const cellId of canditateCells) {
const [x, y] = cells.p[cellId];
if (religionsTree.find(x, y, spacing) === undefined) {
religionCells.push(cellId);
religionsTree.add([x, y]);
if (religionCells.length === religionsNumber) return religionCells;
}
}
WARN && console.warn(`Placed only ${religionCells.length} of ${religionsNumber} religions`);
return religionCells;
}
function getCandidateCells() {
const validBurgs = burgs.filter(isBurg);
if (validBurgs.length >= religionsNumber)
return validBurgs.sort((a, b) => b.population - a.population).map(burg => burg.cell);
return cells.i.filter(i => cells.pop[i] > 2).sort((a, b) => cells.pop[b] - cells.pop[a]);
}
}
function getDeityName(cultures: TCultures, cultureId: number) {
if (cultureId === undefined) throw "CultureId is undefined";
const meaning = generateMeaning();
const base = cultures[cultureId].base;
const cultureName = Names.getBase(base);
return cultureName + ", The " + meaning;
}
function generateMeaning() {
const approach = ra(approaches);
if (approach === "Number") return ra(base.number);
if (approach === "Being") return ra(base.being);
if (approach === "Adjective") return ra(base.adjective);
if (approach === "Color + Animal") return `${ra(base.color)} ${ra(base.animal)}`;
if (approach === "Adjective + Animal") return `${ra(base.adjective)} ${ra(base.animal)}`;
if (approach === "Adjective + Being") return `${ra(base.adjective)} ${ra(base.being)}`;
if (approach === "Adjective + Genitive") return `${ra(base.adjective)} ${ra(base.genitive)}`;
if (approach === "Color + Being") return `${ra(base.color)} ${ra(base.being)}`;
if (approach === "Color + Genitive") return `${ra(base.color)} ${ra(base.genitive)}`;
if (approach === "Being + of + Genitive") return `${ra(base.being)} of ${ra(base.genitive)}`;
if (approach === "Being + of the + Genitive") return `${ra(base.being)} of the ${ra(base.theGenitive)}`;
if (approach === "Animal + of + Genitive") return `${ra(base.animal)} of ${ra(base.genitive)}`;
if (approach === "Adjective + Being + of + Genitive")
return `${ra(base.adjective)} ${ra(base.being)} of ${ra(base.genitive)}`;
if (approach === "Adjective + Animal + of + Genitive")
return `${ra(base.adjective)} ${ra(base.animal)} of ${ra(base.genitive)}`;
throw "Unknown generation approach";
}
function getReligionsInRadius(
religionIds: Uint16Array,
{x, y, r, max}: {x: number; y: number; r: number; max: number}
) {
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];
}

6
src/utils/typeUtils.ts Normal file
View file

@ -0,0 +1,6 @@
export const isState = (state: TNeutrals | IState): state is IState => state.i !== 0 && !(state as IState).removed;
export const isCulture = (culture: TWilderness | ICulture): culture is ICulture =>
culture.i !== 0 && !(culture as ICulture).removed;
export const isBurg = (burg: TNoBurg | IBurg): burg is IBurg => burg.i !== 0 && !(burg as IBurg).removed;