more state types

This commit is contained in:
Azgaar 2021-07-11 01:18:39 +03:00
parent 805684f79e
commit c8eccfb0e7
2 changed files with 413 additions and 271 deletions

View file

@ -2537,6 +2537,7 @@
<option value="Diarchy">Diarchy</option> <option value="Diarchy">Diarchy</option>
<option value="Federation">Federation</option> <option value="Federation">Federation</option>
<option value="Free City">Free City</option> <option value="Free City">Free City</option>
<option value="Most Serene Republic">Most Serene Republic</option>
<option value="Oligarchy">Oligarchy</option> <option value="Oligarchy">Oligarchy</option>
<option value="Protectorate">Protectorate</option> <option value="Protectorate">Protectorate</option>
<option value="Republic">Republic</option> <option value="Republic">Republic</option>
@ -2559,10 +2560,17 @@
<option value="Tribes">United Tribes</option> <option value="Tribes">United Tribes</option>
</optgroup> </optgroup>
<optgroup label="Theocracy"> <optgroup label="Theocracy">
<option value="Bishopric">Bishopric</option>
<option value="Brotherhood">Brotherhood</option> <option value="Brotherhood">Brotherhood</option>
<option value="Caliphate">Caliphate</option> <option value="Caliphate">Caliphate</option>
<option value="Diocese">Diocese</option> <option value="Diocese">Diocese</option>
<option value="Divine Duchy">Divine Duchy</option>
<option value="Divine Grand Duchy">Divine Grand Duchy</option>
<option value="Divine Principality">Divine Principality</option>
<option value="Divine Kingdom">Divine Kingdom</option>
<option value="Divine Empire">Divine Empire</option>
<option value="Eparchy">Eparchy</option> <option value="Eparchy">Eparchy</option>
<option value="Holy State">Holy State</option>
<option value="Imamah">Imamah</option> <option value="Imamah">Imamah</option>
<option value="Theocracy">Theocracy</option> <option value="Theocracy">Theocracy</option>
</optgroup> </optgroup>

View file

