refactor: migrate names-generator

This commit is contained in:
Marc Emmanuel 2026-01-25 22:14:50 +01:00
parent 29bc2832e0
commit a58fb7e2c0
5 changed files with 104 additions and 76 deletions

View file

@ -8494,7 +8494,6 @@
<script defer src="config/heightmap-templates.js"></script> <script defer src="config/heightmap-templates.js"></script>
<script defer src="config/precreated-heightmaps.js"></script> <script defer src="config/precreated-heightmaps.js"></script>
<script defer src="modules/ice.js?v=1.111.0"></script> <script defer src="modules/ice.js?v=1.111.0"></script>
<script defer src="modules/names-generator.js?v=1.106.0"></script>
<script defer src="modules/cultures-generator.js?v=1.106.0"></script> <script defer src="modules/cultures-generator.js?v=1.106.0"></script>
<script defer src="modules/burgs-generator.js?v=1.109.5"></script> <script defer src="modules/burgs-generator.js?v=1.109.5"></script>
<script defer src="modules/states-generator.js?v=1.107.0"></script> <script defer src="modules/states-generator.js?v=1.107.0"></script>

View file

@ -1,7 +1,8 @@
import "./voronoi"; import "./voronoi";
import "./heightmap-generator"; import "./heightmap-generator";
import "./features"; import "./features";
import "./lakes"; import "./names-generator"; // used by lakes and rivers
import "./ocean-layers"; import "./ocean-layers";
import "./lakes";
import "./river-generator"; import "./river-generator";
import "./biomes" import "./biomes"

View file

