diff --git a/src/index.html b/src/index.html
index d14cea96..d0c66986 100644
--- a/src/index.html
+++ b/src/index.html
@@ -8494,7 +8494,6 @@
-
diff --git a/src/modules/index.ts b/src/modules/index.ts
index 41beaabd..32b52cde 100644
--- a/src/modules/index.ts
+++ b/src/modules/index.ts
@@ -1,7 +1,8 @@
import "./voronoi";
import "./heightmap-generator";
import "./features";
-import "./lakes";
+import "./names-generator"; // used by lakes and rivers
import "./ocean-layers";
+import "./lakes";
import "./river-generator";
import "./biomes"
diff --git a/public/modules/names-generator.js b/src/modules/names-generator.ts
similarity index 95%
rename from public/modules/names-generator.js
rename to src/modules/names-generator.ts
index c35afedc..3bd0847b 100644
--- a/public/modules/names-generator.js
+++ b/src/modules/names-generator.ts
@@ -1,14 +1,31 @@
-"use strict";
+import { capitalize, isVowel, last, P, ra, rand } from "../utils";
-window.Names = (function () {
- let chains = [];
+declare global {
+ var Names: NamesGenerator;
+}
- // calculate Markov chain for a namesbase
- const calculateChain = function (string) {
- const chain = [];
- const array = string.split(",");
+export interface NameBase {
+ name: string; // name of the base
+ i: number; // index of the base
+ 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;
+
+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();
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 (!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
if (that === "y" && next === "e") continue; // 'ye'
@@ -36,11 +53,11 @@ window.Names = (function () {
if (that === "c" && next === "h") continue; // 'ch'
}
- if (vowel(that) === next) break; // two same vowels in a row
- if (v && vowel(name[c + 2])) break; // syllable has vowel and additional vowel is expected soon
+ if (isVowel(that) === (next as unknown as boolean)) break; // two same vowels in a row (original quirky behavior)
+ 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);
}
}
@@ -48,17 +65,20 @@ window.Names = (function () {
return chain;
};
- const updateChain = i => {
- chains[i] = nameBases[i]?.b ? calculateChain(nameBases[i].b) : null;
+ updateChain(index: number): void {
+ this.chains[index] = nameBases[index]?.b ? this.calculateChain(nameBases[index].b) : null;
};
- const clearChains = () => {
- chains = [];
+ clearChains(): void {
+ this.chains = [];
};
// generate name using Markov's chain
- const getBase = function (base, min, max, dupl) {
- if (base === undefined) return ERROR && console.error("Please define a base");
+ getBase(base: number, min?: number, max?: number, dupl?: string): string {
+ if (base === undefined) {
+ ERROR && console.error("Please define a base");
+ return "ERROR";
+ }
if (nameBases[base] === undefined) {
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) {
tip("Namesbase " + base + " is incorrect. Please check in namesbase editor", false, "error");
ERROR && console.error("Namebase " + base + " is incorrect!");
@@ -99,7 +119,7 @@ window.Names = (function () {
// word too long
if (w.length < min) w += cur;
break;
- } else v = data[last(cur)] || data[""];
+ } else v = data[last(cur.split("")) as string] || data[""];
}
w += cur;
@@ -107,7 +127,7 @@ window.Names = (function () {
}
// 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
let name = [...w].reduce(function (r, c, i, d) {
@@ -137,29 +157,57 @@ window.Names = (function () {
};
// generate name for culture
- const getCulture = function (culture, min, max, dupl) {
- if (culture === undefined) return ERROR && console.error("Please define a culture");
+ getCulture(culture: number, min?: number, max?: number, dupl?: string): string {
+ if (culture === undefined) {
+ ERROR && console.error("Please define a culture");
+ return "ERROR";
+ }
const base = pack.cultures[culture].base;
- return getBase(base, min, max, dupl);
+ return this.getBase(base, min, max, dupl);
};
// generate short name for culture
- const getCultureShort = function (culture) {
- if (culture === undefined) return ERROR && console.error("Please define a culture");
- return getBaseShort(pack.cultures[culture].base);
+ getCultureShort(culture: number): string {
+ if (culture === undefined) {
+ ERROR && console.error("Please define a culture");
+ return "ERROR";
+ }
+ return this.getBaseShort(pack.cultures[culture].base);
};
// generate short name for base
- const getBaseShort = function (base) {
- const min = nameBases[base] ? nameBases[base].min - 1 : null;
- const max = min ? Math.max(nameBases[base].max - 2, min) : null;
- return getBase(base, min, max, "", 0);
+ getBaseShort(base: number): string {
+ const min = nameBases[base] ? nameBases[base].min - 1 : undefined;
+ const max = min ? Math.max(nameBases[base].max - 2, min) : undefined;
+ 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
- const getState = function (name, culture, base) {
- if (name === undefined) return ERROR && console.error("Please define a base name");
- if (culture === undefined && base === undefined) return ERROR && console.error("Please define a culture");
+ getState(name: string, culture: number, base: number): string {
+ if (name === undefined) {
+ 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;
// 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);
// 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
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
if (base > 32 && base < 42) return name;
// define if suffix should be used
- if (name.length > 3 && vowel(name.slice(-1))) {
- if (vowel(name.slice(-2, -1)) && P(0.85)) name = name.slice(0, -2);
+ if (name.length > 3 && isVowel(name.slice(-1))) {
+ if (isVowel(name.slice(-2, -1)) && P(0.85)) name = name.slice(0, -2);
// 85% for vv
else if (P(0.7)) name = name.slice(0, -1);
// ~60% for cv
@@ -225,20 +273,11 @@ window.Names = (function () {
// Berber
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
- const getMapName = function (force) {
+ getMapName(force: boolean) {
if (!force && locked("mapName")) return;
if (force && locked("mapName")) unlock("mapName");
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 max = Math.max(nameBases[base].max - 3, min);
- const baseName = getBase(base, min, max, "", 0);
- const name = P(0.7) ? addSuffix(baseName) : baseName;
+ const baseName = this.getBase(base, min, max, "") as string;
+ const name = P(0.7) ? this.addSuffix(baseName) : baseName;
mapName.value = name;
};
- function addSuffix(name) {
- 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 () {
+ getNameBases(): NameBase[] {
// name, min length, max length, letters to allow duplication, multi-word name rate [deprecated]
// prettier-ignore
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"}
];
};
+}
- return {
- getBase,
- getCulture,
- getCultureShort,
- getBaseShort,
- getState,
- updateChain,
- clearChains,
- getNameBases,
- getMapName,
- calculateChain
- };
-})();
+window.Names = new NamesGenerator();
\ No newline at end of file
diff --git a/src/types/PackedGraph.ts b/src/types/PackedGraph.ts
index 23f464df..193274b0 100644
--- a/src/types/PackedGraph.ts
+++ b/src/types/PackedGraph.ts
@@ -33,4 +33,5 @@ export interface PackedGraph {
};
rivers: River[];
features: PackedGraphFeature[];
+ cultures: any[];
}
\ No newline at end of file
diff --git a/src/types/global.ts b/src/types/global.ts
index 1f37d64e..cb71e31d 100644
--- a/src/types/global.ts
+++ b/src/types/global.ts
@@ -1,5 +1,6 @@
import type { Selection } from 'd3';
import { PackedGraph } from "./PackedGraph";
+import { NameBase } from '../modules/names-generator';
declare global {
var seed: string;
@@ -13,10 +14,11 @@ declare global {
var ERROR: boolean;
var heightmapTemplates: any;
- var Names: any;
+ var nameBases: NameBase[];
var pointsInput: HTMLInputElement;
var heightExponentInput: HTMLInputElement;
+ var mapName: HTMLInputElement;
var rivers: Selection;
var oceanLayers: Selection;
@@ -30,4 +32,8 @@ declare global {
icons: string[][];
cost: number[];
};
+
+ var tip: (message: string, autoHide?: boolean, type?: "info" | "warning" | "error") => void;
+ var locked: (settingId: string) => boolean;
+ var unlock: (settingId: string) => void;
}
\ No newline at end of file