diff --git a/src/scripts/generation/pack/burgsAndStates/createTowns.ts b/src/scripts/generation/pack/burgsAndStates/createTowns.ts index c5e28243..a6b6dbf9 100644 --- a/src/scripts/generation/pack/burgsAndStates/createTowns.ts +++ b/src/scripts/generation/pack/burgsAndStates/createTowns.ts @@ -53,13 +53,13 @@ function placeTowns(townsNumber: number, scoredCellIds: UintArray, points: TPoin const townCells: number[] = []; 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) { const [x, y] = points[cellId]; // 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) { townCells.push(cellId); diff --git a/src/scripts/generation/pack/burgsAndStates/expandStates.ts b/src/scripts/generation/pack/burgsAndStates/expandStates.ts index 85630262..98f1237d 100644 --- a/src/scripts/generation/pack/burgsAndStates/expandStates.ts +++ b/src/scripts/generation/pack/burgsAndStates/expandStates.ts @@ -6,6 +6,7 @@ import {minmax} from "utils/numberUtils"; import type {createCapitals} from "./createCapitals"; import type {createStates} from "./createStates"; import {ELEVATION, FOREST_BIOMES, MIN_LAND_HEIGHT, DISTANCE_FIELD} from "config/generation"; +import {isState} from "utils/typeUtils"; type TCapitals = ReturnType; type TStates = ReturnType; @@ -104,13 +105,9 @@ export function expandStates( return normalizeStates(stateIds, capitalCells, cells.c, cells.h); - function isNeutrals(state: Entry): state is TNeutrals { - return state.i === 0; - } - function getState(stateId: number) { const state = states[stateId]; - if (isNeutrals(state)) throw new Error("Neutrals cannot expand"); + if (!isState(state)) throw new Error("Neutrals cannot expand"); return state; } diff --git a/src/scripts/generation/pack/cultures.ts b/src/scripts/generation/pack/cultures.ts index d26c197f..bf598782 100644 --- a/src/scripts/generation/pack/cultures.ts +++ b/src/scripts/generation/pack/cultures.ts @@ -18,6 +18,7 @@ import {minmax, rn} from "utils/numberUtils"; import {biased, P, rand} from "utils/probabilityUtils"; import {byId} from "utils/shorthands"; import {defaultNameBases} from "config/namebases"; +import {isCulture} from "utils/typeUtils"; const {COA} = window; @@ -284,9 +285,7 @@ export const expandCultures = function ( const cultureIds = new Uint16Array(cells.h.length); // cell cultures const queue = new FlatQueue<{cellId: number; cultureId: number}>(); - const isWilderness = (culture: ICulture | TWilderness): culture is TWilderness => culture.i === 0; - cultures.forEach(culture => { - if (isWilderness(culture) || culture.removed) return; + cultures.filter(isCulture).forEach(culture => { queue.push({cellId: culture.center, cultureId: culture.i}, 0); }); @@ -323,7 +322,7 @@ export const expandCultures = function ( function getCulture(cultureId: number) { 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; } diff --git a/src/scripts/generation/pack/generateReligions.ts b/src/scripts/generation/pack/generateReligions.ts deleted file mode 100644 index 7fd1414c..00000000 --- a/src/scripts/generation/pack/generateReligions.ts +++ /dev/null @@ -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; - -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"; -} diff --git a/src/scripts/generation/pack/pack.ts b/src/scripts/generation/pack/pack.ts index b6fd80d7..1746c41f 100644 --- a/src/scripts/generation/pack/pack.ts +++ b/src/scripts/generation/pack/pack.ts @@ -13,7 +13,7 @@ import {generateCultures, expandCultures} from "./cultures"; import {generateRivers} from "./rivers"; import {generateBurgsAndStates} from "./burgsAndStates/generateBurgsAndStates"; import {generateRoutes} from "./generateRoutes"; -import {generateReligions} from "./generateReligions"; +import {generateReligions} from "./religions/generateReligions"; const {LAND_COAST, WATER_COAST, DEEPER_WATER} = DISTANCE_FIELD; const {Biomes} = window; @@ -128,14 +128,24 @@ export function createPack(grid: IGrid): IPack { burg: burgIds }); - const {religionIds} = generateReligions(states, cultures, { - c: cells.c, - p: cells.p, - g: cells.g, - h: heights, - t: distanceField, - biome, - burg: burgIds + const {religionIds} = generateReligions({ + states, + cultures, + burgs, + cultureIds, + stateIds, + burgIds, + cells: { + i: cells.i, + c: cells.c, + p: cells.p, + g: cells.g, + h: heights, + t: distanceField, + biome, + pop: population, + burg: burgIds + } }); // Religions.generate(); diff --git a/src/scripts/generation/pack/religions/generateReligionName.ts b/src/scripts/generation/pack/religions/generateReligionName.ts new file mode 100644 index 00000000..5abe3e0d --- /dev/null +++ b/src/scripts/generation/pack/religions/generateReligionName.ts @@ -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}; +} diff --git a/src/scripts/generation/pack/religions/generateReligions.ts b/src/scripts/generation/pack/religions/generateReligions.ts new file mode 100644 index 00000000..8fea9270 --- /dev/null +++ b/src/scripts/generation/pack/religions/generateReligions.ts @@ -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; + +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, + cells: Pick +) { + 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]; +} diff --git a/src/utils/typeUtils.ts b/src/utils/typeUtils.ts new file mode 100644 index 00000000..ef1ebf1b --- /dev/null +++ b/src/utils/typeUtils.ts @@ -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;