@ -1,14 +1,31 @@
"use strict"; import { capitalize, isVowel, last, P, ra, rand } from "../utils";
window.Names = (function () { declare global {
let chains = []; var Names: NamesGenerator;
}
// calculate Markov chain for a namesbase export interface NameBase {
const calculateChain = function (string) { name: string; // name of the base
const chain = []; i: number; // index of the base
const array = string.split(","); min: number; // minimum length of generated names
max: number; // maximum length of generated names
d: string; // letters allowed to duplicate
m: number; // multi-word name rate [deprecated]
b: string; // base string with names separated by comma
}
for (const n of array) { // Markov chain lookup table: key is a letter (or empty string for word start), value is array of possible next syllables
// Note: Uses array with string keys (sparse array) to match original JS behavior
type MarkovChain = string[][] & Record<string, string[]>;
class NamesGenerator {
chains: (MarkovChain | null)[] = []; // Markov chains for namebases
calculateChain(namesList: string): MarkovChain {
const chain: MarkovChain = [] as unknown as MarkovChain;
const availableNames = namesList.split(",");
for (const n of availableNames) {
let name = n.trim().toLowerCase(); let name = n.trim().toLowerCase();
const basic = !/[^\u0000-\u007f]/.test(name); // basic chars and English rules can be applied const basic = !/[^\u0000-\u007f]/.test(name); // basic chars and English rules can be applied
@ -24,7 +41,7 @@ window.Names = (function () {
if (syllable === " " || syllable === "-") break; // syllable starts with space or hyphen if (syllable === " " || syllable === "-") break; // syllable starts with space or hyphen
if (!next || next === " " || next === "-") break; // no need to check if (!next || next === " " || next === "-") break; // no need to check
if (vowel(that)) v = 1; // check if letter is vowel if (isVowel(that)) v = 1; // check if letter is vowel
// do not split some diphthongs // do not split some diphthongs
if (that === "y" && next === "e") continue; // 'ye' if (that === "y" && next === "e") continue; // 'ye'
@ -36,11 +53,11 @@ window.Names = (function () {
if (that === "c" && next === "h") continue; // 'ch' if (that === "c" && next === "h") continue; // 'ch'
} }
if (vowel(that) === next) break; // two same vowels in a row if (isVowel(that) === (next as unknown as boolean)) break; // two same vowels in a row (original quirky behavior)
if (v && vowel(name[c + 2])) break; // syllable has vowel and additional vowel is expected soon if (v && isVowel(name[c + 2])) break; // syllable has vowel and additional vowel is expected soon
} }
if (chain[prev] === undefined) chain[prev] = []; if (!chain[prev]) chain[prev] = [];
chain[prev].push(syllable); chain[prev].push(syllable);
} }
} }
@ -48,17 +65,20 @@ window.Names = (function () {
return chain; return chain;
}; };
const updateChain = i => { updateChain(index: number): void {
chains[i] = nameBases[i]?.b ? calculateChain(nameBases[i].b) : null; this.chains[index] = nameBases[index]?.b ? this.calculateChain(nameBases[index].b) : null;
}; };
const clearChains = () => { clearChains(): void {
chains = []; this.chains = [];
}; };
// generate name using Markov's chain // generate name using Markov's chain
const getBase = function (base, min, max, dupl) { getBase(base: number, min?: number, max?: number, dupl?: string): string {
if (base === undefined) return ERROR && console.error("Please define a base"); if (base === undefined) {
ERROR && console.error("Please define a base");
return "ERROR";
}
if (nameBases[base] === undefined) { if (nameBases[base] === undefined) {
if (nameBases[0]) { if (nameBases[0]) {
@ -70,9 +90,9 @@ window.Names = (function () {
} }
} }
if (!chains[base]) updateChain(base); if (!this.chains[base]) this.updateChain(base);
const data = chains[base]; const data = this.chains[base];
if (!data || data[""] === undefined) { if (!data || data[""] === undefined) {
tip("Namesbase " + base + " is incorrect. Please check in namesbase editor", false, "error"); tip("Namesbase " + base + " is incorrect. Please check in namesbase editor", false, "error");
ERROR && console.error("Namebase " + base + " is incorrect!"); ERROR && console.error("Namebase " + base + " is incorrect!");
@ -99,7 +119,7 @@ window.Names = (function () {
// word too long // word too long
if (w.length < min) w += cur; if (w.length < min) w += cur;
break; break;
} else v = data[last(cur)] || data[""]; } else v = data[last(cur.split("")) as string] || data[""];
} }
w += cur; w += cur;
@ -107,7 +127,7 @@ window.Names = (function () {
} }
// parse word to get a final name // parse word to get a final name
const l = last(w); // last letter const l = last(w.split("")); // last letter
if (l === "'" || l === " " || l === "-") w = w.slice(0, -1); // not allow some characters at the end if (l === "'" || l === " " || l === "-") w = w.slice(0, -1); // not allow some characters at the end
let name = [...w].reduce(function (r, c, i, d) { let name = [...w].reduce(function (r, c, i, d) {
@ -137,29 +157,57 @@ window.Names = (function () {
}; };
// generate name for culture // generate name for culture
const getCulture = function (culture, min, max, dupl) { getCulture(culture: number, min?: number, max?: number, dupl?: string): string {
if (culture === undefined) return ERROR && console.error("Please define a culture"); if (culture === undefined) {
ERROR && console.error("Please define a culture");
return "ERROR";
}
const base = pack.cultures[culture].base; const base = pack.cultures[culture].base;
return getBase(base, min, max, dupl); return this.getBase(base, min, max, dupl);
}; };
// generate short name for culture // generate short name for culture
const getCultureShort = function (culture) { getCultureShort(culture: number): string {
if (culture === undefined) return ERROR && console.error("Please define a culture"); if (culture === undefined) {
return getBaseShort(pack.cultures[culture].base); ERROR && console.error("Please define a culture");
return "ERROR";
}
return this.getBaseShort(pack.cultures[culture].base);
}; };
// generate short name for base // generate short name for base
const getBaseShort = function (base) { getBaseShort(base: number): string {
const min = nameBases[base] ? nameBases[base].min - 1 : null; const min = nameBases[base] ? nameBases[base].min - 1 : undefined;
const max = min ? Math.max(nameBases[base].max - 2, min) : null; const max = min ? Math.max(nameBases[base].max - 2, min) : undefined;
return getBase(base, min, max, "", 0); return this.getBase(base, min, max, "");
}; };
private validateSuffix(name: string, suffix: string): string {
if (name.slice(-1 * suffix.length) === suffix) return name; // no suffix if name already ends with it
const s1 = suffix.charAt(0);
if (name.slice(-1) === s1) name = name.slice(0, -1); // remove name last letter if it's a suffix first letter
if (isVowel(s1) === isVowel(name.slice(-1)) && isVowel(s1) === isVowel(name.slice(-2, -1))) name = name.slice(0, -1); // remove name last char if 2 last chars are the same type as suffix's 1st
if (name.slice(-1) === s1) name = name.slice(0, -1); // remove name last letter if it's a suffix first letter
return name + suffix;
};
private addSuffix(name: string): string {
const suffix = P(0.8) ? "ia" : "land";
if (suffix === "ia" && name.length > 6) name = name.slice(0, -(name.length - 3));
else if (suffix === "land" && name.length > 6) name = name.slice(0, -(name.length - 5));
return this.validateSuffix(name, suffix);
}
// generate state name based on capital or random name and culture-specific suffix // generate state name based on capital or random name and culture-specific suffix
const getState = function (name, culture, base) { getState(name: string, culture: number, base: number): string {
if (name === undefined) return ERROR && console.error("Please define a base name"); if (name === undefined) {
if (culture === undefined && base === undefined) return ERROR && console.error("Please define a culture"); ERROR && console.error("Please define a base name");
return "ERROR";
}
if (culture === undefined && base === undefined) {
ERROR && console.error("Please define a culture");
return "ERROR";
}
if (base === undefined) base = pack.cultures[culture].base; if (base === undefined) base = pack.cultures[culture].base;
// exclude endings inappropriate for states name // exclude endings inappropriate for states name
@ -169,17 +217,17 @@ window.Names = (function () {
if (base === 5 && ["sk", "ev", "ov"].includes(name.slice(-2))) name = name.slice(0, -2); if (base === 5 && ["sk", "ev", "ov"].includes(name.slice(-2))) name = name.slice(0, -2);
// remove -sk/-ev/-ov for Ruthenian // remove -sk/-ev/-ov for Ruthenian
else if (base === 12) return vowel(name.slice(-1)) ? name : name + "u"; else if (base === 12) return isVowel(name.slice(-1)) ? name : name + "u";
// Japanese ends on any vowel or -u // Japanese ends on any vowel or -u
else if (base === 18 && P(0.4)) else if (base === 18 && P(0.4))
name = vowel(name.slice(0, 1).toLowerCase()) ? "Al" + name.toLowerCase() : "Al " + name; // Arabic starts with -Al name = isVowel(name.slice(0, 1).toLowerCase()) ? "Al" + name.toLowerCase() : "Al " + name; // Arabic starts with -Al
// no suffix for fantasy bases // no suffix for fantasy bases
if (base > 32 && base < 42) return name; if (base > 32 && base < 42) return name;
// define if suffix should be used // define if suffix should be used
if (name.length > 3 && vowel(name.slice(-1))) { if (name.length > 3 && isVowel(name.slice(-1))) {
if (vowel(name.slice(-2, -1)) && P(0.85)) name = name.slice(0, -2); if (isVowel(name.slice(-2, -1)) && P(0.85)) name = name.slice(0, -2);
// 85% for vv // 85% for vv
else if (P(0.7)) name = name.slice(0, -1); else if (P(0.7)) name = name.slice(0, -1);
// ~60% for cv // ~60% for cv
@ -225,20 +273,11 @@ window.Names = (function () {
// Berber // Berber
else if (base === 18 && rnd < 0.8) suffix = "a"; // Arabic else if (base === 18 && rnd < 0.8) suffix = "a"; // Arabic
return validateSuffix(name, suffix); return this.validateSuffix(name, suffix);
}; };
function validateSuffix(name, suffix) {
if (name.slice(-1 * suffix.length) === suffix) return name; // no suffix if name already ends with it
const s1 = suffix.charAt(0);
if (name.slice(-1) === s1) name = name.slice(0, -1); // remove name last letter if it's a suffix first letter
if (vowel(s1) === vowel(name.slice(-1)) && vowel(s1) === vowel(name.slice(-2, -1))) name = name.slice(0, -1); // remove name last char if 2 last chars are the same type as suffix's 1st
if (name.slice(-1) === s1) name = name.slice(0, -1); // remove name last letter if it's a suffix first letter
return name + suffix;
}
// generato name for the map // generato name for the map
const getMapName = function (force) { getMapName(force: boolean) {
if (!force && locked("mapName")) return; if (!force && locked("mapName")) return;
if (force && locked("mapName")) unlock("mapName"); if (force && locked("mapName")) unlock("mapName");
const base = P(0.7) ? 2 : P(0.5) ? rand(0, 6) : rand(0, 31); const base = P(0.7) ? 2 : P(0.5) ? rand(0, 6) : rand(0, 31);
@ -248,19 +287,12 @@ window.Names = (function () {
} }
const min = nameBases[base].min - 1; const min = nameBases[base].min - 1;
const max = Math.max(nameBases[base].max - 3, min); const max = Math.max(nameBases[base].max - 3, min);
const baseName = getBase(base, min, max, "", 0); const baseName = this.getBase(base, min, max, "") as string;
const name = P(0.7) ? addSuffix(baseName) : baseName; const name = P(0.7) ? this.addSuffix(baseName) : baseName;
mapName.value = name; mapName.value = name;
}; };
function addSuffix(name) { getNameBases(): NameBase[] {
const suffix = P(0.8) ? "ia" : "land";
if (suffix === "ia" && name.length > 6) name = name.slice(0, -(name.length - 3));
else if (suffix === "land" && name.length > 6) name = name.slice(0, -(name.length - 5));
return validateSuffix(name, suffix);
}
const getNameBases = function () {
// name, min length, max length, letters to allow duplication, multi-word name rate [deprecated] // name, min length, max length, letters to allow duplication, multi-word name rate [deprecated]
// prettier-ignore // prettier-ignore
return [ return [
@ -312,17 +344,6 @@ window.Names = (function () {
{name: "Levantine", i: 42, min: 4, max: 12, d: "ankprs", m: 0, b: "Adme,Adramet,Agadir,Akko,Akzib,Alimas,Alis-Ubbo,Alqosh,Amid,Ammon,Ampi,Amurru,Andarig,Anpa,Araden,Aram,Arwad,Ashkelon,Athar,Atiq,Aza,Azeka,Baalbek,Babel,Batrun,Beerot,Beersheba,Beit Shemesh,Berytus,Bet Agus,Bet Anya,Beth-Horon,Bethel,Bethlehem,Bethuel,Bet Nahrin,Bet Nohadra,Bet Zalin,Birmula,Biruta,Bit Agushi,Bitan,Bit Zamani,Cerne,Dammeseq,Darmsuq,Dor,Eddial,Eden Ekron,Elah,Emek,Emun,Ephratah,Eyn Ganim,Finike,Gades,Galatia,Gaza,Gebal,Gedera,Gerizzim,Gethsemane,Gibeon,Gilead,Gilgal,Golgotha,Goshen,Gytte,Hagalil,Haifa,Halab,Haqel Dma,Har Habayit,Har Nevo,Har Pisga,Havilah,Hazor,Hebron,Hormah,Iboshim,Iriho,Irinem,Irridu,Israel,Kadesh,Kanaan,Kapara,Karaly,Kart-Hadasht,Keret Chadeshet,Kernah,Kesed,Keysariya,Kfar,Kfar Nahum,Khalibon,Khalpe,Khamat,Kiryat,Kittim,Kurda,Lapethos,Larna,Lepqis,Lepriptza,Liksos,Lod,Luv,Malaka,Malet,Marat,Megido,Melitta,Merdin,Metsada,Mishmarot,Mitzrayim,Moab,Mopsos,Motye,Mukish,Nampigi,Nampigu,Natzrat,Nimrud,Nineveh,Nob,Nuhadra,Oea,Ofir,Oyat,Phineka,Phoenicus,Pleshet,Qart-Tubah Sarepta,Qatna,Rabat Amon,Rakkath,Ramat Aviv,Ramitha,Ramta,Rehovot,Reshef,Rushadir,Rushakad,Samrin,Sefarad,Sehyon,Sepat,Sexi,Sharon,Shechem,Shefelat,Shfanim,Shiloh,Shmaya,Shomron,Sidon,Sinay,Sis,Solki,Sur,Suria,Tabetu,Tadmur,Tarshish,Tartus,Teberya,Tefessedt,Tekoa,Teyman,Tinga,Tipasa,Tsabratan,Tur Abdin,Tzarfat,Tziyon,Tzor,Ugarit,Unubaal,Ureshlem,Urhay,Urushalim,Vaga,Yaffa,Yamhad,Yam hamelach,Yam Kineret,Yamutbal,Yathrib,Yaudi,Yavne,Yehuda,Yerushalayim,Yev,Yevus,Yizreel,Yurdnan,Zarefat,Zeboim,Zeurta,Zeytim,Zikhron,Zmurna"} {name: "Levantine", i: 42, min: 4, max: 12, d: "ankprs", m: 0, b: "Adme,Adramet,Agadir,Akko,Akzib,Alimas,Alis-Ubbo,Alqosh,Amid,Ammon,Ampi,Amurru,Andarig,Anpa,Araden,Aram,Arwad,Ashkelon,Athar,Atiq,Aza,Azeka,Baalbek,Babel,Batrun,Beerot,Beersheba,Beit Shemesh,Berytus,Bet Agus,Bet Anya,Beth-Horon,Bethel,Bethlehem,Bethuel,Bet Nahrin,Bet Nohadra,Bet Zalin,Birmula,Biruta,Bit Agushi,Bitan,Bit Zamani,Cerne,Dammeseq,Darmsuq,Dor,Eddial,Eden Ekron,Elah,Emek,Emun,Ephratah,Eyn Ganim,Finike,Gades,Galatia,Gaza,Gebal,Gedera,Gerizzim,Gethsemane,Gibeon,Gilead,Gilgal,Golgotha,Goshen,Gytte,Hagalil,Haifa,Halab,Haqel Dma,Har Habayit,Har Nevo,Har Pisga,Havilah,Hazor,Hebron,Hormah,Iboshim,Iriho,Irinem,Irridu,Israel,Kadesh,Kanaan,Kapara,Karaly,Kart-Hadasht,Keret Chadeshet,Kernah,Kesed,Keysariya,Kfar,Kfar Nahum,Khalibon,Khalpe,Khamat,Kiryat,Kittim,Kurda,Lapethos,Larna,Lepqis,Lepriptza,Liksos,Lod,Luv,Malaka,Malet,Marat,Megido,Melitta,Merdin,Metsada,Mishmarot,Mitzrayim,Moab,Mopsos,Motye,Mukish,Nampigi,Nampigu,Natzrat,Nimrud,Nineveh,Nob,Nuhadra,Oea,Ofir,Oyat,Phineka,Phoenicus,Pleshet,Qart-Tubah Sarepta,Qatna,Rabat Amon,Rakkath,Ramat Aviv,Ramitha,Ramta,Rehovot,Reshef,Rushadir,Rushakad,Samrin,Sefarad,Sehyon,Sepat,Sexi,Sharon,Shechem,Shefelat,Shfanim,Shiloh,Shmaya,Shomron,Sidon,Sinay,Sis,Solki,Sur,Suria,Tabetu,Tadmur,Tarshish,Tartus,Teberya,Tefessedt,Tekoa,Teyman,Tinga,Tipasa,Tsabratan,Tur Abdin,Tzarfat,Tziyon,Tzor,Ugarit,Unubaal,Ureshlem,Urhay,Urushalim,Vaga,Yaffa,Yamhad,Yam hamelach,Yam Kineret,Yamutbal,Yathrib,Yaudi,Yavne,Yehuda,Yerushalayim,Yev,Yevus,Yizreel,Yurdnan,Zarefat,Zeboim,Zeurta,Zeytim,Zikhron,Zmurna"}
]; ];
}; };
}
return { window.Names = new NamesGenerator();
getBase,
getCulture,
getCultureShort,
getBaseShort,
getState,
updateChain,
clearChains,
getNameBases,
getMapName,
calculateChain
};
})();

View file

@ -33,4 +33,5 @@ export interface PackedGraph {
}; };
rivers: River[]; rivers: River[];
features: PackedGraphFeature[]; features: PackedGraphFeature[];
cultures: any[];
} }

View file

@ -1,5 +1,6 @@
import type { Selection } from 'd3'; import type { Selection } from 'd3';
import { PackedGraph } from "./PackedGraph"; import { PackedGraph } from "./PackedGraph";
import { NameBase } from '../modules/names-generator';
declare global { declare global {
var seed: string; var seed: string;
@ -13,10 +14,11 @@ declare global {
var ERROR: boolean; var ERROR: boolean;
var heightmapTemplates: any; var heightmapTemplates: any;
var Names: any; var nameBases: NameBase[];
var pointsInput: HTMLInputElement; var pointsInput: HTMLInputElement;
var heightExponentInput: HTMLInputElement; var heightExponentInput: HTMLInputElement;
var mapName: HTMLInputElement;
var rivers: Selection<SVGElement, unknown, null, undefined>; var rivers: Selection<SVGElement, unknown, null, undefined>;
var oceanLayers: Selection<SVGGElement, unknown, null, undefined>; var oceanLayers: Selection<SVGGElement, unknown, null, undefined>;
@ -30,4 +32,8 @@ declare global {
icons: string[][]; icons: string[][];
cost: number[]; cost: number[];
}; };
var tip: (message: string, autoHide?: boolean, type?: "info" | "warning" | "error") => void;
var locked: (settingId: string) => boolean;
var unlock: (settingId: string) => void;
} }