@ -1,17 +1,18 @@
(function (global, factory) { (function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof exports === "object" && typeof module !== "undefined" ? (module.exports = factory()) : typeof define === "function" && define.amd ? define(factory) : (global.BurgsAndStates = factory());
typeof define === 'function' && define.amd ? define(factory) : })(this, function () {
(global.BurgsAndStates = factory()); "use strict";
}(this, (function () { 'use strict';
const generate = function () { const generate = function () {
const cells = pack.cells, cultures = pack.cultures, n = cells.i.length; const cells = pack.cells,
cultures = pack.cultures,
n = cells.i.length;
cells.burg = new Uint16Array(n); // cell burg cells.burg = new Uint16Array(n); // cell burg
cells.road = new Uint16Array(n); // cell road power cells.road = new Uint16Array(n); // cell road power
cells.crossroad = new Uint16Array(n); // cell crossroad power cells.crossroad = new Uint16Array(n); // cell crossroad power
const burgs = pack.burgs = placeCapitals(); const burgs = (pack.burgs = placeCapitals());
pack.states = createStates(); pack.states = createStates();
const capitalRoutes = Routes.getRoads(); const capitalRoutes = Routes.getRoads();
@ -32,7 +33,7 @@
drawBurgs(); drawBurgs();
function placeCapitals() { function placeCapitals() {
TIME && console.time('placeCapitals'); TIME && console.time("placeCapitals");
let count = +regionsInput.value; let count = +regionsInput.value;
let burgs = [0]; let burgs = [0];
@ -41,15 +42,21 @@
if (sorted.length < count * 10) { if (sorted.length < count * 10) {
count = Math.floor(sorted.length / 10); count = Math.floor(sorted.length / 10);
if (!count) {WARN && console.warn(`There is no populated cells. Cannot generate states`); return burgs;} if (!count) {
else {WARN && console.warn(`Not enough populated cells (${sorted.length}). Will generate only ${count} states`);} WARN && console.warn(`There is no populated cells. Cannot generate states`);
return burgs;
} else {
WARN && console.warn(`Not enough populated cells (${sorted.length}). Will generate only ${count} states`);
}
} }
let burgsTree = d3.quadtree(); let burgsTree = d3.quadtree();
let spacing = (graphWidth + graphHeight) / 2 / count; // min distance between capitals let spacing = (graphWidth + graphHeight) / 2 / count; // min distance between capitals
for (let i = 0; burgs.length <= count; i++) { for (let i = 0; burgs.length <= count; i++) {
const cell = sorted[i], x = cells.p[cell][0], y = cells.p[cell][1]; const cell = sorted[i],
x = cells.p[cell][0],
y = cells.p[cell][1];
if (burgsTree.find(x, y, spacing) === undefined) { if (burgsTree.find(x, y, spacing) === undefined) {
burgs.push({cell, x, y}); burgs.push({cell, x, y});
@ -59,18 +66,18 @@
if (i === sorted.length - 1) { if (i === sorted.length - 1) {
WARN && console.warn("Cannot place capitals with current spacing. Trying again with reduced spacing"); WARN && console.warn("Cannot place capitals with current spacing. Trying again with reduced spacing");
burgsTree = d3.quadtree(); burgsTree = d3.quadtree();
i = -1, burgs = [0], spacing /= 1.2; (i = -1), (burgs = [0]), (spacing /= 1.2);
} }
} }
burgs[0] = burgsTree; burgs[0] = burgsTree;
TIME && console.timeEnd('placeCapitals'); TIME && console.timeEnd("placeCapitals");
return burgs; return burgs;
} }
// For each capital create a state // For each capital create a state
function createStates() { function createStates() {
TIME && console.time('createStates'); TIME && console.time("createStates");
const states = [{i: 0, name: "Neutrals"}]; const states = [{i: 0, name: "Neutrals"}];
const colors = getColors(burgs.length - 1); const colors = getColors(burgs.length - 1);
@ -96,28 +103,30 @@
cells.burg[b.cell] = i; cells.burg[b.cell] = i;
}); });
TIME && console.timeEnd('createStates'); TIME && console.timeEnd("createStates");
return states; return states;
} }
// place secondary settlements based on geo and economical evaluation // place secondary settlements based on geo and economical evaluation
function placeTowns() { function placeTowns() {
TIME && console.time('placeTowns'); TIME && console.time("placeTowns");
const score = new Int16Array(cells.s.map(s => s * gauss(1, 3, 0, 20, 3))); // a bit randomized cell score for towns placement const score = new Int16Array(cells.s.map(s => s * gauss(1, 3, 0, 20, 3))); // a bit randomized cell score for towns placement
const sorted = cells.i.filter(i => !cells.burg[i] && score[i] > 0 && cells.culture[i]).sort((a, b) => score[b] - score[a]); // filtered and sorted array of indexes const sorted = cells.i.filter(i => !cells.burg[i] && score[i] > 0 && cells.culture[i]).sort((a, b) => score[b] - score[a]); // filtered and sorted array of indexes
const desiredNumber = manorsInput.value == 1000 ? rn(sorted.length / 5 / (grid.points.length / 10000) ** .8) : manorsInput.valueAsNumber; const desiredNumber = manorsInput.value == 1000 ? rn(sorted.length / 5 / (grid.points.length / 10000) ** 0.8) : manorsInput.valueAsNumber;
const burgsNumber = Math.min(desiredNumber, sorted.length); // towns to generate const burgsNumber = Math.min(desiredNumber, sorted.length); // towns to generate
let burgsAdded = 0; let burgsAdded = 0;
const burgsTree = burgs[0]; const burgsTree = burgs[0];
let spacing = (graphWidth + graphHeight) / 150 / (burgsNumber ** .7 / 66); // min distance between towns let spacing = (graphWidth + graphHeight) / 150 / (burgsNumber ** 0.7 / 66); // min distance between towns
while (burgsAdded < burgsNumber && spacing > 1) { while (burgsAdded < burgsNumber && spacing > 1) {
for (let i = 0; burgsAdded < burgsNumber && i < sorted.length; i++) { for (let i = 0; burgsAdded < burgsNumber && i < sorted.length; i++) {
if (cells.burg[sorted[i]]) continue; if (cells.burg[sorted[i]]) continue;
const cell = sorted[i], x = cells.p[cell][0], y = cells.p[cell][1]; const cell = sorted[i],
const s = spacing * gauss(1, .3, .2, 2, 2); // randomize to make placement not uniform x = cells.p[cell][0],
y = cells.p[cell][1];
const s = spacing * gauss(1, 0.3, 0.2, 2, 2); // randomize to make placement not uniform
if (burgsTree.find(x, y, s) !== undefined) continue; // to close to existing burg if (burgsTree.find(x, y, s) !== undefined) continue; // to close to existing burg
const burg = burgs.length; const burg = burgs.length;
const culture = cells.culture[cell]; const culture = cells.culture[cell];
@ -127,7 +136,7 @@
cells.burg[cell] = burg; cells.burg[cell] = burg;
burgsAdded++; burgsAdded++;
} }
spacing *= .5; spacing *= 0.5;
} }
if (manorsInput.value != 1000 && burgsAdded < desiredNumber) { if (manorsInput.value != 1000 && burgsAdded < desiredNumber) {
@ -135,14 +144,17 @@
} }
burgs[0] = {name: undefined}; // do not store burgsTree anymore burgs[0] = {name: undefined}; // do not store burgsTree anymore
TIME && console.timeEnd('placeTowns'); TIME && console.timeEnd("placeTowns");
}
} }
};
// define burg coordinates, coa, port status and define details // define burg coordinates, coa, port status and define details
const specifyBurgs = function () { const specifyBurgs = function () {
TIME && console.time("specifyBurgs"); TIME && console.time("specifyBurgs");
const cells = pack.cells, vertices = pack.vertices, features = pack.features, temp = grid.cells.temp; const cells = pack.cells,
vertices = pack.vertices,
features = pack.features,
temp = grid.cells.temp;
for (const b of pack.burgs) { for (const b of pack.burgs) {
if (!b.i || b.lock) continue; if (!b.i || b.lock) continue;
@ -158,7 +170,7 @@
} else b.port = 0; } else b.port = 0;
// define burg population (keep urbanization at about 10% rate) // define burg population (keep urbanization at about 10% rate)
b.population = rn(Math.max((cells.s[i] + cells.road[i] / 2) / 8 + b.i / 1000 + i % 100 / 1000, .1), 3); b.population = rn(Math.max((cells.s[i] + cells.road[i] / 2) / 8 + b.i / 1000 + (i % 100) / 1000, 0.1), 3);
if (b.capital) b.population = rn(b.population * 1.3, 3); // increase capital population if (b.capital) b.population = rn(b.population * 1.3, 3); // increase capital population
if (b.port) { if (b.port) {
@ -169,24 +181,26 @@
} }
// add random factor // add random factor
b.population = rn(b.population * gauss(2,3,.6,20,3), 3); b.population = rn(b.population * gauss(2, 3, 0.6, 20, 3), 3);
// shift burgs on rivers semi-randomly and just a bit // shift burgs on rivers semi-randomly and just a bit
if (!b.port && cells.r[i]) { if (!b.port && cells.r[i]) {
const shift = Math.min(cells.fl[i] / 150, 1); const shift = Math.min(cells.fl[i] / 150, 1);
if (i%2) b.x = rn(b.x + shift, 2); else b.x = rn(b.x - shift, 2); if (i % 2) b.x = rn(b.x + shift, 2);
if (cells.r[i]%2) b.y = rn(b.y + shift, 2); else b.y = rn(b.y - shift, 2); else b.x = rn(b.x - shift, 2);
if (cells.r[i] % 2) b.y = rn(b.y + shift, 2);
else b.y = rn(b.y - shift, 2);
} }
// define emblem // define emblem
const state = pack.states[b.state]; const state = pack.states[b.state];
const stateCOA = state.coa; const stateCOA = state.coa;
let kinship = .25; let kinship = 0.25;
if (b.capital) kinship += .1; if (b.capital) kinship += 0.1;
else if (b.port) kinship -= .1; else if (b.port) kinship -= 0.1;
if (b.culture !== state.culture) kinship -= .25; if (b.culture !== state.culture) kinship -= 0.25;
b.type = getType(i, b.port); b.type = getType(i, b.port);
const type = b.capital && P(.2) ? "Capital" : b.type === "Generic" ? "City" : b.type; const type = b.capital && P(0.2) ? "Capital" : b.type === "Generic" ? "City" : b.type;
b.coa = COA.generate(stateCOA, kinship, null, type); b.coa = COA.generate(stateCOA, kinship, null, type);
b.coa.shield = COA.getShield(b.culture, b.state); b.coa.shield = COA.getShield(b.culture, b.state);
} }
@ -200,7 +214,7 @@
} }
TIME && console.timeEnd("specifyBurgs"); TIME && console.timeEnd("specifyBurgs");
} };
const getType = function (i, port) { const getType = function (i, port) {
const cells = pack.cells; const cells = pack.cells;
@ -215,21 +229,23 @@
} }
return "Generic"; return "Generic";
} };
const defineBurgFeatures = function (newburg) { const defineBurgFeatures = function (newburg) {
const cells = pack.cells; const cells = pack.cells;
pack.burgs.filter(b => newburg ? b.i == newburg.i : (b.i && !b.removed)).forEach(b => { pack.burgs
.filter(b => (newburg ? b.i == newburg.i : b.i && !b.removed))
.forEach(b => {
const pop = b.population; const pop = b.population;
b.citadel = b.capital || pop > 50 && P(.75) || P(.5) ? 1 : 0; b.citadel = b.capital || (pop > 50 && P(0.75)) || P(0.5) ? 1 : 0;
b.plaza = pop > 50 || pop > 30 && P(.75) || pop > 10 && P(.5) || P(.25) ? 1 : 0; b.plaza = pop > 50 || (pop > 30 && P(0.75)) || (pop > 10 && P(0.5)) || P(0.25) ? 1 : 0;
b.walls = b.capital || pop > 30 || pop > 20 && P(.75) || pop > 10 && P(.5) || P(.2) ? 1 : 0; b.walls = b.capital || pop > 30 || (pop > 20 && P(0.75)) || (pop > 10 && P(0.5)) || P(0.2) ? 1 : 0;
b.shanty = pop > 30 || pop > 20 && P(.75) || b.walls && P(.75) ? 1 : 0; b.shanty = pop > 30 || (pop > 20 && P(0.75)) || (b.walls && P(0.75)) ? 1 : 0;
const religion = cells.religion[b.cell]; const religion = cells.religion[b.cell];
const theocracy = pack.states[b.state].form === "Theocracy"; const theocracy = pack.states[b.state].form === "Theocracy";
b.temple = religion && theocracy || pop > 50 || pop > 35 && P(.75) || pop > 20 && P(.5) ? 1 : 0; b.temple = (religion && theocracy) || pop > 50 || (pop > 35 && P(0.75)) || (pop > 20 && P(0.5)) ? 1 : 0;
}); });
} };
const drawBurgs = function () { const drawBurgs = function () {
TIME && console.time("drawBurgs"); TIME && console.time("drawBurgs");
@ -247,18 +263,40 @@
const capitalAnchors = anchors.selectAll("#cities"); const capitalAnchors = anchors.selectAll("#cities");
const caSize = capitalAnchors.attr("size") || 2; const caSize = capitalAnchors.attr("size") || 2;
capitalIcons.selectAll("circle").data(capitals).enter() capitalIcons
.append("circle").attr("id", d => "burg"+d.i).attr("data-id", d => d.i) .selectAll("circle")
.attr("cx", d => d.x).attr("cy", d => d.y).attr("r", capitalSize); .data(capitals)
.enter()
.append("circle")
.attr("id", d => "burg" + d.i)
.attr("data-id", d => d.i)
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", capitalSize);
capitalLabels.selectAll("text").data(capitals).enter() capitalLabels
.append("text").attr("id", d => "burgLabel"+d.i).attr("data-id", d => d.i) .selectAll("text")
.attr("x", d => d.x).attr("y", d => d.y).attr("dy", `${capitalSize * -1.5}px`).text(d => d.name); .data(capitals)
.enter()
.append("text")
.attr("id", d => "burgLabel" + d.i)
.attr("data-id", d => d.i)
.attr("x", d => d.x)
.attr("y", d => d.y)
.attr("dy", `${capitalSize * -1.5}px`)
.text(d => d.name);
capitalAnchors.selectAll("use").data(capitals.filter(c => c.port)).enter() capitalAnchors
.append("use").attr("xlink:href", "#icon-anchor").attr("data-id", d => d.i) .selectAll("use")
.attr("x", d => rn(d.x - caSize * .47, 2)).attr("y", d => rn(d.y - caSize * .47, 2)) .data(capitals.filter(c => c.port))
.attr("width", caSize).attr("height", caSize); .enter()
.append("use")
.attr("xlink:href", "#icon-anchor")
.attr("data-id", d => d.i)
.attr("x", d => rn(d.x - caSize * 0.47, 2))
.attr("y", d => rn(d.y - caSize * 0.47, 2))
.attr("width", caSize)
.attr("height", caSize);
// towns // towns
const towns = pack.burgs.filter(b => b.i && !b.capital); const towns = pack.burgs.filter(b => b.i && !b.capital);
@ -268,21 +306,43 @@
const townsAnchors = anchors.selectAll("#towns"); const townsAnchors = anchors.selectAll("#towns");
const taSize = townsAnchors.attr("size") || 1; const taSize = townsAnchors.attr("size") || 1;
townIcons.selectAll("circle").data(towns).enter() townIcons
.append("circle").attr("id", d => "burg"+d.i).attr("data-id", d => d.i) .selectAll("circle")
.attr("cx", d => d.x).attr("cy", d => d.y).attr("r", townSize); .data(towns)
.enter()
.append("circle")
.attr("id", d => "burg" + d.i)
.attr("data-id", d => d.i)
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", townSize);
townLabels.selectAll("text").data(towns).enter() townLabels
.append("text").attr("id", d => "burgLabel"+d.i).attr("data-id", d => d.i) .selectAll("text")
.attr("x", d => d.x).attr("y", d => d.y).attr("dy", `${townSize * -1.5}px`).text(d => d.name); .data(towns)
.enter()
.append("text")
.attr("id", d => "burgLabel" + d.i)
.attr("data-id", d => d.i)
.attr("x", d => d.x)
.attr("y", d => d.y)
.attr("dy", `${townSize * -1.5}px`)
.text(d => d.name);
townsAnchors.selectAll("use").data(towns.filter(c => c.port)).enter() townsAnchors
.append("use").attr("xlink:href", "#icon-anchor").attr("data-id", d => d.i) .selectAll("use")
.attr("x", d => rn(d.x - taSize * .47, 2)).attr("y", d => rn(d.y - taSize * .47, 2)) .data(towns.filter(c => c.port))
.attr("width", taSize).attr("height", taSize); .enter()
.append("use")
.attr("xlink:href", "#icon-anchor")
.attr("data-id", d => d.i)
.attr("x", d => rn(d.x - taSize * 0.47, 2))
.attr("y", d => rn(d.y - taSize * 0.47, 2))
.attr("width", taSize)
.attr("height", taSize);
TIME && console.timeEnd("drawBurgs"); TIME && console.timeEnd("drawBurgs");
} };
// growth algorithm to assign cells to states like we did for cultures // growth algorithm to assign cells to states like we did for cultures
const expandStates = function () { const expandStates = function () {
@ -292,9 +352,11 @@
cells.state = new Uint16Array(cells.i.length); cells.state = new Uint16Array(cells.i.length);
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const cost = []; const cost = [];
const neutral = cells.i.length / 5000 * 2500 * neutralInput.value * statesNeutral.value; // limit cost for state growth const neutral = (cells.i.length / 5000) * 2500 * neutralInput.value * statesNeutral.value; // limit cost for state growth
states.filter(s => s.i && !s.removed).forEach(s => { states
.filter(s => s.i && !s.removed)
.forEach(s => {
const capitalCell = burgs[s.capital].cell; const capitalCell = burgs[s.capital].cell;
cells.state[capitalCell] = s.i; cells.state[capitalCell] = s.i;
const cultureCenter = cultures[s.culture].center; const cultureCenter = cultures[s.culture].center;
@ -330,7 +392,7 @@
}); });
} }
burgs.filter(b => b.i && !b.removed).forEach(b => b.state = cells.state[b.cell]); // assign state to burgs burgs.filter(b => b.i && !b.removed).forEach(b => (b.state = cells.state[b.cell])); // assign state to burgs
function getBiomeCost(b, biome, type) { function getBiomeCost(b, biome, type) {
if (b === biome) return 10; // tiny penalty for native biome if (b === biome) return 10; // tiny penalty for native biome
@ -354,7 +416,7 @@
function getRiverCost(r, i, type) { function getRiverCost(r, i, type) {
if (type === "River") return r ? 0 : 100; // penalty for river cultures if (type === "River") return r ? 0 : 100; // penalty for river cultures
if (!r) return 0; // no penalty for others if there is no river if (!r) return 0; // no penalty for others if there is no river
return Math.min(Math.max(cells.fl[i] / 10, 20), 100) // river penalty from 20 to 100 based on flux return Math.min(Math.max(cells.fl[i] / 10, 20), 100); // river penalty from 20 to 100 based on flux
} }
function getTypeCost(t, type) { function getTypeCost(t, type) {
@ -365,11 +427,12 @@
} }
TIME && console.timeEnd("expandStates"); TIME && console.timeEnd("expandStates");
} };
const normalizeStates = function () { const normalizeStates = function () {
TIME && console.time("normalizeStates"); TIME && console.time("normalizeStates");
const cells = pack.cells, burgs = pack.burgs; const cells = pack.cells,
burgs = pack.burgs;
for (const i of cells.i) { for (const i of cells.i) {
if (cells.h[i] < 20 || cells.burg[i]) continue; // do not overwrite burgs if (cells.h[i] < 20 || cells.burg[i]) continue; // do not overwrite burgs
@ -383,12 +446,12 @@
cells.state[i] = cells.state[adversaries[0]]; cells.state[i] = cells.state[adversaries[0]];
} }
TIME && console.timeEnd("normalizeStates"); TIME && console.timeEnd("normalizeStates");
} };
// Resets the cultures of all burgs and states to their // Resets the cultures of all burgs and states to their
// cell or center cell's (respectively) culture. // cell or center cell's (respectively) culture.
const updateCultures = function () { const updateCultures = function () {
TIME && console.time('updateCulturesForBurgsAndStates'); TIME && console.time("updateCulturesForBurgsAndStates");
// Assign the culture associated with the burgs cell. // Assign the culture associated with the burgs cell.
pack.burgs = pack.burgs.map((burg, index) => { pack.burgs = pack.burgs.map((burg, index) => {
@ -408,13 +471,15 @@
return {...state, culture: pack.cells.culture[state.center]}; return {...state, culture: pack.cells.culture[state.center]};
}); });
TIME && console.timeEnd('updateCulturesForBurgsAndStates'); TIME && console.timeEnd("updateCulturesForBurgsAndStates");
} };
// calculate and draw curved state labels for a list of states // calculate and draw curved state labels for a list of states
const drawStateLabels = function (list) { const drawStateLabels = function (list) {
TIME && console.time("drawStateLabels"); TIME && console.time("drawStateLabels");
const cells = pack.cells, features = pack.features, states = pack.states; const cells = pack.cells,
features = pack.features,
states = pack.states;
const paths = []; // text paths const paths = []; // text paths
lineGen.curve(d3.curveBundle.beta(1)); lineGen.curve(d3.curveBundle.beta(1));
@ -432,7 +497,8 @@
paths.push([s.i, relaxed]); paths.push([s.i, relaxed]);
function getHull(start, state, maxLake) { function getHull(start, state, maxLake) {
const queue = [start], hull = new Set(); const queue = [start],
hull = new Set();
while (queue.length) { while (queue.length) {
const q = queue.pop(); const q = queue.pop();
@ -440,10 +506,16 @@
cells.c[q].forEach(function (c, d) { cells.c[q].forEach(function (c, d) {
const passableLake = features[cells.f[c]].type === "lake" && features[cells.f[c]].cells < maxLake; const passableLake = features[cells.f[c]].type === "lake" && features[cells.f[c]].cells < maxLake;
if (cells.b[c] || (cells.state[c] !== state && !passableLake)) {hull.add(cells.v[q][d]); return;} if (cells.b[c] || (cells.state[c] !== state && !passableLake)) {
hull.add(cells.v[q][d]);
return;
}
const nC = cells.c[c].filter(n => cells.state[n] === state); const nC = cells.c[c].filter(n => cells.state[n] === state);
const intersected = common(nQ, nC).length const intersected = common(nQ, nC).length;
if (hull.size > 20 && !intersected && !passableLake) {hull.add(cells.v[q][d]); return;} if (hull.size > 20 && !intersected && !passableLake) {
hull.add(cells.v[q][d]);
return;
}
if (used[c]) return; if (used[c]) return;
used[c] = 1; used[c] = 1;
queue.push(c); queue.push(c);
@ -462,17 +534,20 @@
const pointsInside = d3.range(c.p.length).filter(i => inside[i]); const pointsInside = d3.range(c.p.length).filter(i => inside[i]);
if (!pointsInside.length) return [0]; if (!pointsInside.length) return [0];
const h = c.p.length < 200 ? 0 : c.p.length < 600 ? .5 : 1; // power of horyzontality shift const h = c.p.length < 200 ? 0 : c.p.length < 600 ? 0.5 : 1; // power of horyzontality shift
const end = pointsInside[d3.scan(pointsInside, (a, b) => (c.p[a][0] - c.p[b][0]) + (Math.abs(c.p[a][1] - y) - Math.abs(c.p[b][1] - y)) * h)]; // left point const end = pointsInside[d3.scan(pointsInside, (a, b) => c.p[a][0] - c.p[b][0] + (Math.abs(c.p[a][1] - y) - Math.abs(c.p[b][1] - y)) * h)]; // left point
const start = pointsInside[d3.scan(pointsInside, (a, b) => (c.p[b][0] - c.p[a][0]) - (Math.abs(c.p[b][1] - y) - Math.abs(c.p[a][1] - y)) * h)]; // right point const start = pointsInside[d3.scan(pointsInside, (a, b) => c.p[b][0] - c.p[a][0] - (Math.abs(c.p[b][1] - y) - Math.abs(c.p[a][1] - y)) * h)]; // right point
// connect leftmost and rightmost points with shortest path // connect leftmost and rightmost points with shortest path
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const cost = [], from = []; const cost = [],
from = [];
queue.queue({e: start, p: 0}); queue.queue({e: start, p: 0});
while (queue.length) { while (queue.length) {
const next = queue.dequeue(), n = next.e, p = next.p; const next = queue.dequeue(),
n = next.e,
p = next.p;
if (n === end) break; if (n === end) break;
for (const v of c.v[n]) { for (const v of c.v[n]) {
@ -494,11 +569,11 @@
} }
return chain; return chain;
} }
} }
void function drawLabels() { void (function drawLabels() {
const g = labels.select("#states"), t = defs.select("#textPaths"); const g = labels.select("#states"),
t = defs.select("#textPaths");
const displayed = layerIsOn("toggleLabels"); const displayed = layerIsOn("toggleLabels");
if (!displayed) toggleLabels(); if (!displayed) toggleLabels();
@ -521,31 +596,37 @@
} }
const path = p[1].length > 1 ? lineGen(p[1]) : `M${p[1][0][0] - 50},${p[1][0][1]}h${100}`; const path = p[1].length > 1 ? lineGen(p[1]) : `M${p[1][0][0] - 50},${p[1][0][1]}h${100}`;
const textPath = t.append("path").attr("d", path).attr("id", "textPath_stateLabel"+id); const textPath = t
.append("path")
.attr("d", path)
.attr("id", "textPath_stateLabel" + id);
const pathLength = p[1].length > 1 ? textPath.node().getTotalLength() / letterLength : 0; // path length in letters const pathLength = p[1].length > 1 ? textPath.node().getTotalLength() / letterLength : 0; // path length in letters
let lines = [], ratio = 100; let lines = [],
ratio = 100;
if (pathLength < s.name.length) { if (pathLength < s.name.length) {
// only short name will fit // only short name will fit
lines = splitInTwo(s.name); lines = splitInTwo(s.name);
ratio = Math.max(Math.min(rn(pathLength / lines[0].length * 60), 150), 50); ratio = Math.max(Math.min(rn((pathLength / lines[0].length) * 60), 150), 50);
} else if (pathLength > s.fullName.length * 2.5) { } else if (pathLength > s.fullName.length * 2.5) {
// full name will fit in one line // full name will fit in one line
lines = [s.fullName]; lines = [s.fullName];
ratio = Math.max(Math.min(rn(pathLength / lines[0].length * 70), 170), 70); ratio = Math.max(Math.min(rn((pathLength / lines[0].length) * 70), 170), 70);
} else { } else {
// try miltilined label // try miltilined label
lines = splitInTwo(s.fullName); lines = splitInTwo(s.fullName);
ratio = Math.max(Math.min(rn(pathLength / lines[0].length * 60), 150), 70); ratio = Math.max(Math.min(rn((pathLength / lines[0].length) * 60), 150), 70);
} }
// prolongate path if it's too short // prolongate path if it's too short
if (pathLength && pathLength < lines[0].length) { if (pathLength && pathLength < lines[0].length) {
const points = p[1]; const points = p[1];
const f = points[0], l = points[points.length-1]; const f = points[0],
const dx = l[0] - f[0], dy = l[1] - f[1]; l = points[points.length - 1];
const mod = Math.abs(letterLength * lines[0].length / dx) / 2; const dx = l[0] - f[0],
dy = l[1] - f[1];
const mod = Math.abs((letterLength * lines[0].length) / dx) / 2;
points[0] = [rn(f[0] - dx * mod), rn(f[1] - dy * mod)]; points[0] = [rn(f[0] - dx * mod), rn(f[1] - dy * mod)];
points[points.length - 1] = [rn(l[0] + dx * mod), rn(l[1] + dy * mod)]; points[points.length - 1] = [rn(l[0] + dx * mod), rn(l[1] + dy * mod)];
textPath.attr("d", round(lineGen(points))); textPath.attr("d", round(lineGen(points)));
@ -559,15 +640,21 @@
return `<tspan x="${left}px" dy="${d ? 1 : top}em">${l}</tspan>`; return `<tspan x="${left}px" dy="${d ? 1 : top}em">${l}</tspan>`;
}); });
const el = g.append("text").attr("id", "stateLabel"+id) const el = g
.append("textPath").attr("xlink:href", "#textPath_stateLabel"+id) .append("text")
.attr("startOffset", "50%").attr("font-size", ratio+"%").node(); .attr("id", "stateLabel" + id)
.append("textPath")
.attr("xlink:href", "#textPath_stateLabel" + id)
.attr("startOffset", "50%")
.attr("font-size", ratio + "%")
.node();
el.insertAdjacentHTML("afterbegin", spans.join("")); el.insertAdjacentHTML("afterbegin", spans.join(""));
if (lines.length < 2) return; if (lines.length < 2) return;
// check whether multilined label is generally inside the state. If no, replace with short name label // check whether multilined label is generally inside the state. If no, replace with short name label
const cs = pack.cells.state, b = el.parentNode.getBBox(); const cs = pack.cells.state,
b = el.parentNode.getBBox();
const c1 = () => +cs[findCell(b.x, b.y)] === id; const c1 = () => +cs[findCell(b.x, b.y)] === id;
const c2 = () => +cs[findCell(b.x + b.width / 2, b.y)] === id; const c2 = () => +cs[findCell(b.x + b.width / 2, b.y)] === id;
const c3 = () => +cs[findCell(b.x + b.width, b.y)] === id; const c3 = () => +cs[findCell(b.x + b.width, b.y)] === id;
@ -581,21 +668,22 @@
example.text(name); example.text(name);
const left = example.node().getBBox().width / -2; // x offset const left = example.node().getBBox().width / -2; // x offset
el.innerHTML = `<tspan x="${left}px">${name}</tspan>`; el.innerHTML = `<tspan x="${left}px">${name}</tspan>`;
ratio = Math.max(Math.min(rn(pathLength / name.length * 60), 130), 40); ratio = Math.max(Math.min(rn((pathLength / name.length) * 60), 130), 40);
el.setAttribute("font-size", ratio + "%"); el.setAttribute("font-size", ratio + "%");
}); });
example.remove(); example.remove();
if (!displayed) toggleLabels(); if (!displayed) toggleLabels();
}() })();
TIME && console.timeEnd("drawStateLabels"); TIME && console.timeEnd("drawStateLabels");
} };
// calculate states data like area, population etc. // calculate states data like area, population etc.
const collectStatistics = function () { const collectStatistics = function () {
TIME && console.time("collectStatistics"); TIME && console.time("collectStatistics");
const cells = pack.cells, states = pack.states; const cells = pack.cells,
states = pack.states;
states.forEach(s => { states.forEach(s => {
if (s.removed) return; if (s.removed) return;
s.cells = s.area = s.burgs = s.rural = s.urban = 0; s.cells = s.area = s.burgs = s.rural = s.urban = 0;
@ -626,7 +714,7 @@
}); });
TIME && console.timeEnd("collectStatistics"); TIME && console.timeEnd("collectStatistics");
} };
const assignColors = function () { const assignColors = function () {
TIME && console.time("assignColors"); TIME && console.time("assignColors");
@ -651,37 +739,40 @@
}); });
TIME && console.timeEnd("assignColors"); TIME && console.timeEnd("assignColors");
} };
// generate historical conflicts of each state // generate historical conflicts of each state
const generateCampaigns = function () { const generateCampaigns = function () {
const wars = {"War":6, "Conflict":2, "Campaign":4, "Invasion":2, "Rebellion":2, "Conquest":2, "Intervention":1, "Expedition":1, "Crusade":1}; const wars = {War: 6, Conflict: 2, Campaign: 4, Invasion: 2, Rebellion: 2, Conquest: 2, Intervention: 1, Expedition: 1, Crusade: 1};
pack.states.forEach(s => { pack.states.forEach(s => {
if (!s.i || s.removed) return; if (!s.i || s.removed) return;
const n = s.neighbors.length ? s.neighbors : [0]; const n = s.neighbors.length ? s.neighbors : [0];
s.campaigns = n.map(i => { s.campaigns = n
const name = i && P(.8) ? pack.states[i].name : Names.getCultureShort(s.culture); .map(i => {
const name = i && P(0.8) ? pack.states[i].name : Names.getCultureShort(s.culture);
const start = gauss(options.year - 100, 150, 1, options.year - 6); const start = gauss(options.year - 100, 150, 1, options.year - 6);
const end = start + gauss(4, 5, 1, options.year - start - 1); const end = start + gauss(4, 5, 1, options.year - start - 1);
return {name: getAdjective(name) + " " + rw(wars), start, end}; return {name: getAdjective(name) + " " + rw(wars), start, end};
}).sort((a, b) => a.start - b.start); })
.sort((a, b) => a.start - b.start);
}); });
} };
// generate Diplomatic Relationships // generate Diplomatic Relationships
const generateDiplomacy = function () { const generateDiplomacy = function () {
TIME && console.time("generateDiplomacy"); TIME && console.time("generateDiplomacy");
const cells = pack.cells, states = pack.states; const cells = pack.cells,
const chronicle = states[0].diplomacy = []; states = pack.states;
const chronicle = (states[0].diplomacy = []);
const valid = states.filter(s => s.i && !states.removed); 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 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 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 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 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 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 if (valid.length < 2) return; // no states to renerate relations with
const areaMean = d3.mean(valid.map(s => s.area)); // avarage state area const areaMean = d3.mean(valid.map(s => s.area)); // avarage state area
@ -713,16 +804,22 @@
const suzerain = states[t].diplomacy.indexOf("Vassal"); const suzerain = states[t].diplomacy.indexOf("Vassal");
states[f].diplomacy[t] = states[f].diplomacy[suzerain]; states[f].diplomacy[t] = states[f].diplomacy[suzerain];
continue; continue;
}; }
const naval = states[f].type === "Naval" && states[t].type === "Naval" && cells.f[states[f].center] !== cells.f[states[t].center]; 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 neib = naval ? false : states[f].neighbors.includes(t);
const neibOfNeib = naval || neib ? false : states[f].neighbors.map(n => states[n].neighbors).join("").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); let status = naval ? rw(navals) : neib ? rw(neibs) : neibOfNeib ? rw(neibsOfNeibs) : rw(far);
// add Vassal // add Vassal
if (neib && P(.8) && states[f].area > areaMean && states[t].area < areaMean && states[f].area / states[t].area > 2) status = "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[f].diplomacy[t] = status === "Vassal" ? "Suzerain" : status;
states[t].diplomacy[f] = status; states[t].diplomacy[f] = status;
} }
@ -737,11 +834,14 @@
if (ad.includes("Enemy")) continue; // already at war if (ad.includes("Enemy")) continue; // already at war
// random independent rival // random independent rival
const defender = ra(ad.map((r, d) => r === "Rival" && !states[d].diplomacy.includes("Vassal") ? d : 0).filter(d => d)); 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, dp = states[defender].area * states[defender].expansionism; let ap = states[attacker].area * states[attacker].expansionism,
if (ap < dp * gauss(1.6, .8, 0, 10, 2)) continue; // defender is too strong dp = states[defender].area * states[defender].expansionism;
const an = states[attacker].name, dn = states[defender].name; // names if (ap < dp * gauss(1.6, 0.8, 0, 10, 2)) continue; // defender is too strong
const attackers = [attacker], defenders = [defender]; // attackers and defenders array const an = states[attacker].name,
dn = states[defender].name; // names
const attackers = [attacker],
defenders = [defender]; // attackers and defenders array
const dd = states[defender].diplomacy; // defender relations; const dd = states[defender].diplomacy; // defender relations;
// start a war // start a war
@ -752,16 +852,20 @@
states[defender].campaigns.push({name: `${trimVowels(an)}ian War`, start, end}); states[defender].campaigns.push({name: `${trimVowels(an)}ian War`, start, end});
// attacker vassals join the war // attacker vassals join the war
ad.forEach((r, d) => {if (r === "Suzerain") { ad.forEach((r, d) => {
if (r === "Suzerain") {
attackers.push(d); attackers.push(d);
war.push(`${an}'s vassal ${states[d].name} joined the war on attackers side`); war.push(`${an}'s vassal ${states[d].name} joined the war on attackers side`);
}}); }
});
// defender vassals join the war // defender vassals join the war
dd.forEach((r, d) => {if (r === "Suzerain") { dd.forEach((r, d) => {
if (r === "Suzerain") {
defenders.push(d); defenders.push(d);
war.push(`${dn}'s vassal ${states[d].name} joined the war on defenders side`); 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 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 dp = d3.sum(defenders.map(d => states[d].area * states[d].expansionism)); // defender joined power
@ -769,7 +873,7 @@
// defender allies join // defender allies join
dd.forEach((r, d) => { dd.forEach((r, d) => {
if (r !== "Ally" || states[d].diplomacy.includes("Vassal")) return; if (r !== "Ally" || states[d].diplomacy.includes("Vassal")) return;
if (states[d].diplomacy[attacker] !== "Rival" && ap / dp > (2 * gauss(1.6, .8, 0, 10, 2))) { 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},`; 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}`); war.push(`${reason} ${states[d].name} severed the defense pact with ${dn}`);
dd[d] = states[d].diplomacy[defender] = "Suspicion"; dd[d] = states[d].diplomacy[defender] = "Suspicion";
@ -780,7 +884,10 @@
war.push(`${dn}'s ally ${states[d].name} joined the war on defenders side`); war.push(`${dn}'s ally ${states[d].name} joined the war on defenders side`);
// ally vassals join // ally vassals join
states[d].diplomacy.map((r, d) => r === "Suzerain" ? d : 0).filter(d => d).forEach(v => { states[d].diplomacy
.map((r, d) => (r === "Suzerain" ? d : 0))
.filter(d => d)
.forEach(v => {
defenders.push(v); defenders.push(v);
dp += states[v].area * states[v].expansionism; dp += states[v].area * states[v].expansionism;
war.push(`${states[d].name}'s vassal ${states[v].name} joined the war on defenders side`); war.push(`${states[d].name}'s vassal ${states[v].name} joined the war on defenders side`);
@ -791,16 +898,25 @@
ad.forEach((r, d) => { ad.forEach((r, d) => {
if (r !== "Ally" || states[d].diplomacy.includes("Vassal") || defenders.includes(d)) return; if (r !== "Ally" || states[d].diplomacy.includes("Vassal") || defenders.includes(d)) return;
const name = states[d].name; const name = states[d].name;
if (states[d].diplomacy[defender] !== "Rival" && (P(.2) || ap <= dp * 1.2)) {war.push(`${an}'s ally ${name} avoided entering the war`); return;} if (states[d].diplomacy[defender] !== "Rival" && (P(0.2) || ap <= dp * 1.2)) {
const allies = states[d].diplomacy.map((r, d) => r === "Ally" ? d : 0).filter(d => d); war.push(`${an}'s ally ${name} avoided entering the war`);
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;}; 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); attackers.push(d);
ap += states[d].area * states[d].expansionism; ap += states[d].area * states[d].expansionism;
war.push(`${an}'s ally ${name} joined the war on attackers side`); war.push(`${an}'s ally ${name} joined the war on attackers side`);
// ally vassals join // ally vassals join
states[d].diplomacy.map((r, d) => r === "Suzerain" ? d : 0).filter(d => d).forEach(v => { states[d].diplomacy
.map((r, d) => (r === "Suzerain" ? d : 0))
.filter(d => d)
.forEach(v => {
attackers.push(v); attackers.push(v);
dp += states[v].area * states[v].expansionism; dp += states[v].area * states[v].expansionism;
war.push(`${states[d].name}'s vassal ${states[v].name} joined the war on attackers side`); war.push(`${states[d].name}'s vassal ${states[v].name} joined the war on attackers side`);
@ -808,13 +924,13 @@
}); });
// change relations to Enemy for all participants // change relations to Enemy for all participants
attackers.forEach(a => defenders.forEach(d => states[a].diplomacy[d] = states[d].diplomacy[a] = "Enemy")); attackers.forEach(a => defenders.forEach(d => (states[a].diplomacy[d] = states[d].diplomacy[a] = "Enemy")));
chronicle.push(war); // add a record to diplomatical history chronicle.push(war); // add a record to diplomatical history
} }
TIME && console.timeEnd("generateDiplomacy"); TIME && console.timeEnd("generateDiplomacy");
//console.table(states.map(s => s.diplomacy)); //console.table(states.map(s => s.diplomacy));
} };
// select a forms for listed or all valid states // select a forms for listed or all valid states
const defineStateForms = function (list) { const defineStateForms = function (list) {
@ -826,25 +942,26 @@
const naval = {Monarchy: 25, Republic: 8, Union: 3}; const naval = {Monarchy: 25, Republic: 8, Union: 3};
const median = d3.median(pack.states.map(s => s.area)); 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 ** .4) - 2, 0)]; 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 => { const expTiers = pack.states.map(s => {
let tier = Math.min(Math.floor(s.area / median * 2.6), 4); let tier = Math.min(Math.floor((s.area / median) * 2.6), 4);
if (tier === 4 && s.area < empireMin) tier = 3; if (tier === 4 && s.area < empireMin) tier = 3;
return tier; return tier;
}); });
const monarchy = ["Duchy", "Grand Duchy", "Principality", "Kingdom", "Empire"]; // per expansionism tier const monarchy = ["Duchy", "Grand Duchy", "Principality", "Kingdom", "Empire"]; // per expansionism tier
const republic = {Republic:75, Federation:4, Oligarchy:2, Tetrarchy:1, Triumvirate:1, Diarchy:1, "Trade Company":4, Junta:1}; // weighted random const republic = {Republic: 75, Federation: 4, Oligarchy: 2, "Most Serene Republic": 2, Tetrarchy: 1, Triumvirate: 1, Diarchy: 1, "Trade Company": 4, 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 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}; const theocracy = {Theocracy: 20, Brotherhood: 1, Thearchy: 2, See: 1, "Holy State": 1};
const anarchy = {"Free Territory": 2, Council: 3, Commune: 1, Community: 1}; const anarchy = {"Free Territory": 2, Council: 3, Commune: 1, Community: 1};
for (const s of states) { for (const s of states) {
if (list && !list.includes(s.i)) continue; if (list && !list.includes(s.i)) continue;
const tier = expTiers[s.i];
const religion = pack.cells.religion[s.center]; const religion = pack.cells.religion[s.center];
const isTheocracy = religion && pack.religions[religion].expansion === "state" || (P(.1) && ["Organized", "Cult"].includes(pack.religions[religion].type)); const isTheocracy = (religion && pack.religions[religion].expansion === "state") || (P(0.1) && ["Organized", "Cult"].includes(pack.religions[religion].type));
const isAnarchy = P(.01 - expTiers[s.i]/500); const isAnarchy = P(0.01 - tier / 500);
if (isTheocracy) s.form = "Theocracy"; if (isTheocracy) s.form = "Theocracy";
else if (isAnarchy) s.form = "Anarchy"; else if (isAnarchy) s.form = "Anarchy";
@ -857,11 +974,11 @@
const base = pack.cultures[s.culture].base; const base = pack.cultures[s.culture].base;
if (s.form === "Monarchy") { if (s.form === "Monarchy") {
const form = monarchy[expTiers[s.i]]; const form = monarchy[tier];
// Default name depends on exponent tier, some culture bases have special names for tiers // Default name depends on exponent tier, some culture bases have special names for tiers
if (s.diplomacy) { if (s.diplomacy) {
if (form === "Duchy" && s.neighbors.length > 1 && rand(6) < s.neighbors.length && s.diplomacy.includes("Vassal")) return "Marches"; // some vassal dutchies on borderland if (form === "Duchy" && s.neighbors.length > 1 && rand(6) < s.neighbors.length && s.diplomacy.includes("Vassal")) return "Marches"; // some vassal dutchies on borderland
if (P(.3) && s.diplomacy.includes("Vassal")) return "Protectorate"; // some vassals if (P(0.3) && s.diplomacy.includes("Vassal")) return "Protectorate"; // some vassals
} }
if (base === 16 && (form === "Empire" || form === "Kingdom")) return "Sultanate"; // Turkic if (base === 16 && (form === "Empire" || form === "Kingdom")) return "Sultanate"; // Turkic
@ -879,12 +996,12 @@
if (s.form === "Republic") { if (s.form === "Republic") {
// Default name is from weighted array, special case for small states with only 1 burg // Default name is from weighted array, special case for small states with only 1 burg
if (expTiers[s.i] < 2 && s.burgs === 1) { if (tier < 2 && s.burgs === 1) {
if (trimVowels(s.name) === trimVowels(pack.burgs[s.capital].name)) { if (trimVowels(s.name) === trimVowels(pack.burgs[s.capital].name)) {
s.name = pack.burgs[s.capital].name; s.name = pack.burgs[s.capital].name;
return "Free City"; return "Free City";
} }
if (P(.3)) return "City-state"; if (P(0.3)) return "City-state";
} }
return rw(republic); return rw(republic);
} }
@ -893,16 +1010,21 @@
if (s.form === "Anarchy") return rw(anarchy); if (s.form === "Anarchy") return rw(anarchy);
if (s.form === "Theocracy") { if (s.form === "Theocracy") {
if (P(.5) && [0, 1, 2, 3, 4, 6, 8, 9, 13, 15, 20].includes(base)) return "Diocese"; // Euporean // European
if (P(.9) && [7, 5].includes(base)) return "Eparchy"; // Greek, Ruthenian if ([0, 1, 2, 3, 4, 6, 8, 9, 13, 15, 20].includes(base)) {
if (P(.9) && [21, 16].includes(base)) return "Imamah"; // Nigerian, Turkish if (P(0.1)) return "Divine " + monarchy[tier];
if (P(.8) && [18, 17, 28].includes(base)) return "Caliphate"; // Arabic, Berber, Swahili if (tier < 2 && P(0.5)) return "Diocese";
if (tier < 2 && P(0.5)) return "Bishopric";
}
if (tier < 2 && P(0.9) && [7, 5].includes(base)) return "Eparchy"; // Greek, Ruthenian
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); return rw(theocracy);
} }
} }
TIME && console.timeEnd("defineStateForms"); TIME && console.timeEnd("defineStateForms");
} };
// state forms requiring Adjective + Name, all other forms use scheme Form + Of + Name // 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 adjForms = ["Empire", "Sultanate", "Khaganate", "Shogunate", "Caliphate", "Despotate", "Theocracy", "Oligarchy", "Union", "Confederation", "Trade Company", "League", "Tetrarchy", "Triumvirate", "Diarchy", "Horde", "Marches"];
@ -910,21 +1032,26 @@
const getFullName = function (s) { const getFullName = function (s) {
if (!s.formName) return s.name; if (!s.formName) return s.name;
if (!s.name && s.formName) return "The " + s.formName; if (!s.name && s.formName) return "The " + s.formName;
const adjName = adjForms.includes(s.formName) && !(/-| /).test(s.name); const adjName = adjForms.includes(s.formName) && !/-| /.test(s.name);
return adjName ? `${getAdjective(s.name)} ${s.formName}` : `${s.formName} of ${s.name}`; return adjName ? `${getAdjective(s.name)} ${s.formName}` : `${s.formName} of ${s.name}`;
} };
const generateProvinces = function (regenerate) { const generateProvinces = function (regenerate) {
TIME && console.time("generateProvinces"); TIME && console.time("generateProvinces");
const localSeed = regenerate ? Math.floor(Math.random() * 1e9).toString() : seed; const localSeed = regenerate ? Math.floor(Math.random() * 1e9).toString() : seed;
Math.random = aleaPRNG(localSeed); Math.random = aleaPRNG(localSeed);
const cells = pack.cells, states = pack.states, burgs = pack.burgs; const cells = pack.cells,
const provinces = pack.provinces = [0]; states = pack.states,
burgs = pack.burgs;
const provinces = (pack.provinces = [0]);
cells.province = new Uint16Array(cells.i.length); // cell state cells.province = new Uint16Array(cells.i.length); // cell state
const percentage = +provincesInput.value; const percentage = +provincesInput.value;
if (states.length < 2 || !percentage) {states.forEach(s => s.provinces = []); return;} // no provinces if (states.length < 2 || !percentage) {
const max = percentage == 100 ? 1000 : gauss(20, 5, 5, 100) * percentage ** .5; // max growth states.forEach(s => (s.provinces = []));
return;
} // no provinces
const max = percentage == 100 ? 1000 : gauss(20, 5, 5, 100) * percentage ** 0.5; // max growth
const forms = { const forms = {
Monarchy: {County: 11, Earldom: 3, Shire: 1, Landgrave: 1, Margrave: 1, Barony: 1}, Monarchy: {County: 11, Earldom: 3, Shire: 1, Landgrave: 1, Margrave: 1, Barony: 1},
@ -933,18 +1060,19 @@
Union: {Province: 1, State: 1, Canton: 1, Republic: 1, County: 1, Council: 1}, Union: {Province: 1, State: 1, Canton: 1, Republic: 1, County: 1, Council: 1},
Anarchy: {Council: 1, Commune: 1, Community: 1, Tribe: 1}, Anarchy: {Council: 1, Commune: 1, Community: 1, Tribe: 1},
Wild: {Territory: 10, Land: 5, Region: 2, Tribe: 1, Clan: 1, Dependency: 1, Area: 1} Wild: {Territory: 10, Land: 5, Region: 2, Tribe: 1, Clan: 1, Dependency: 1, Area: 1}
} };
// generate provinces for a selected burgs // generate provinces for a selected burgs
Math.random = aleaPRNG(localSeed); Math.random = aleaPRNG(localSeed);
states.forEach(s => { states.forEach(s => {
s.provinces = []; s.provinces = [];
if (!s.i || s.removed) return; if (!s.i || s.removed) return;
const stateBurgs = burgs.filter(b => b.state === s.i && !b.removed) const stateBurgs = burgs
.sort((a, b) => b.population * gauss(1, .2, .5, 1.5, 3) - a.population) .filter(b => b.state === s.i && !b.removed)
.sort((a, b) => b.population * gauss(1, 0.2, 0.5, 1.5, 3) - a.population)
.sort((a, b) => b.capital - a.capital); .sort((a, b) => b.capital - a.capital);
if (stateBurgs.length < 2) return; // at least 2 provinces are required if (stateBurgs.length < 2) return; // at least 2 provinces are required
const provincesNumber = Math.max(Math.ceil(stateBurgs.length * percentage / 100), 2); const provincesNumber = Math.max(Math.ceil((stateBurgs.length * percentage) / 100), 2);
const form = Object.assign({}, forms[s.form]); const form = Object.assign({}, forms[s.form]);
for (let i = 0; i < provincesNumber; i++) { for (let i = 0; i < provincesNumber; i++) {
@ -953,13 +1081,13 @@
const center = stateBurgs[i].cell; const center = stateBurgs[i].cell;
const burg = stateBurgs[i].i; const burg = stateBurgs[i].i;
const c = stateBurgs[i].culture; const c = stateBurgs[i].culture;
const nameByBurg = P(.5); const nameByBurg = P(0.5);
const name = nameByBurg ? stateBurgs[i].name : Names.getState(Names.getCultureShort(c), c); const name = nameByBurg ? stateBurgs[i].name : Names.getState(Names.getCultureShort(c), c);
const formName = rw(form); const formName = rw(form);
form[formName] += 10; form[formName] += 10;
const fullName = name + " " + formName; const fullName = name + " " + formName;
const color = getMixedColor(s.color); const color = getMixedColor(s.color);
const kinship = nameByBurg ? .8 : .4; const kinship = nameByBurg ? 0.8 : 0.4;
const type = getType(center, burg.port); const type = getType(center, burg.port);
const coa = COA.generate(stateBurgs[i].coa, kinship, null, type); const coa = COA.generate(stateBurgs[i].coa, kinship, null, type);
coa.shield = COA.getShield(c, s.i); coa.shield = COA.getShield(c, s.i);
@ -978,7 +1106,11 @@
}); });
while (queue.length) { while (queue.length) {
const next = queue.dequeue(), n = next.e, p = next.p, province = next.province, state = next.state; const next = queue.dequeue(),
n = next.e,
p = next.p,
province = next.province,
state = next.state;
cells.c[n].forEach(function (e) { cells.c[n].forEach(function (e) {
const land = cells.h[e] >= 20; const land = cells.h[e] >= 20;
if (!land && !cells.t[e]) return; // cannot pass deep ocean if (!land && !cells.t[e]) return; // cannot pass deep ocean
@ -1003,7 +1135,7 @@
if (adversaries.length < 2) continue; if (adversaries.length < 2) continue;
const buddies = neibs.filter(c => c === cells.province[i]).length; const buddies = neibs.filter(c => c === cells.province[i]).length;
if (buddies.length > 2) continue; if (buddies.length > 2) continue;
const competitors = adversaries.map(p => adversaries.reduce((s, v) => v === p ? s+1 : s, 0)); const competitors = adversaries.map(p => adversaries.reduce((s, v) => (v === p ? s + 1 : s), 0));
const max = d3.max(competitors); const max = d3.max(competitors);
if (buddies >= max) continue; if (buddies >= max) continue;
cells.province[i] = adversaries[competitors.indexOf(max)]; cells.province[i] = adversaries[competitors.indexOf(max)];
@ -1023,16 +1155,19 @@
cells.province[center] = province; cells.province[center] = province;
// expand province // expand province
const cost = []; cost[center] = 1; const cost = [];
cost[center] = 1;
queue.queue({e: center, p: 0}); queue.queue({e: center, p: 0});
while (queue.length) { while (queue.length) {
const next = queue.dequeue(), n = next.e, p = next.p; const next = queue.dequeue(),
n = next.e,
p = next.p;
cells.c[n].forEach(function (e) { cells.c[n].forEach(function (e) {
if (cells.province[e]) return; if (cells.province[e]) return;
const land = cells.h[e] >= 20; const land = cells.h[e] >= 20;
if (cells.state[e] && cells.state[e] !== s.i) return; if (cells.state[e] && cells.state[e] !== s.i) return;
const ter = land ? cells.state[e] === s.i ? 3 : 20 : cells.t[e] ? 10 : 30; const ter = land ? (cells.state[e] === s.i ? 3 : 20) : cells.t[e] ? 10 : 30;
const totalCost = p + ter; const totalCost = p + ter;
if (totalCost > max) return; if (totalCost > max) return;
@ -1046,18 +1181,18 @@
// generate "wild" province name // generate "wild" province name
const c = cells.culture[center]; const c = cells.culture[center];
const nameByBurg = burgCell && P(.5); const nameByBurg = burgCell && P(0.5);
const name = nameByBurg ? burgs[burg].name : Names.getState(Names.getCultureShort(c), c); const name = nameByBurg ? burgs[burg].name : Names.getState(Names.getCultureShort(c), c);
const f = pack.features[cells.f[center]]; const f = pack.features[cells.f[center]];
const provCells = stateNoProvince.filter(i => cells.province[i] === province); const provCells = stateNoProvince.filter(i => cells.province[i] === province);
const singleIsle = provCells.length === f.cells && !provCells.find(i => cells.f[i] !== f.i); const singleIsle = provCells.length === f.cells && !provCells.find(i => cells.f[i] !== f.i);
const isleGroup = !singleIsle && !provCells.find(i => pack.features[cells.f[i]].group !== "isle"); const isleGroup = !singleIsle && !provCells.find(i => pack.features[cells.f[i]].group !== "isle");
const colony = !singleIsle && !isleGroup && P(.5) && !isPassable(s.center, center); const colony = !singleIsle && !isleGroup && P(0.5) && !isPassable(s.center, center);
const formName = singleIsle ? "Island" : isleGroup ? "Islands" : colony ? "Colony" : rw(forms["Wild"]); const formName = singleIsle ? "Island" : isleGroup ? "Islands" : colony ? "Colony" : rw(forms["Wild"]);
const fullName = name + " " + formName; const fullName = name + " " + formName;
const color = getMixedColor(s.color); const color = getMixedColor(s.color);
const dominion = colony ? P(.95) : singleIsle || isleGroup ? P(.7) : P(.3); const dominion = colony ? P(0.95) : singleIsle || isleGroup ? P(0.7) : P(0.3);
const kinship = dominion ? 0 : .4; const kinship = dominion ? 0 : 0.4;
const type = getType(center, burgs[burg]?.port); const type = getType(center, burgs[burg]?.port);
const coa = COA.generate(s.coa, kinship, dominion, type); const coa = COA.generate(s.coa, kinship, dominion, type);
coa.shield = COA.getShield(c, s.i); coa.shield = COA.getShield(c, s.i);
@ -1067,7 +1202,9 @@
// check if there is a land way within the same state between two cells // check if there is a land way within the same state between two cells
function isPassable(from, to) { function isPassable(from, to) {
if (cells.f[from] !== cells.f[to]) return false; // on different islands if (cells.f[from] !== cells.f[to]) return false; // on different islands
const queue = [from], used = new Uint8Array(cells.i.length), state = cells.state[from]; const queue = [from],
used = new Uint8Array(cells.i.length),
state = cells.state[from];
while (queue.length) { while (queue.length) {
const current = queue.pop(); const current = queue.pop();
if (current === to) return true; // way is found if (current === to) return true; // way is found
@ -1086,10 +1223,7 @@
}); });
TIME && console.timeEnd("generateProvinces"); TIME && console.timeEnd("generateProvinces");
} };
return {generate, expandStates, normalizeStates, assignColors, return {generate, expandStates, normalizeStates, assignColors, drawBurgs, specifyBurgs, defineBurgFeatures, getType, drawStateLabels, collectStatistics, generateCampaigns, generateDiplomacy, defineStateForms, getFullName, generateProvinces, updateCultures};
drawBurgs, specifyBurgs, defineBurgFeatures, getType, drawStateLabels, collectStatistics, });
generateCampaigns, generateDiplomacy, defineStateForms, getFullName, generateProvinces, updateCultures};
})));