mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2026-02-04 17:41:23 +01:00
Merge branch 'master' into refactor/migrate-routes
This commit is contained in:
commit
642ce54208
6 changed files with 829 additions and 643 deletions
|
|
@ -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
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
@ -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/states-generator.js?v=1.107.0"></script>
|
|
||||||
<script defer src="modules/provinces-generator.js?v=1.106.0"></script>
|
<script defer src="modules/provinces-generator.js?v=1.106.0"></script>
|
||||||
<script defer src="modules/religions-generator.js?v=1.106.0"></script>
|
<script defer src="modules/religions-generator.js?v=1.106.0"></script>
|
||||||
<script defer src="modules/military-generator.js?v=1.107.0"></script>
|
<script defer src="modules/military-generator.js?v=1.107.0"></script>
|
||||||
|
|
|
||||||
|
|
@ -9,3 +9,4 @@ import "./burgs-generator";
|
||||||
import "./biomes";
|
import "./biomes";
|
||||||
import "./cultures-generator";
|
import "./cultures-generator";
|
||||||
import "./routes-generator";
|
import "./routes-generator";
|
||||||
|
import "./states-generator";
|
||||||
|
|
|
||||||
|
|
@ -223,7 +223,7 @@ class NamesGenerator {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
getState(name: string, culture: number, base: number): string {
|
getState(name: string, culture: number, base?: number): string {
|
||||||
if (name === undefined) {
|
if (name === undefined) {
|
||||||
ERROR && console.error("Please define a base name");
|
ERROR && console.error("Please define a base name");
|
||||||
return "ERROR";
|
return "ERROR";
|
||||||
|
|
|
||||||
824
src/modules/states-generator.ts
Normal file
824
src/modules/states-generator.ts
Normal file
|
|
@ -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<number>[] = [];
|
||||||
|
|
||||||
|
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 generate 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
|
||||||
|
ap += 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();
|
||||||
|
|
@ -3,6 +3,7 @@ import type { Culture } from "../modules/cultures-generator";
|
||||||
import type { PackedGraphFeature } from "../modules/features";
|
import type { PackedGraphFeature } from "../modules/features";
|
||||||
import type { River } from "../modules/river-generator";
|
import type { River } from "../modules/river-generator";
|
||||||
import type { Route } from "../modules/routes-generator";
|
import type { Route } from "../modules/routes-generator";
|
||||||
|
import type { State } from "../modules/states-generator";
|
||||||
|
|
||||||
type TypedArray =
|
type TypedArray =
|
||||||
| Uint8Array
|
| Uint8Array
|
||||||
|
|
@ -51,7 +52,8 @@ export interface PackedGraph {
|
||||||
rivers: River[];
|
rivers: River[];
|
||||||
features: PackedGraphFeature[];
|
features: PackedGraphFeature[];
|
||||||
burgs: Burg[];
|
burgs: Burg[];
|
||||||
states: any[];
|
states: State[];
|
||||||
cultures: Culture[];
|
cultures: Culture[];
|
||||||
routes: Route[];
|
routes: Route[];
|
||||||
|
religions: any[];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue