diff --git a/public/modules/states-generator.js b/public/modules/states-generator.js
deleted file mode 100644
index 9662e648..00000000
--- a/public/modules/states-generator.js
+++ /dev/null
@@ -1,640 +0,0 @@
-"use strict";
-
-window.States = (() => {
- const generate = () => {
- TIME && console.time("generateStates");
- pack.states = createStates();
- expandStates();
- normalize();
- getPoles();
- findNeighbors();
- assignColors();
- generateCampaigns();
- generateDiplomacy();
-
- TIME && console.timeEnd("generateStates");
-
- // for each capital create a state
- function createStates() {
- const states = [{i: 0, name: "Neutrals"}];
- const each5th = each(5);
- const sizeVariety = byId("sizeVariety").valueAsNumber;
-
- pack.burgs.forEach(burg => {
- if (!burg.i || !burg.capital) return;
-
- const expansionism = rn(Math.random() * sizeVariety + 1, 1);
- const basename = burg.name.length < 9 && each5th(burg.cell) ? burg.name : Names.getCultureShort(burg.culture);
- const name = Names.getState(basename, burg.culture);
- const type = pack.cultures[burg.culture].type;
- const coa = COA.generate(null, null, null, type);
- coa.shield = COA.getShield(burg.culture, null);
- states.push({
- i: burg.i,
- name,
- expansionism,
- capital: burg.i,
- type,
- center: burg.cell,
- culture: burg.culture,
- coa
- });
- });
-
- return states;
- }
- };
-
- // expand cultures across the map (Dijkstra-like algorithm)
- const expandStates = () => {
- TIME && console.time("expandStates");
- const {cells, states, cultures, burgs} = pack;
-
- cells.state = cells.state || new Uint16Array(cells.i.length);
-
- const queue = new FlatQueue();
- const cost = [];
-
- const globalGrowthRate = byId("growthRate").valueAsNumber || 1;
- const statesGrowthRate = byId("statesGrowthRate")?.valueAsNumber || 1;
- const growthRate = (cells.i.length / 2) * globalGrowthRate * statesGrowthRate; // limit cost for state growth
-
- // remove state from all cells except of locked
- for (const cellId of cells.i) {
- const state = states[cells.state[cellId]];
- if (state.lock) continue;
- cells.state[cellId] = 0;
- }
-
- for (const state of states) {
- if (!state.i || state.removed) continue;
-
- const capitalCell = burgs[state.capital].cell;
- cells.state[capitalCell] = state.i;
- const cultureCenter = cultures[state.culture].center;
- const b = cells.biome[cultureCenter]; // state native biome
- queue.push({e: state.center, p: 0, s: state.i, b}, 0);
- cost[state.center] = 1;
- }
-
- while (queue.length) {
- const next = queue.pop();
-
- const {e, p, s, b} = next;
- const {type, culture} = states[s];
-
- cells.c[e].forEach(e => {
- const state = states[cells.state[e]];
- if (state.lock) return; // do not overwrite cell of locked states
- if (cells.state[e] && e === state.center) return; // do not overwrite capital cells
-
- const cultureCost = culture === cells.culture[e] ? -9 : 100;
- const populationCost = cells.h[e] < 20 ? 0 : cells.s[e] ? Math.max(20 - cells.s[e], 0) : 5000;
- const biomeCost = getBiomeCost(b, cells.biome[e], type);
- const heightCost = getHeightCost(pack.features[cells.f[e]], cells.h[e], type);
- const riverCost = getRiverCost(cells.r[e], e, type);
- const typeCost = getTypeCost(cells.t[e], type);
- const cellCost = Math.max(cultureCost + populationCost + biomeCost + heightCost + riverCost + typeCost, 0);
- const totalCost = p + 10 + cellCost / states[s].expansionism;
-
- if (totalCost > growthRate) return;
-
- if (!cost[e] || totalCost < cost[e]) {
- if (cells.h[e] >= 20) cells.state[e] = s; // assign state to cell
- cost[e] = totalCost;
- queue.push({e, p: totalCost, s, b}, totalCost);
- }
- });
- }
-
- burgs.filter(b => b.i && !b.removed).forEach(b => (b.state = cells.state[b.cell])); // assign state to burgs
-
- function getBiomeCost(b, biome, type) {
- if (b === biome) return 10; // tiny penalty for native biome
- if (type === "Hunting") return biomesData.cost[biome] * 2; // non-native biome penalty for hunters
- if (type === "Nomadic" && biome > 4 && biome < 10) return biomesData.cost[biome] * 3; // forest biome penalty for nomads
- return biomesData.cost[biome]; // general non-native biome penalty
- }
-
- function getHeightCost(f, h, type) {
- if (type === "Lake" && f.type === "lake") return 10; // low lake crossing penalty for Lake cultures
- if (type === "Naval" && h < 20) return 300; // low sea crossing penalty for Navals
- if (type === "Nomadic" && h < 20) return 10000; // giant sea crossing penalty for Nomads
- if (h < 20) return 1000; // general sea crossing penalty
- if (type === "Highland" && h < 62) return 1100; // penalty for highlanders on lowlands
- if (type === "Highland") return 0; // no penalty for highlanders on highlands
- if (h >= 67) return 2200; // general mountains crossing penalty
- if (h >= 44) return 300; // general hills crossing penalty
- return 0;
- }
-
- function getRiverCost(r, i, type) {
- if (type === "River") return r ? 0 : 100; // penalty for river cultures
- if (!r) return 0; // no penalty for others if there is no river
- return minmax(cells.fl[i] / 10, 20, 100); // river penalty from 20 to 100 based on flux
- }
-
- function getTypeCost(t, type) {
- if (t === 1) return type === "Naval" || type === "Lake" ? 0 : type === "Nomadic" ? 60 : 20; // penalty for coastline
- if (t === 2) return type === "Naval" || type === "Nomadic" ? 30 : 0; // low penalty for land level 2 for Navals and nomads
- if (t !== -1) return type === "Naval" || type === "Lake" ? 100 : 0; // penalty for mainland for navals
- return 0;
- }
-
- TIME && console.timeEnd("expandStates");
- };
-
- const normalize = () => {
- TIME && console.time("normalizeStates");
- const {cells, burgs} = pack;
-
- for (const i of cells.i) {
- if (cells.h[i] < 20 || cells.burg[i]) continue; // do not overwrite burgs
- if (pack.states[cells.state[i]]?.lock) continue; // do not overwrite cells of locks states
- if (cells.c[i].some(c => burgs[cells.burg[c]].capital)) continue; // do not overwrite near capital
- const neibs = cells.c[i].filter(c => cells.h[c] >= 20);
- const adversaries = neibs.filter(c => !pack.states[cells.state[c]]?.lock && cells.state[c] !== cells.state[i]);
- if (adversaries.length < 2) continue;
- const buddies = neibs.filter(c => !pack.states[cells.state[c]]?.lock && cells.state[c] === cells.state[i]);
- if (buddies.length > 2) continue;
- if (adversaries.length <= buddies.length) continue;
- cells.state[i] = cells.state[adversaries[0]];
- }
- TIME && console.timeEnd("normalizeStates");
- };
-
- // calculate pole of inaccessibility for each state
- const getPoles = () => {
- const getType = cellId => pack.cells.state[cellId];
- const poles = getPolesOfInaccessibility(pack, getType);
-
- pack.states.forEach(s => {
- if (!s.i || s.removed) return;
- s.pole = poles[s.i] || [0, 0];
- });
- };
-
- const findNeighbors = () => {
- const {cells, states} = pack;
-
- states.forEach(s => {
- if (s.removed) return;
- s.neighbors = new Set();
- });
-
- for (const i of cells.i) {
- if (cells.h[i] < 20) continue;
- const s = cells.state[i];
-
- cells.c[i]
- .filter(c => cells.h[c] >= 20 && cells.state[c] !== s)
- .forEach(c => states[s].neighbors.add(cells.state[c]));
- }
-
- // convert neighbors Set object into array
- states.forEach(s => {
- if (!s.neighbors || s.removed) return;
- s.neighbors = Array.from(s.neighbors);
- });
- };
-
- const assignColors = () => {
- TIME && console.time("assignColors");
- const colors = ["#66c2a5", "#fc8d62", "#8da0cb", "#e78ac3", "#a6d854", "#ffd92f"]; // d3.schemeSet2;
- const states = pack.states;
-
- // assign basic color using greedy coloring algorithm
- states.forEach(state => {
- if (!state.i || state.removed || state.lock) return;
- state.color = colors.find(color => state.neighbors.every(neibStateId => states[neibStateId].color !== color));
- if (!state.color) state.color = getRandomColor();
- colors.push(colors.shift());
- });
-
- // randomize each already used color a bit
- colors.forEach(c => {
- const sameColored = states.filter(state => state.color === c && state.i && !state.lock);
- sameColored.forEach((state, index) => {
- if (!index) return;
- state.color = getMixedColor(state.color);
- });
- });
-
- TIME && console.timeEnd("assignColors");
- };
-
- // calculate states data like area, population etc.
- const collectStatistics = () => {
- TIME && console.time("collectStatistics");
- const {cells, states} = pack;
-
- states.forEach(s => {
- if (s.removed) return;
- s.cells = s.area = s.burgs = s.rural = s.urban = 0;
- });
-
- for (const i of cells.i) {
- if (cells.h[i] < 20) continue;
- const s = cells.state[i];
-
- // collect stats
- states[s].cells += 1;
- states[s].area += cells.area[i];
- states[s].rural += cells.pop[i];
- if (cells.burg[i]) {
- states[s].urban += pack.burgs[cells.burg[i]].population;
- states[s].burgs++;
- }
- }
-
- TIME && console.timeEnd("collectStatistics");
- };
-
- const wars = {
- War: 6,
- Conflict: 2,
- Campaign: 4,
- Invasion: 2,
- Rebellion: 2,
- Conquest: 2,
- Intervention: 1,
- Expedition: 1,
- Crusade: 1
- };
-
- const generateCampaign = state => {
- const neighbors = state.neighbors.length ? state.neighbors : [0];
- return neighbors
- .map(i => {
- const name = i && P(0.8) ? pack.states[i].name : Names.getCultureShort(state.culture);
- const start = gauss(options.year - 100, 150, 1, options.year - 6);
- const end = start + gauss(4, 5, 1, options.year - start - 1);
- return {name: getAdjective(name) + " " + rw(wars), start, end};
- })
- .sort((a, b) => a.start - b.start);
- };
-
- // generate historical conflicts of each state
- const generateCampaigns = () => {
- pack.states.forEach(s => {
- if (!s.i || s.removed) return;
- s.campaigns = generateCampaign(s);
- });
- };
-
- // generate Diplomatic Relationships
- const generateDiplomacy = () => {
- TIME && console.time("generateDiplomacy");
- const {cells, states} = pack;
- const chronicle = (states[0].diplomacy = []);
- const valid = states.filter(s => s.i && !states.removed);
-
- const neibs = {Ally: 1, Friendly: 2, Neutral: 1, Suspicion: 10, Rival: 9}; // relations to neighbors
- const neibsOfNeibs = {Ally: 10, Friendly: 8, Neutral: 5, Suspicion: 1}; // relations to neighbors of neighbors
- const far = {Friendly: 1, Neutral: 12, Suspicion: 2, Unknown: 6}; // relations to other
- const navals = {Neutral: 1, Suspicion: 2, Rival: 1, Unknown: 1}; // relations of naval powers
-
- valid.forEach(s => (s.diplomacy = new Array(states.length).fill("x"))); // clear all relationships
- if (valid.length < 2) return; // no states to renerate relations with
- const areaMean = d3.mean(valid.map(s => s.area)); // average state area
-
- // generic relations
- for (let f = 1; f < states.length; f++) {
- if (states[f].removed) continue;
-
- if (states[f].diplomacy.includes("Vassal")) {
- // Vassals copy relations from their Suzerains
- const suzerain = states[f].diplomacy.indexOf("Vassal");
-
- for (let i = 1; i < states.length; i++) {
- if (i === f || i === suzerain) continue;
- states[f].diplomacy[i] = states[suzerain].diplomacy[i];
- if (states[suzerain].diplomacy[i] === "Suzerain") states[f].diplomacy[i] = "Ally";
- for (let e = 1; e < states.length; e++) {
- if (e === f || e === suzerain) continue;
- if (states[e].diplomacy[suzerain] === "Suzerain" || states[e].diplomacy[suzerain] === "Vassal") continue;
- states[e].diplomacy[f] = states[e].diplomacy[suzerain];
- }
- }
- continue;
- }
-
- for (let t = f + 1; t < states.length; t++) {
- if (states[t].removed) continue;
-
- if (states[t].diplomacy.includes("Vassal")) {
- const suzerain = states[t].diplomacy.indexOf("Vassal");
- states[f].diplomacy[t] = states[f].diplomacy[suzerain];
- continue;
- }
-
- const naval =
- states[f].type === "Naval" &&
- states[t].type === "Naval" &&
- cells.f[states[f].center] !== cells.f[states[t].center];
- const neib = naval ? false : states[f].neighbors.includes(t);
- const neibOfNeib =
- naval || neib
- ? false
- : states[f].neighbors
- .map(n => states[n].neighbors)
- .join("")
- .includes(t);
-
- let status = naval ? rw(navals) : neib ? rw(neibs) : neibOfNeib ? rw(neibsOfNeibs) : rw(far);
-
- // add Vassal
- if (
- neib &&
- P(0.8) &&
- states[f].area > areaMean &&
- states[t].area < areaMean &&
- states[f].area / states[t].area > 2
- )
- status = "Vassal";
- states[f].diplomacy[t] = status === "Vassal" ? "Suzerain" : status;
- states[t].diplomacy[f] = status;
- }
- }
-
- // declare wars
- for (let attacker = 1; attacker < states.length; attacker++) {
- const ad = states[attacker].diplomacy; // attacker relations;
- if (states[attacker].removed) continue;
- if (!ad.includes("Rival")) continue; // no rivals to attack
- if (ad.includes("Vassal")) continue; // not independent
- if (ad.includes("Enemy")) continue; // already at war
-
- // random independent rival
- const defender = ra(
- ad.map((r, d) => (r === "Rival" && !states[d].diplomacy.includes("Vassal") ? d : 0)).filter(d => d)
- );
- let ap = states[attacker].area * states[attacker].expansionism;
- let dp = states[defender].area * states[defender].expansionism;
- if (ap < dp * gauss(1.6, 0.8, 0, 10, 2)) continue; // defender is too strong
-
- const an = states[attacker].name;
- const dn = states[defender].name; // names
- const attackers = [attacker];
- const defenders = [defender]; // attackers and defenders array
- const dd = states[defender].diplomacy; // defender relations;
-
- // start an ongoing war
- const name = `${an}-${trimVowels(dn)}ian War`;
- const start = options.year - gauss(2, 3, 0, 10);
- const war = [name, `${an} declared a war on its rival ${dn}`];
- const campaign = {name, start, attacker, defender};
- states[attacker].campaigns.push(campaign);
- states[defender].campaigns.push(campaign);
-
- // attacker vassals join the war
- ad.forEach((r, d) => {
- if (r === "Suzerain") {
- attackers.push(d);
- war.push(`${an}'s vassal ${states[d].name} joined the war on attackers side`);
- }
- });
-
- // defender vassals join the war
- dd.forEach((r, d) => {
- if (r === "Suzerain") {
- defenders.push(d);
- war.push(`${dn}'s vassal ${states[d].name} joined the war on defenders side`);
- }
- });
-
- ap = d3.sum(attackers.map(a => states[a].area * states[a].expansionism)); // attackers joined power
- dp = d3.sum(defenders.map(d => states[d].area * states[d].expansionism)); // defender joined power
-
- // defender allies join
- dd.forEach((r, d) => {
- if (r !== "Ally" || states[d].diplomacy.includes("Vassal")) return;
- if (states[d].diplomacy[attacker] !== "Rival" && ap / dp > 2 * gauss(1.6, 0.8, 0, 10, 2)) {
- const reason = states[d].diplomacy.includes("Enemy") ? "Being already at war," : `Frightened by ${an},`;
- war.push(`${reason} ${states[d].name} severed the defense pact with ${dn}`);
- dd[d] = states[d].diplomacy[defender] = "Suspicion";
- return;
- }
- defenders.push(d);
- dp += states[d].area * states[d].expansionism;
- war.push(`${dn}'s ally ${states[d].name} joined the war on defenders side`);
-
- // ally vassals join
- states[d].diplomacy
- .map((r, d) => (r === "Suzerain" ? d : 0))
- .filter(d => d)
- .forEach(v => {
- defenders.push(v);
- dp += states[v].area * states[v].expansionism;
- war.push(`${states[d].name}'s vassal ${states[v].name} joined the war on defenders side`);
- });
- });
-
- // attacker allies join if the defender is their rival or joined power > defenders power and defender is not an ally
- ad.forEach((r, d) => {
- if (r !== "Ally" || states[d].diplomacy.includes("Vassal") || defenders.includes(d)) return;
- const name = states[d].name;
- if (states[d].diplomacy[defender] !== "Rival" && (P(0.2) || ap <= dp * 1.2)) {
- war.push(`${an}'s ally ${name} avoided entering the war`);
- return;
- }
- const allies = states[d].diplomacy.map((r, d) => (r === "Ally" ? d : 0)).filter(d => d);
- if (allies.some(ally => defenders.includes(ally))) {
- war.push(`${an}'s ally ${name} did not join the war as its allies are in war on both sides`);
- return;
- }
-
- attackers.push(d);
- ap += states[d].area * states[d].expansionism;
- war.push(`${an}'s ally ${name} joined the war on attackers side`);
-
- // ally vassals join
- states[d].diplomacy
- .map((r, d) => (r === "Suzerain" ? d : 0))
- .filter(d => d)
- .forEach(v => {
- attackers.push(v);
- dp += states[v].area * states[v].expansionism;
- war.push(`${states[d].name}'s vassal ${states[v].name} joined the war on attackers side`);
- });
- });
-
- // change relations to Enemy for all participants
- attackers.forEach(a => defenders.forEach(d => (states[a].diplomacy[d] = states[d].diplomacy[a] = "Enemy")));
- chronicle.push(war); // add a record to diplomatical history
- }
-
- TIME && console.timeEnd("generateDiplomacy");
- };
-
- // select a forms for listed or all valid states
- const defineStateForms = list => {
- TIME && console.time("defineStateForms");
- const states = pack.states.filter(s => s.i && !s.removed && !s.lock);
- if (states.length < 1) return;
-
- const generic = {Monarchy: 25, Republic: 2, Union: 1};
- const naval = {Monarchy: 25, Republic: 8, Union: 3};
-
- const median = d3.median(pack.states.map(s => s.area));
- const empireMin = states.map(s => s.area).sort((a, b) => b - a)[Math.max(Math.ceil(states.length ** 0.4) - 2, 0)];
- const expTiers = pack.states.map(s => {
- let tier = Math.min(Math.floor((s.area / median) * 2.6), 4);
- if (tier === 4 && s.area < empireMin) tier = 3;
- return tier;
- });
-
- const monarchy = ["Duchy", "Grand Duchy", "Principality", "Kingdom", "Empire"]; // per expansionism tier
- const republic = {
- Republic: 75,
- Federation: 4,
- "Trade Company": 4,
- "Most Serene Republic": 2,
- Oligarchy: 2,
- Tetrarchy: 1,
- Triumvirate: 1,
- Diarchy: 1,
- Junta: 1
- }; // weighted random
- const union = {
- Union: 3,
- League: 4,
- Confederation: 1,
- "United Kingdom": 1,
- "United Republic": 1,
- "United Provinces": 2,
- Commonwealth: 1,
- Heptarchy: 1
- }; // weighted random
- const theocracy = {Theocracy: 20, Brotherhood: 1, Thearchy: 2, See: 1, "Holy State": 1};
- const anarchy = {"Free Territory": 2, Council: 3, Commune: 1, Community: 1};
-
- for (const s of states) {
- if (list && !list.includes(s.i)) continue;
- const tier = expTiers[s.i];
-
- const religion = pack.cells.religion[s.center];
- const isTheocracy =
- (religion && pack.religions[religion].expansion === "state") ||
- (P(0.1) && ["Organized", "Cult"].includes(pack.religions[religion].type));
- const isAnarchy = P(0.01 - tier / 500);
-
- if (isTheocracy) s.form = "Theocracy";
- else if (isAnarchy) s.form = "Anarchy";
- else s.form = s.type === "Naval" ? rw(naval) : rw(generic);
- s.formName = selectForm(s, tier);
- s.fullName = getFullName(s);
- }
-
- function selectForm(s, tier) {
- const base = pack.cultures[s.culture].base;
-
- if (s.form === "Monarchy") {
- const form = monarchy[tier];
- // Default name depends on exponent tier, some culture bases have special names for tiers
- if (s.diplomacy) {
- if (
- form === "Duchy" &&
- s.neighbors.length > 1 &&
- rand(6) < s.neighbors.length &&
- s.diplomacy.includes("Vassal")
- )
- return "Marches"; // some vassal duchies on borderland
- if (base === 1 && P(0.3) && s.diplomacy.includes("Vassal")) return "Dominion"; // English vassals
- if (P(0.3) && s.diplomacy.includes("Vassal")) return "Protectorate"; // some vassals
- }
-
- if (base === 31 && (form === "Empire" || form === "Kingdom")) return "Khanate"; // Mongolian
- if (base === 16 && form === "Principality") return "Beylik"; // Turkic
- if (base === 5 && (form === "Empire" || form === "Kingdom")) return "Tsardom"; // Ruthenian
- if (base === 16 && (form === "Empire" || form === "Kingdom")) return "Khaganate"; // Turkic
- if (base === 12 && (form === "Kingdom" || form === "Grand Duchy")) return "Shogunate"; // Japanese
- if ([18, 17].includes(base) && form === "Empire") return "Caliphate"; // Arabic, Berber
- if (base === 18 && (form === "Grand Duchy" || form === "Duchy")) return "Emirate"; // Arabic
- if (base === 7 && (form === "Grand Duchy" || form === "Duchy")) return "Despotate"; // Greek
- if (base === 31 && (form === "Grand Duchy" || form === "Duchy")) return "Ulus"; // Mongolian
- if (base === 16 && (form === "Grand Duchy" || form === "Duchy")) return "Horde"; // Turkic
- if (base === 24 && (form === "Grand Duchy" || form === "Duchy")) return "Satrapy"; // Iranian
- return form;
- }
-
- if (s.form === "Republic") {
- // Default name is from weighted array, special case for small states with only 1 burg
- if (tier < 2 && s.burgs === 1) {
- if (trimVowels(s.name) === trimVowels(pack.burgs[s.capital].name)) {
- s.name = pack.burgs[s.capital].name;
- return "Free City";
- }
- if (P(0.3)) return "City-state";
- }
- return rw(republic);
- }
-
- if (s.form === "Union") return rw(union);
- if (s.form === "Anarchy") return rw(anarchy);
-
- if (s.form === "Theocracy") {
- // European
- if ([0, 1, 2, 3, 4, 6, 8, 9, 13, 15, 20].includes(base)) {
- if (P(0.1)) return "Divine " + monarchy[tier];
- if (tier < 2 && P(0.5)) return "Diocese";
- if (tier < 2 && P(0.5)) return "Bishopric";
- }
- if (P(0.9) && [7, 5].includes(base)) {
- // Greek, Ruthenian
- if (tier < 2) return "Eparchy";
- if (tier === 2) return "Exarchate";
- if (tier > 2) return "Patriarchate";
- }
- if (P(0.9) && [21, 16].includes(base)) return "Imamah"; // Nigerian, Turkish
- if (tier > 2 && P(0.8) && [18, 17, 28].includes(base)) return "Caliphate"; // Arabic, Berber, Swahili
- return rw(theocracy);
- }
- }
-
- TIME && console.timeEnd("defineStateForms");
- };
-
- // state forms requiring Adjective + Name, all other forms use scheme Form + Of + Name
- const adjForms = [
- "Empire",
- "Sultanate",
- "Khaganate",
- "Shogunate",
- "Caliphate",
- "Despotate",
- "Theocracy",
- "Oligarchy",
- "Union",
- "Confederation",
- "Trade Company",
- "League",
- "Tetrarchy",
- "Triumvirate",
- "Diarchy",
- "Horde",
- "Marches"
- ];
-
- const getFullName = state => {
- if (!state.formName) return state.name;
- if (!state.name && state.formName) return "The " + state.formName;
- const adjName = adjForms.includes(state.formName) && !/-| /.test(state.name);
- return adjName ? `${getAdjective(state.name)} ${state.formName}` : `${state.formName} of ${state.name}`;
- };
-
- return {
- generate,
- expandStates,
- normalize,
- getPoles,
- findNeighbors,
- assignColors,
- collectStatistics,
- generateCampaign,
- generateCampaigns,
- generateDiplomacy,
- defineStateForms,
- getFullName
- };
-})();
diff --git a/src/index.html b/src/index.html
index cf121474..14b61949 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 f4a3def3..660fc100 100644
--- a/src/modules/index.ts
+++ b/src/modules/index.ts
@@ -8,3 +8,4 @@ import "./river-generator";
import "./burgs-generator";
import "./biomes";
import "./cultures-generator";
+import "./states-generator";
diff --git a/src/modules/names-generator.ts b/src/modules/names-generator.ts
index da60beca..5805cc92 100644
--- a/src/modules/names-generator.ts
+++ b/src/modules/names-generator.ts
@@ -223,7 +223,7 @@ class NamesGenerator {
}
// generate state name based on capital or random name and culture-specific suffix
- getState(name: string, culture: number, base: number): string {
+ getState(name: string, culture: number, base?: number): string {
if (name === undefined) {
ERROR && console.error("Please define a base name");
return "ERROR";
diff --git a/src/modules/states-generator.ts b/src/modules/states-generator.ts
new file mode 100644
index 00000000..88e21897
--- /dev/null
+++ b/src/modules/states-generator.ts
@@ -0,0 +1,824 @@
+import { mean, median, sum } from "d3";
+import {
+ byId,
+ each,
+ gauss,
+ getAdjective,
+ getMixedColor,
+ getPolesOfInaccessibility,
+ getRandomColor,
+ minmax,
+ P,
+ ra,
+ rand,
+ rn,
+ rw,
+ trimVowels,
+} from "../utils";
+
+declare global {
+ var States: StatesModule;
+}
+
+interface Campaign {
+ name: string;
+ start: number;
+ end?: number;
+}
+
+export interface State {
+ i: number;
+ name: string;
+ expansionism: number;
+ capital: number;
+ type: string;
+ center: number;
+ culture: number;
+ coa: any;
+ lock?: boolean;
+ removed?: boolean;
+ pole?: [number, number];
+ neighbors?: number[];
+ color?: string;
+ cells?: number;
+ area?: number;
+ burgs?: number;
+ rural?: number;
+ urban?: number;
+ campaigns?: Campaign[];
+ diplomacy?: string[];
+ formName?: string;
+ fullName?: string;
+ form?: string;
+}
+
+class StatesModule {
+ private createStates() {
+ const states: State[] = [{ i: 0, name: "Neutrals" } as State];
+ const each5th = each(5);
+ const sizeVariety = (byId("sizeVariety") as HTMLInputElement).valueAsNumber;
+
+ pack.burgs.forEach((burg) => {
+ if (!burg.i || !burg.capital) return;
+
+ const expansionism = rn(Math.random() * sizeVariety + 1, 1);
+ const basename =
+ burg.name!.length < 9 && each5th(burg.cell)
+ ? burg.name!
+ : Names.getCultureShort(burg.culture!);
+ const name = Names.getState(basename, burg.culture!);
+ const type = pack.cultures[burg.culture!].type;
+ const coa = COA.generate(null, null, null, type);
+ coa.shield = COA.getShield(burg.culture, null);
+ states.push({
+ i: burg.i,
+ name,
+ expansionism,
+ capital: burg.i,
+ type: type!,
+ center: burg.cell,
+ culture: burg.culture!,
+ coa,
+ });
+ });
+
+ return states;
+ }
+
+ private getBiomeCost(b: number, biome: number, type: string) {
+ if (b === biome) return 10; // tiny penalty for native biome
+ if (type === "Hunting") return biomesData.cost[biome] * 2; // non-native biome penalty for hunters
+ if (type === "Nomadic" && biome > 4 && biome < 10)
+ return biomesData.cost[biome] * 3; // forest biome penalty for nomads
+ return biomesData.cost[biome]; // general non-native biome penalty
+ }
+
+ private getHeightCost(f: any, h: number, type: string) {
+ if (type === "Lake" && f.type === "lake") return 10; // low lake crossing penalty for Lake cultures
+ if (type === "Naval" && h < 20) return 300; // low sea crossing penalty for Navals
+ if (type === "Nomadic" && h < 20) return 10000; // giant sea crossing penalty for Nomads
+ if (h < 20) return 1000; // general sea crossing penalty
+ if (type === "Highland" && h < 62) return 1100; // penalty for highlanders on lowlands
+ if (type === "Highland") return 0; // no penalty for highlanders on highlands
+ if (h >= 67) return 2200; // general mountains crossing penalty
+ if (h >= 44) return 300; // general hills crossing penalty
+ return 0;
+ }
+
+ private getRiverCost(r: any, i: number, type: string) {
+ if (type === "River") return r ? 0 : 100; // penalty for river cultures
+ if (!r) return 0; // no penalty for others if there is no river
+ return minmax(pack.cells.fl[i] / 10, 20, 100); // river penalty from 20 to 100 based on flux
+ }
+
+ private getTypeCost(t: number, type: string) {
+ if (t === 1)
+ return type === "Naval" || type === "Lake"
+ ? 0
+ : type === "Nomadic"
+ ? 60
+ : 20; // penalty for coastline
+ if (t === 2) return type === "Naval" || type === "Nomadic" ? 30 : 0; // low penalty for land level 2 for Navals and nomads
+ if (t !== -1) return type === "Naval" || type === "Lake" ? 100 : 0; // penalty for mainland for navals
+ return 0;
+ }
+
+ generate() {
+ TIME && console.time("generateStates");
+ pack.states = this.createStates();
+ this.expandStates();
+ this.normalize();
+ this.getPoles();
+ this.findNeighbors();
+ this.assignColors();
+ this.generateCampaigns();
+ this.generateDiplomacy();
+
+ TIME && console.timeEnd("generateStates");
+ }
+
+ expandStates() {
+ TIME && console.time("expandStates");
+ const { cells, states, cultures, burgs } = pack;
+
+ cells.state = cells.state || new Uint16Array(cells.i.length);
+
+ const queue = new FlatQueue();
+ const cost: number[] = [];
+
+ const globalGrowthRate =
+ (byId("growthRate") as HTMLInputElement)?.valueAsNumber || 1;
+ const statesGrowthRate =
+ (byId("statesGrowthRate") as HTMLInputElement)?.valueAsNumber || 1;
+ const growthRate =
+ (cells.i.length / 2) * globalGrowthRate * statesGrowthRate; // limit cost for state growth
+
+ // remove state from all cells except of locked
+ for (const cellId of cells.i) {
+ const state = states[cells.state[cellId]];
+ if (state.lock) continue;
+ cells.state[cellId] = 0;
+ }
+
+ for (const state of states) {
+ if (!state.i || state.removed) continue;
+
+ const capitalCell = burgs[state.capital].cell;
+ cells.state[capitalCell] = state.i;
+ const cultureCenter = cultures[state.culture].center!;
+ const b = cells.biome[cultureCenter]; // state native biome
+ queue.push({ e: state.center, p: 0, s: state.i, b }, 0);
+ cost[state.center] = 1;
+ }
+
+ while (queue.length) {
+ const next = queue.pop();
+
+ const { e, p, s, b } = next;
+ const { type, culture } = states[s];
+
+ cells.c[e].forEach((e) => {
+ const state = states[cells.state[e]];
+ if (state.lock) return; // do not overwrite cell of locked states
+ if (cells.state[e] && e === state.center) return; // do not overwrite capital cells
+
+ const cultureCost = culture === cells.culture[e] ? -9 : 100;
+ const populationCost =
+ cells.h[e] < 20
+ ? 0
+ : cells.s[e]
+ ? Math.max(20 - cells.s[e], 0)
+ : 5000;
+ const biomeCost = this.getBiomeCost(b, cells.biome[e], type);
+ const heightCost = this.getHeightCost(
+ pack.features[cells.f[e]],
+ cells.h[e],
+ type,
+ );
+ const riverCost = this.getRiverCost(cells.r[e], e, type);
+ const typeCost = this.getTypeCost(cells.t[e], type);
+ const cellCost = Math.max(
+ cultureCost +
+ populationCost +
+ biomeCost +
+ heightCost +
+ riverCost +
+ typeCost,
+ 0,
+ );
+ const totalCost = p + 10 + cellCost / states[s].expansionism;
+
+ if (totalCost > growthRate) return;
+
+ if (!cost[e] || totalCost < cost[e]) {
+ if (cells.h[e] >= 20) cells.state[e] = s; // assign state to cell
+ cost[e] = totalCost;
+ queue.push({ e, p: totalCost, s, b }, totalCost);
+ }
+ });
+ }
+
+ burgs
+ .filter((b) => b.i && !b.removed)
+ .forEach((b) => {
+ b.state = cells.state[b.cell]; // assign state to burgs
+ });
+ TIME && console.timeEnd("expandStates");
+ }
+
+ normalize() {
+ TIME && console.time("normalizeStates");
+ const { cells, burgs } = pack;
+
+ for (const i of cells.i) {
+ if (cells.h[i] < 20 || cells.burg[i]) continue; // do not overwrite burgs
+ if (pack.states[cells.state[i]]?.lock) continue; // do not overwrite cells of locks states
+ if (cells.c[i].some((c) => burgs[cells.burg[c]].capital)) continue; // do not overwrite near capital
+ const neibs = cells.c[i].filter((c) => cells.h[c] >= 20);
+ const adversaries = neibs.filter(
+ (c) =>
+ !pack.states[cells.state[c]]?.lock &&
+ cells.state[c] !== cells.state[i],
+ );
+ if (adversaries.length < 2) continue;
+ const buddies = neibs.filter(
+ (c) =>
+ !pack.states[cells.state[c]]?.lock &&
+ cells.state[c] === cells.state[i],
+ );
+ if (buddies.length > 2) continue;
+ if (adversaries.length <= buddies.length) continue;
+ cells.state[i] = cells.state[adversaries[0]];
+ }
+ TIME && console.timeEnd("normalizeStates");
+ }
+
+ // calculate pole of inaccessibility for each state
+ getPoles() {
+ const getType = (cellId: number) => pack.cells.state[cellId];
+ const poles = getPolesOfInaccessibility(pack, getType);
+
+ pack.states.forEach((s) => {
+ if (!s.i || s.removed) return;
+ s.pole = poles[s.i] || [0, 0];
+ });
+ }
+
+ findNeighbors() {
+ const { cells, states } = pack;
+
+ const stateNeighbors: Set[] = [];
+
+ states.forEach((s) => {
+ if (s.removed) return;
+ stateNeighbors[s.i] = new Set();
+ // s.neighbors = stateNeighbors[s.i];
+ });
+
+ for (const i of cells.i) {
+ if (cells.h[i] < 20) continue;
+ const s = cells.state[i];
+
+ cells.c[i]
+ .filter((c) => cells.h[c] >= 20 && cells.state[c] !== s)
+ .forEach((c) => {
+ stateNeighbors[s].add(cells.state[c]);
+ });
+ }
+
+ // convert neighbors Set object into array
+ states.forEach((s) => {
+ if (!stateNeighbors[s.i] || s.removed) return;
+ s.neighbors = Array.from(stateNeighbors[s.i]);
+ });
+ }
+
+ assignColors() {
+ TIME && console.time("assignColors");
+ const colors = [
+ "#66c2a5",
+ "#fc8d62",
+ "#8da0cb",
+ "#e78ac3",
+ "#a6d854",
+ "#ffd92f",
+ ]; // d3.schemeSet2;
+ const states = pack.states;
+
+ // assign basic color using greedy coloring algorithm
+ states.forEach((state) => {
+ if (!state.i || state.removed || state.lock) return;
+ state.color = colors.find((color) =>
+ state.neighbors!.every(
+ (neibStateId) => states[neibStateId].color !== color,
+ ),
+ );
+ if (!state.color) state.color = getRandomColor();
+ colors.push(colors.shift() as string);
+ });
+
+ // randomize each already used color a bit
+ colors.forEach((c) => {
+ const sameColored = states.filter(
+ (state) => state.color === c && state.i && !state.lock,
+ );
+ sameColored.forEach((state, index) => {
+ if (!index) return;
+ state.color = getMixedColor(state.color!);
+ });
+ });
+
+ TIME && console.timeEnd("assignColors");
+ }
+
+ // calculate states data like area, population etc.
+ collectStatistics() {
+ TIME && console.time("collectStatistics");
+ const { cells, states } = pack;
+
+ states.forEach((s) => {
+ if (s.removed) return;
+ s.cells = s.area = s.burgs = s.rural = s.urban = 0;
+ });
+
+ for (const i of cells.i) {
+ if (cells.h[i] < 20) continue;
+ const s = cells.state[i];
+
+ // collect stats
+ states[s].cells! += 1;
+ states[s].area! += cells.area[i];
+ states[s].rural! += cells.pop[i];
+ if (cells.burg[i]) {
+ states[s].urban! += pack.burgs[cells.burg[i]].population!;
+ states[s].burgs!++;
+ }
+ }
+
+ TIME && console.timeEnd("collectStatistics");
+ }
+
+ generateCampaign(state: State) {
+ const wars = {
+ War: 6,
+ Conflict: 2,
+ Campaign: 4,
+ Invasion: 2,
+ Rebellion: 2,
+ Conquest: 2,
+ Intervention: 1,
+ Expedition: 1,
+ Crusade: 1,
+ };
+ const neighbors = state.neighbors?.length ? state.neighbors : [0];
+ return neighbors
+ .map((i: number) => {
+ const name =
+ i && P(0.8)
+ ? pack.states[i].name
+ : Names.getCultureShort(state.culture);
+ const start = gauss(options.year - 100, 150, 1, options.year - 6);
+ const end = start + gauss(4, 5, 1, options.year - start - 1);
+ return { name: `${getAdjective(name)} ${rw(wars)}`, start, end };
+ })
+ .sort((a, b) => a.start - b.start);
+ }
+
+ generateCampaigns() {
+ pack.states.forEach((s) => {
+ if (!s.i || s.removed) return;
+ s.campaigns = this.generateCampaign(s);
+ });
+ }
+
+ // generate Diplomatic Relationships
+ generateDiplomacy() {
+ TIME && console.time("generateDiplomacy");
+ const { cells, states } = pack;
+ states[0].diplomacy = [];
+ // FIRST STATE IS ALWAYS NEUTRAL and contains the history of diplomacy
+ const chronicle = states[0].diplomacy;
+ const valid = states.filter((s) => s.i && !s.removed); // will filter out neutral as i is 0 => false
+
+ const neibs = { Ally: 1, Friendly: 2, Neutral: 1, Suspicion: 10, Rival: 9 }; // relations to neighbors
+ const neibsOfNeibs = { Ally: 10, Friendly: 8, Neutral: 5, Suspicion: 1 }; // relations to neighbors of neighbors
+ const far = { Friendly: 1, Neutral: 12, Suspicion: 2, Unknown: 6 }; // relations to other
+ const navals = { Neutral: 1, Suspicion: 2, Rival: 1, Unknown: 1 }; // relations of naval powers
+
+ valid.forEach((s) => {
+ s.diplomacy = new Array(states.length).fill("x"); // clear all relationships
+ });
+ if (valid.length < 2) return; // no states to renerate relations with
+ const areaMean: number = mean(valid.map((s) => s.area!)) as number; // average state area
+
+ // generic relations
+ for (let f = 1; f < states.length; f++) {
+ if (states[f].removed) continue;
+ if (states[f].diplomacy!.includes("Vassal")) {
+ // Vassals copy relations from their Suzerains
+ const suzerain = states[f].diplomacy!.indexOf("Vassal");
+
+ for (let i = 1; i < states.length; i++) {
+ if (i === f || i === suzerain) continue;
+ states[f].diplomacy![i] = states[suzerain].diplomacy![i];
+ if (states[suzerain].diplomacy![i] === "Suzerain")
+ states[f].diplomacy![i] = "Ally";
+ for (let e = 1; e < states.length; e++) {
+ if (e === f || e === suzerain) continue;
+ if (
+ states[e].diplomacy![suzerain] === "Suzerain" ||
+ states[e].diplomacy![suzerain] === "Vassal"
+ )
+ continue;
+ states[e].diplomacy![f] = states[e].diplomacy![suzerain];
+ }
+ }
+ continue;
+ }
+
+ for (let t = f + 1; t < states.length; t++) {
+ if (states[t].removed) continue;
+
+ if (states[t].diplomacy!.includes("Vassal")) {
+ const suzerain = states[t].diplomacy!.indexOf("Vassal");
+ states[f].diplomacy![t] = states[f].diplomacy![suzerain];
+ continue;
+ }
+
+ const naval =
+ states[f].type === "Naval" &&
+ states[t].type === "Naval" &&
+ cells.f[states[f].center] !== cells.f[states[t].center];
+ const neib = naval ? false : states[f].neighbors!.includes(t);
+ const neibOfNeib =
+ naval || neib
+ ? false
+ : states[f]
+ .neighbors!.map((n) => states[n].neighbors)
+ .join("")
+ .includes(t.toString());
+
+ let status = naval
+ ? rw(navals)
+ : neib
+ ? rw(neibs)
+ : neibOfNeib
+ ? rw(neibsOfNeibs)
+ : rw(far);
+
+ // add Vassal
+ if (
+ neib &&
+ P(0.8) &&
+ states[f].area! > areaMean &&
+ states[t].area! < areaMean &&
+ states[f].area! / states[t].area! > 2
+ )
+ status = "Vassal";
+ states[f].diplomacy![t] = status === "Vassal" ? "Suzerain" : status;
+ states[t].diplomacy![f] = status;
+ }
+ }
+
+ // declare wars
+ for (let attacker = 1; attacker < states.length; attacker++) {
+ const ad = states[attacker].diplomacy as string[]; // attacker relations;
+ if (states[attacker].removed) continue;
+ if (!ad.includes("Rival")) continue; // no rivals to attack
+ if (ad.includes("Vassal")) continue; // not independent
+ if (ad.includes("Enemy")) continue; // already at war
+
+ // random independent rival
+ const defender = ra(
+ ad
+ .map((r, d) =>
+ r === "Rival" && !states[d].diplomacy!.includes("Vassal") ? d : 0,
+ )
+ .filter((d) => d),
+ );
+ let ap = states[attacker].area! * states[attacker].expansionism;
+ let dp = states[defender].area! * states[defender].expansionism;
+ if (ap < dp * gauss(1.6, 0.8, 0, 10, 2)) continue; // defender is too strong
+
+ const an = states[attacker].name;
+ const dn = states[defender].name; // names
+ const attackers = [attacker];
+ const defenders = [defender]; // attackers and defenders array
+ const dd = states[defender].diplomacy as string[]; // defender relations;
+
+ // start an ongoing war
+ const name = `${an}-${trimVowels(dn)}ian War`;
+ const start = options.year - gauss(2, 3, 0, 10);
+ const war = [name, `${an} declared a war on its rival ${dn}`];
+ const campaign = { name, start, attacker, defender };
+ states[attacker].campaigns!.push(campaign);
+ states[defender].campaigns!.push(campaign);
+
+ // attacker vassals join the war
+ ad.forEach((r, d) => {
+ if (r === "Suzerain") {
+ attackers.push(d);
+ war.push(
+ `${an}'s vassal ${states[d].name} joined the war on attackers side`,
+ );
+ }
+ });
+
+ // defender vassals join the war
+ dd.forEach((r, d) => {
+ if (r === "Suzerain") {
+ defenders.push(d);
+ war.push(
+ `${dn}'s vassal ${states[d].name} joined the war on defenders side`,
+ );
+ }
+ });
+
+ ap = sum(attackers.map((a) => states[a].area! * states[a].expansionism)); // attackers joined power
+ dp = sum(defenders.map((d) => states[d].area! * states[d].expansionism)); // defender joined power
+
+ // defender allies join
+ dd.forEach((r, d) => {
+ if (r !== "Ally" || states[d].diplomacy!.includes("Vassal")) return;
+ if (
+ states[d].diplomacy![attacker] !== "Rival" &&
+ ap / dp > 2 * gauss(1.6, 0.8, 0, 10, 2)
+ ) {
+ const reason = states[d].diplomacy!.includes("Enemy")
+ ? "Being already at war,"
+ : `Frightened by ${an},`;
+ war.push(
+ `${reason} ${states[d].name} severed the defense pact with ${dn}`,
+ );
+ dd[d] = states[d].diplomacy![defender] = "Suspicion";
+ return;
+ }
+ defenders.push(d);
+ dp += states[d].area! * states[d].expansionism;
+ war.push(
+ `${dn}'s ally ${states[d].name} joined the war on defenders side`,
+ );
+
+ // ally vassals join
+ states[d]
+ .diplomacy!.map((r, d) => (r === "Suzerain" ? d : 0))
+ .filter((d) => d)
+ .forEach((v) => {
+ defenders.push(v);
+ dp += states[v].area! * states[v].expansionism;
+ war.push(
+ `${states[d].name}'s vassal ${states[v].name} joined the war on defenders side`,
+ );
+ });
+ });
+
+ // attacker allies join if the defender is their rival or joined power > defenders power and defender is not an ally
+ ad.forEach((r, d) => {
+ if (
+ r !== "Ally" ||
+ states[d].diplomacy!.includes("Vassal") ||
+ defenders.includes(d)
+ )
+ return;
+ const name = states[d].name;
+ if (
+ states[d].diplomacy![defender] !== "Rival" &&
+ (P(0.2) || ap <= dp * 1.2)
+ ) {
+ war.push(`${an}'s ally ${name} avoided entering the war`);
+ return;
+ }
+ const allies = states[d]
+ .diplomacy!.map((r, d) => (r === "Ally" ? d : 0))
+ .filter((d) => d);
+ if (allies.some((ally) => defenders.includes(ally))) {
+ war.push(
+ `${an}'s ally ${name} did not join the war as its allies are in war on both sides`,
+ );
+ return;
+ }
+
+ attackers.push(d);
+ ap += states[d].area! * states[d].expansionism;
+ war.push(`${an}'s ally ${name} joined the war on attackers side`);
+
+ // ally vassals join
+ states[d]
+ .diplomacy!.map((r, d) => (r === "Suzerain" ? d : 0))
+ .filter((d) => d)
+ .forEach((v) => {
+ attackers.push(v);
+ // TODO: I think here is a bug, it should be ap instead of dp
+ dp += states[v].area! * states[v].expansionism;
+ war.push(
+ `${states[d].name}'s vassal ${states[v].name} joined the war on attackers side`,
+ );
+ });
+ });
+
+ // change relations to Enemy for all participants
+ attackers.forEach((a) => {
+ defenders.forEach((d: number) => {
+ states[a].diplomacy![d] = states[d].diplomacy![a] = "Enemy";
+ });
+ });
+ // TODO: record war in chronicle to keep state interface clean
+ chronicle.push(war as any); // add a record to diplomatical history
+ }
+ TIME && console.timeEnd("generateDiplomacy");
+ }
+
+ // select a forms for listed or all valid states
+ defineStateForms(list: number[] | null = null) {
+ TIME && console.time("defineStateForms");
+ const states = pack.states.filter((s) => s.i && !s.removed && !s.lock);
+ if (states.length < 1) return;
+
+ const generic = { Monarchy: 25, Republic: 2, Union: 1 };
+ const naval = { Monarchy: 25, Republic: 8, Union: 3 };
+
+ const medianState = median(pack.states.map((s) => s.area))!;
+ const empireMin = states.map((s) => s.area).sort((a = 0, b = 0) => b - a)[
+ Math.max(Math.ceil(states.length ** 0.4) - 2, 0)
+ ]!;
+ const expTiers = pack.states.map((s) => {
+ let tier = Math.min(Math.floor((s.area! / medianState) * 2.6), 4);
+ if (tier === 4 && s.area! < empireMin) tier = 3;
+ return tier;
+ });
+
+ const monarchy = [
+ "Duchy",
+ "Grand Duchy",
+ "Principality",
+ "Kingdom",
+ "Empire",
+ ]; // per expansionism tier
+ const republic = {
+ Republic: 75,
+ Federation: 4,
+ "Trade Company": 4,
+ "Most Serene Republic": 2,
+ Oligarchy: 2,
+ Tetrarchy: 1,
+ Triumvirate: 1,
+ Diarchy: 1,
+ Junta: 1,
+ }; // weighted random
+ const union = {
+ Union: 3,
+ League: 4,
+ Confederation: 1,
+ "United Kingdom": 1,
+ "United Republic": 1,
+ "United Provinces": 2,
+ Commonwealth: 1,
+ Heptarchy: 1,
+ }; // weighted random
+ const theocracy = {
+ Theocracy: 20,
+ Brotherhood: 1,
+ Thearchy: 2,
+ See: 1,
+ "Holy State": 1,
+ };
+ const anarchy = {
+ "Free Territory": 2,
+ Council: 3,
+ Commune: 1,
+ Community: 1,
+ };
+
+ for (const s of states) {
+ if (list && !list.includes(s.i)) continue;
+ const tier = expTiers[s.i];
+
+ const religion = pack.cells.religion[s.center];
+ const isTheocracy =
+ (religion && pack.religions[religion].expansion === "state") ||
+ (P(0.1) &&
+ ["Organized", "Cult"].includes(pack.religions[religion].type));
+ const isAnarchy = P(0.01 - tier / 500);
+
+ if (isTheocracy) s.form = "Theocracy";
+ else if (isAnarchy) s.form = "Anarchy";
+ else s.form = s.type === "Naval" ? rw(naval) : rw(generic);
+
+ const selectForm = (s: any, tier: number) => {
+ const base = pack.cultures[s.culture].base;
+
+ if (s.form === "Monarchy") {
+ const form = monarchy[tier];
+ // Default name depends on exponent tier, some culture bases have special names for tiers
+ if (s.diplomacy) {
+ if (
+ form === "Duchy" &&
+ s.neighbors.length > 1 &&
+ rand(6) < s.neighbors.length &&
+ s.diplomacy.includes("Vassal")
+ )
+ return "Marches"; // some vassal duchies on borderland
+ if (base === 1 && P(0.3) && s.diplomacy.includes("Vassal"))
+ return "Dominion"; // English vassals
+ if (P(0.3) && s.diplomacy.includes("Vassal")) return "Protectorate"; // some vassals
+ }
+
+ if (base === 31 && (form === "Empire" || form === "Kingdom"))
+ return "Khanate"; // Mongolian
+ if (base === 16 && form === "Principality") return "Beylik"; // Turkic
+ if (base === 5 && (form === "Empire" || form === "Kingdom"))
+ return "Tsardom"; // Ruthenian
+ if (base === 16 && (form === "Empire" || form === "Kingdom"))
+ return "Khaganate"; // Turkic
+ if (base === 12 && (form === "Kingdom" || form === "Grand Duchy"))
+ return "Shogunate"; // Japanese
+ if ([18, 17].includes(base) && form === "Empire") return "Caliphate"; // Arabic, Berber
+ if (base === 18 && (form === "Grand Duchy" || form === "Duchy"))
+ return "Emirate"; // Arabic
+ if (base === 7 && (form === "Grand Duchy" || form === "Duchy"))
+ return "Despotate"; // Greek
+ if (base === 31 && (form === "Grand Duchy" || form === "Duchy"))
+ return "Ulus"; // Mongolian
+ if (base === 16 && (form === "Grand Duchy" || form === "Duchy"))
+ return "Horde"; // Turkic
+ if (base === 24 && (form === "Grand Duchy" || form === "Duchy"))
+ return "Satrapy"; // Iranian
+ return form;
+ }
+
+ if (s.form === "Republic") {
+ // Default name is from weighted array, special case for small states with only 1 burg
+ if (tier < 2 && s.burgs === 1) {
+ if (
+ trimVowels(s.name) === trimVowels(pack.burgs[s.capital].name!)
+ ) {
+ s.name = pack.burgs[s.capital].name;
+ return "Free City";
+ }
+ if (P(0.3)) return "City-state";
+ }
+ return rw(republic);
+ }
+
+ if (s.form === "Union") return rw(union);
+ if (s.form === "Anarchy") return rw(anarchy);
+
+ if (s.form === "Theocracy") {
+ // European
+ if ([0, 1, 2, 3, 4, 6, 8, 9, 13, 15, 20].includes(base)) {
+ if (P(0.1)) return `Divine ${monarchy[tier]}`;
+ if (tier < 2 && P(0.5)) return "Diocese";
+ if (tier < 2 && P(0.5)) return "Bishopric";
+ }
+ if (P(0.9) && [7, 5].includes(base)) {
+ // Greek, Ruthenian
+ if (tier < 2) return "Eparchy";
+ if (tier === 2) return "Exarchate";
+ if (tier > 2) return "Patriarchate";
+ }
+ if (P(0.9) && [21, 16].includes(base)) return "Imamah"; // Nigerian, Turkish
+ if (tier > 2 && P(0.8) && [18, 17, 28].includes(base))
+ return "Caliphate"; // Arabic, Berber, Swahili
+ return rw(theocracy);
+ }
+ };
+
+ s.formName = selectForm(s, tier);
+ s.fullName = this.getFullName(s);
+ }
+
+ TIME && console.timeEnd("defineStateForms");
+ }
+
+ getFullName(state: State) {
+ // state forms requiring Adjective + Name, all other forms use scheme Form + Of + Name
+ const adjForms = [
+ "Empire",
+ "Sultanate",
+ "Khaganate",
+ "Shogunate",
+ "Caliphate",
+ "Despotate",
+ "Theocracy",
+ "Oligarchy",
+ "Union",
+ "Confederation",
+ "Trade Company",
+ "League",
+ "Tetrarchy",
+ "Triumvirate",
+ "Diarchy",
+ "Horde",
+ "Marches",
+ ];
+ if (!state.formName) return state.name;
+ if (!state.name && state.formName) return `The ${state.formName}`;
+ const adjName =
+ adjForms.includes(state.formName) && !/-| /.test(state.name);
+ return adjName
+ ? `${getAdjective(state.name)} ${state.formName}`
+ : `${state.formName} of ${state.name}`;
+ }
+}
+
+window.States = new StatesModule();
diff --git a/src/types/PackedGraph.ts b/src/types/PackedGraph.ts
index 4df397e4..26bfab5d 100644
--- a/src/types/PackedGraph.ts
+++ b/src/types/PackedGraph.ts
@@ -2,6 +2,7 @@ import type { Burg } from "../modules/burgs-generator";
import type { Culture } from "../modules/cultures-generator";
import type { PackedGraphFeature } from "../modules/features";
import type { River } from "../modules/river-generator";
+import type { State } from "../modules/states-generator";
type TypedArray =
| Uint8Array
@@ -48,6 +49,7 @@ export interface PackedGraph {
rivers: River[];
features: PackedGraphFeature[];
burgs: Burg[];
- states: any[];
+ states: State[];
cultures: Culture[];
+ religions: any[];
}