|
diff --git a/main.js b/main.js
index 9c6e1dfa..33482d49 100644
--- a/main.js
+++ b/main.js
@@ -357,7 +357,7 @@ function applyDefaultBiomesSystem() {
'Wetland'
];
const color = ['#466eab', '#fbe79f', '#b5b887', '#d2d082', '#c8d68f', '#b6d95d', '#29bc56', '#7dcb35', '#409c43', '#4b6b32', '#96784b', '#d5e7eb', '#0b9131'];
- const habitability = [0, 4, 10, 22, 30, 50, 100, 80, 90, 12, 4, 0, 12];
+ const habitability = [0, 4, 10, 25, 40, 60, 100, 80, 90, 12, 4, 0, 12];
const iconsDensity = [0, 3, 2, 120, 120, 120, 120, 150, 150, 100, 5, 0, 150];
const icons = [
{},
@@ -1411,45 +1411,62 @@ function getBiomeId(moisture, temperature, height) {
return biomesData.biomesMartix[m][t];
}
-// assess cells suitability to calculate population and rand cells for culture center and burgs placement
+// assess cells suitability to calculate population and rang cells for culture center and burgs placement
function rankCells() {
TIME && console.time('rankCells');
const {cells, features} = pack;
cells.s = new Int16Array(cells.i.length); // cell suitability array
cells.pop = new Float32Array(cells.i.length); // cell population array
- const flMean = d3.median(cells.fl.filter((f) => f)) || 0,
- flMax = d3.max(cells.fl) + d3.max(cells.conf); // to normalize flux
+ const flMean = d3.median(cells.fl.filter((f) => f)) || 0;
+ const flMax = d3.max(cells.fl) + d3.max(cells.conf); // to normalize flux
const areaMean = d3.mean(cells.area); // to adjust population by cell area
+ const getResValue = (i) => (cells.resource[i] ? Resources.get(cells.resource[i])?.value : 0);
+ const resBonuses = [];
+ const POP_BALANCER = 1.5; // contant to ballance population to desired number not changing ranking
for (const i of cells.i) {
+ if (cells.b[i]) continue; // avoid adding burgs on map border
if (cells.h[i] < 20) continue; // no population in water
- let s = +biomesData.habitability[cells.biome[i]]; // base suitability derived from biome habitability
+ let s = biomesData.habitability[cells.biome[i]] / 10; // base suitability derived from biome habitability
if (!s) continue; // uninhabitable biomes has 0 suitability
- if (flMean) s += normalize(cells.fl[i] + cells.conf[i], flMean, flMax) * 250; // big rivers and confluences are valued
- s -= (cells.h[i] - 50) / 5; // low elevation is valued, high is not;
+
+ if (flMean) s += normalize(cells.fl[i] + cells.conf[i], flMean, flMax) * 50; // big rivers and confluences are valued
+ s -= (cells.h[i] - 50) / 25; // low elevation is valued, high is not;
if (cells.t[i] === 1) {
- if (cells.r[i]) s += 15; // estuary is valued
- const feature = features[cells.f[cells.haven[i]]];
- if (feature.type === 'lake') {
- if (feature.group === 'freshwater') s += 30;
- else if (feature.group == 'salt') s += 10;
- else if (feature.group == 'frozen') s += 1;
- else if (feature.group == 'dry') s -= 5;
- else if (feature.group == 'sinkhole') s -= 5;
- else if (feature.group == 'lava') s -= 30;
+ if (cells.r[i]) s += 3; // estuary is valued
+ const {type, group} = features[cells.f[cells.haven[i]]];
+ if (type === 'lake') {
+ if (group === 'freshwater') s += 5;
+ else if (group == 'salt') s += 2;
+ else if (group == 'dry') s -= 1;
+ else if (group == 'sinkhole') s -= 1;
+ else if (group == 'lava') s -= 6;
} else {
- s += 5; // ocean coast is valued
- if (cells.harbor[i] === 1) s += 20; // safe sea harbor is valued
+ s += 1; // ocean coast is valued
+ if (cells.harbor[i] === 1) s += 4; // safe sea harbor is valued
}
}
- cells.s[i] = s / 5; // general population rate
+ // add bonus for resources around
+ const cellRes = getResValue(i);
+ const neibRes = d3.mean(cells.c[i].map((c) => getResValue(c)));
+ const resBonus = (cellRes ? cellRes + 10 : 0) + neibRes;
+ resBonuses.push(resBonus);
+
// cell rural population is suitability adjusted by cell area
- cells.pop[i] = cells.s[i] > 0 ? (cells.s[i] * cells.area[i]) / areaMean : 0;
+ cells.pop[i] = s > 0 ? (s * POP_BALANCER * cells.area[i]) / areaMean : 0;
+ cells.s[i] = s + resBonus;
+
+ debug.append('text').attr('x', cells.p[i][0]).attr('y', cells.p[i][1]).text(cells.s[i]);
}
+ console.log(resBonuses);
+ console.log(d3.max(resBonuses));
+ console.log(d3.mean(resBonuses));
+ console.log(d3.median(resBonuses.map((v) => rn(v))));
+
TIME && console.timeEnd('rankCells');
}
diff --git a/modules/burgs-and-states.js b/modules/burgs-and-states.js
index 5524e81d..b10d5e0a 100644
--- a/modules/burgs-and-states.js
+++ b/modules/burgs-and-states.js
@@ -1,17 +1,18 @@
(function (global, factory) {
- typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
- typeof define === 'function' && define.amd ? define(factory) :
- (global.BurgsAndStates = factory());
-}(this, (function () { 'use strict';
+ typeof exports === 'object' && typeof module !== 'undefined' ? (module.exports = factory()) : typeof define === 'function' && define.amd ? define(factory) : (global.BurgsAndStates = factory());
+})(this, function () {
+ 'use strict';
- const generate = function() {
- const cells = pack.cells, cultures = pack.cultures, n = cells.i.length;
+ const generate = function () {
+ const cells = pack.cells,
+ cultures = pack.cultures,
+ n = cells.i.length;
cells.burg = new Uint16Array(n); // cell burg
cells.road = new Uint16Array(n); // cell road power
cells.crossroad = new Uint16Array(n); // cell crossroad power
- const burgs = pack.burgs = placeCapitals();
+ const burgs = (pack.burgs = placeCapitals());
pack.states = createStates();
const capitalRoutes = Routes.getRoads();
@@ -36,20 +37,26 @@
let count = +regionsInput.value;
let burgs = [0];
- const score = new Int16Array(cells.s.map(s => s * Math.random())); // cell score for capitals placement
- const sorted = cells.i.filter(i => score[i] > 0 && cells.culture[i]).sort((a, b) => score[b] - score[a]); // filtered and sorted array of indexes
+ const score = new Int16Array(cells.s.map((s) => s * Math.random())); // cell score for capitals placement
+ const sorted = cells.i.filter((i) => score[i] > 0 && cells.culture[i]).sort((a, b) => score[b] - score[a]); // filtered and sorted array of indexes
if (sorted.length < count * 10) {
count = Math.floor(sorted.length / 10);
- if (!count) {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`);}
+ if (!count) {
+ 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 spacing = (graphWidth + graphHeight) / 2 / count; // min distance between capitals
- for (let i=0; burgs.length <= count; i++) {
- const cell = sorted[i], x = cells.p[cell][0], y = cells.p[cell][1];
+ for (let i = 0; burgs.length <= count; i++) {
+ const cell = sorted[i],
+ x = cells.p[cell][0],
+ y = cells.p[cell][1];
if (burgsTree.find(x, y, spacing) === undefined) {
burgs.push({cell, x, y});
@@ -57,9 +64,9 @@
}
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();
- i = -1, burgs = [0], spacing /= 1.2;
+ (i = -1), (burgs = [0]), (spacing /= 1.2);
}
}
@@ -71,10 +78,10 @@
// For each capital create a state
function createStates() {
TIME && console.time('createStates');
- const states = [{i:0, name: "Neutrals"}];
- const colors = getColors(burgs.length-1);
+ const states = [{i: 0, name: 'Neutrals'}];
+ const colors = getColors(burgs.length - 1);
- burgs.forEach(function(b, i) {
+ burgs.forEach(function (b, i) {
if (!i) return; // skip first element
// burgs data
@@ -86,13 +93,13 @@
// states data
const expansionism = rn(Math.random() * powerInput.value + 1, 1);
- const basename = b.name.length < 9 && b.cell%5 === 0 ? b.name : Names.getCultureShort(b.culture);
+ const basename = b.name.length < 9 && b.cell % 5 === 0 ? b.name : Names.getCultureShort(b.culture);
const name = Names.getState(basename, b.culture);
const type = cultures[b.culture].type;
const coa = COA.generate(null, null, null, type);
coa.shield = COA.getShield(b.culture, null);
- states.push({i, color: colors[i-1], name, expansionism, capital: i, type, center: b.cell, culture: b.culture, coa});
+ states.push({i, color: colors[i - 1], name, expansionism, capital: i, type, center: b.cell, culture: b.culture, coa});
cells.burg[b.cell] = i;
});
@@ -103,46 +110,51 @@
// place secondary settlements based on geo and economical evaluation
function 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 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 score = new Int16Array(cells.s.map((s) => s * Math.random())); // a bit randomized cell score for towns placement
+ const sorted = cells.i.filter((i) => score[i] > 0 && cells.culture[i] && !cells.burg[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 / 6 / (grid.points.length / 10000) ** 0.8) : +manorsInput.value;
const burgsNumber = Math.min(desiredNumber, sorted.length); // towns to generate
let burgsAdded = 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) {
- 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;
- const cell = sorted[i], x = cells.p[cell][0], y = cells.p[cell][1];
- const s = spacing * gauss(1, .3, .2, 2, 2); // randomize to make placement not uniform
+ const cell = sorted[i],
+ 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
const burg = burgs.length;
const culture = cells.culture[cell];
const name = Names.getCulture(culture);
- burgs.push({cell, x, y, state: 0, i: burg, culture, name, capital: 0, feature:cells.f[cell]});
+ burgs.push({cell, x, y, state: 0, i: burg, culture, name, capital: 0, feature: cells.f[cell]});
burgsTree.add([x, y]);
cells.burg[cell] = burg;
burgsAdded++;
}
- spacing *= .5;
+ spacing *= 0.5;
}
if (manorsInput.value != 1000 && burgsAdded < desiredNumber) {
ERROR && console.error(`Cannot place all burgs. Requested ${desiredNumber}, placed ${burgsAdded}`);
}
- burgs[0] = {name:undefined}; // do not store burgsTree anymore
+ burgs[0] = {name: undefined}; // do not store burgsTree anymore
TIME && console.timeEnd('placeTowns');
}
- }
+ };
// define burg coordinates, coa, port status and define details
- const specifyBurgs = function() {
- TIME && console.time("specifyBurgs");
- const cells = pack.cells, vertices = pack.vertices, features = pack.features, temp = grid.cells.temp;
+ const specifyBurgs = function () {
+ TIME && console.time('specifyBurgs');
+ const cells = pack.cells,
+ vertices = pack.vertices,
+ features = pack.features,
+ temp = grid.cells.temp;
for (const b of pack.burgs) {
if (!b.i || b.lock) continue;
@@ -158,157 +170,207 @@
} else b.port = 0;
// 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.port) {
b.population = b.population * 1.3; // increase port population
- const e = cells.v[i].filter(v => vertices.c[v].some(c => c === cells.haven[i])); // vertices of common edge
+ const e = cells.v[i].filter((v) => vertices.c[v].some((c) => c === cells.haven[i])); // vertices of common edge
b.x = rn((vertices.p[e[0]][0] + vertices.p[e[1]][0]) / 2, 2);
b.y = rn((vertices.p[e[0]][1] + vertices.p[e[1]][1]) / 2, 2);
}
// 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
if (!b.port && cells.r[i]) {
- 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 (cells.r[i]%2) b.y = rn(b.y + shift, 2); else b.y = rn(b.y - shift, 2);
+ 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 (cells.r[i] % 2) b.y = rn(b.y + shift, 2);
+ else b.y = rn(b.y - shift, 2);
}
// define emblem
const state = pack.states[b.state];
const stateCOA = state.coa;
- let kinship = .25;
- if (b.capital) kinship += .1;
- else if (b.port) kinship -= .1;
- if (b.culture !== state.culture) kinship -= .25;
+ let kinship = 0.25;
+ if (b.capital) kinship += 0.1;
+ else if (b.port) kinship -= 0.1;
+ if (b.culture !== state.culture) kinship -= 0.25;
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.shield = COA.getShield(b.culture, b.state);
}
// de-assign port status if it's the only one on feature
- const ports = pack.burgs.filter(b => !b.removed && b.port > 0);
+ const ports = pack.burgs.filter((b) => !b.removed && b.port > 0);
for (const f of features) {
if (!f.i || f.land || f.border) continue;
- const featurePorts = ports.filter(b => b.port === f.i);
+ const featurePorts = ports.filter((b) => b.port === f.i);
if (featurePorts.length === 1) featurePorts[0].port = 0;
}
- TIME && console.timeEnd("specifyBurgs");
- }
+ TIME && console.timeEnd('specifyBurgs');
+ };
- const getType = function(i, port) {
+ const getType = function (i, port) {
const cells = pack.cells;
- if (port) return "Naval";
- if (cells.haven[i] && pack.features[cells.f[cells.haven[i]]].type === "lake") return "Lake";
- if (cells.h[i] > 60) return "Highland";
- if (cells.r[i] && cells.r[i].length > 100 && cells.r[i].length >= pack.rivers[0].length) return "River";
+ if (port) return 'Naval';
+ if (cells.haven[i] && pack.features[cells.f[cells.haven[i]]].type === 'lake') return 'Lake';
+ if (cells.h[i] > 60) return 'Highland';
+ if (cells.r[i] && cells.r[i].length > 100 && cells.r[i].length >= pack.rivers[0].length) return 'River';
if (!cells.burg[i] || pack.burgs[cells.burg[i]].population < 6) {
- if (population < 5 && [1, 2, 3, 4].includes(cells.biome[i])) return "Nomadic";
- if (cells.biome[i] > 4 && cells.biome[i] < 10) return "Hunting";
+ if (population < 5 && [1, 2, 3, 4].includes(cells.biome[i])) return 'Nomadic';
+ if (cells.biome[i] > 4 && cells.biome[i] < 10) return 'Hunting';
}
- return "Generic";
- }
+ return 'Generic';
+ };
- const defineBurgFeatures = function(newburg) {
+ const defineBurgFeatures = function (newburg) {
const cells = pack.cells;
- pack.burgs.filter(b => newburg ? b.i == newburg.i : (b.i && !b.removed)).forEach(b => {
- const pop = b.population;
- b.citadel = b.capital || pop > 50 && P(.75) || P(.5) ? 1 : 0;
- b.plaza = pop > 50 || pop > 30 && P(.75) || pop > 10 && P(.5) || P(.25) ? 1 : 0;
- b.walls = b.capital || pop > 30 || pop > 20 && P(.75) || pop > 10 && P(.5) || P(.2) ? 1 : 0;
- b.shanty = pop > 30 || pop > 20 && P(.75) || b.walls && P(.75) ? 1 : 0;
- const religion = cells.religion[b.cell];
- const theocracy = pack.states[b.state].form === "Theocracy";
- b.temple = religion && theocracy || pop > 50 || pop > 35 && P(.75) || pop > 20 && P(.5) ? 1 : 0;
- });
- }
+ pack.burgs
+ .filter((b) => (newburg ? b.i == newburg.i : b.i && !b.removed))
+ .forEach((b) => {
+ const pop = b.population;
+ b.citadel = b.capital || (pop > 50 && P(0.75)) || P(0.5) ? 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(0.75)) || (pop > 10 && P(0.5)) || P(0.2) ? 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 theocracy = pack.states[b.state].form === 'Theocracy';
+ b.temple = (religion && theocracy) || pop > 50 || (pop > 35 && P(0.75)) || (pop > 20 && P(0.5)) ? 1 : 0;
+ });
+ };
- const drawBurgs = function() {
- TIME && console.time("drawBurgs");
+ const drawBurgs = function () {
+ TIME && console.time('drawBurgs');
// remove old data
- burgIcons.selectAll("circle").remove();
- burgLabels.selectAll("text").remove();
- icons.selectAll("use").remove();
+ burgIcons.selectAll('circle').remove();
+ burgLabels.selectAll('text').remove();
+ icons.selectAll('use').remove();
// capitals
- const capitals = pack.burgs.filter(b => b.capital);
- const capitalIcons = burgIcons.select("#cities");
- const capitalLabels = burgLabels.select("#cities");
- const capitalSize = capitalIcons.attr("size") || 1;
- const capitalAnchors = anchors.selectAll("#cities");
- const caSize = capitalAnchors.attr("size") || 2;
+ const capitals = pack.burgs.filter((b) => b.capital);
+ const capitalIcons = burgIcons.select('#cities');
+ const capitalLabels = burgLabels.select('#cities');
+ const capitalSize = capitalIcons.attr('size') || 1;
+ const capitalAnchors = anchors.selectAll('#cities');
+ const caSize = capitalAnchors.attr('size') || 2;
- capitalIcons.selectAll("circle").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);
+ capitalIcons
+ .selectAll('circle')
+ .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()
- .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);
+ capitalLabels
+ .selectAll('text')
+ .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()
- .append("use").attr("xlink:href", "#icon-anchor").attr("data-id", d => d.i)
- .attr("x", d => rn(d.x - caSize * .47, 2)).attr("y", d => rn(d.y - caSize * .47, 2))
- .attr("width", caSize).attr("height", caSize);
+ capitalAnchors
+ .selectAll('use')
+ .data(capitals.filter((c) => c.port))
+ .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
- const towns = pack.burgs.filter(b => b.i && !b.capital);
- const townIcons = burgIcons.select("#towns");
- const townLabels = burgLabels.select("#towns");
- const townSize = townIcons.attr("size") || 0.5;
- const townsAnchors = anchors.selectAll("#towns");
- const taSize = townsAnchors.attr("size") || 1;
+ const towns = pack.burgs.filter((b) => b.i && !b.capital);
+ const townIcons = burgIcons.select('#towns');
+ const townLabels = burgLabels.select('#towns');
+ const townSize = townIcons.attr('size') || 0.5;
+ const townsAnchors = anchors.selectAll('#towns');
+ const taSize = townsAnchors.attr('size') || 1;
- townIcons.selectAll("circle").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);
+ townIcons
+ .selectAll('circle')
+ .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()
- .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);
+ townLabels
+ .selectAll('text')
+ .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()
- .append("use").attr("xlink:href", "#icon-anchor").attr("data-id", d => d.i)
- .attr("x", d => rn(d.x - taSize * .47, 2)).attr("y", d => rn(d.y - taSize * .47, 2))
- .attr("width", taSize).attr("height", taSize);
+ townsAnchors
+ .selectAll('use')
+ .data(towns.filter((c) => c.port))
+ .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
- const expandStates = function() {
- TIME && console.time("expandStates");
+ const expandStates = function () {
+ TIME && console.time('expandStates');
const {cells, states, cultures, burgs} = pack;
cells.state = new Uint16Array(cells.i.length);
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
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 => {
- const capitalCell = burgs[s.capital].cell;
- cells.state[capitalCell] = s.i;
- const cultureCenter = cultures[s.culture].center;
- const b = cells.biome[cultureCenter]; // state native biome
- queue.queue({e:s.center, p:0, s:s.i, b});
- cost[s.center] = 1;
- });
+ states
+ .filter((s) => s.i && !s.removed)
+ .forEach((s) => {
+ const capitalCell = burgs[s.capital].cell;
+ cells.state[capitalCell] = s.i;
+ const cultureCenter = cultures[s.culture].center;
+ const b = cells.biome[cultureCenter]; // state native biome
+ queue.queue({e: s.center, p: 0, s: s.i, b});
+ cost[s.center] = 1;
+ });
while (queue.length) {
const next = queue.dequeue();
const {e, p, s, b} = next;
const {type, culture} = states[s];
- cells.c[e].forEach(e => {
+ cells.c[e].forEach((e) => {
if (cells.state[e] && e === states[cells.state[e]].center) return; // do not overwrite capital cells
const cultureCost = culture === cells.culture[e] ? -9 : 100;
@@ -325,65 +387,66 @@
if (!cost[e] || totalCost < cost[e]) {
if (cells.h[e] >= 20) cells.state[e] = s; // assign state to cell
cost[e] = totalCost;
- queue.queue({e, p:totalCost, s, b});
+ queue.queue({e, p: totalCost, s, b});
}
});
}
- 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) {
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
+ 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 (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 (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 (type === 'River') return r ? 0 : 100; // penalty for river cultures
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) {
- 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
+ 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");
- }
+ TIME && console.timeEnd('expandStates');
+ };
- const normalizeStates = function() {
- TIME && console.time("normalizeStates");
- const cells = pack.cells, burgs = pack.burgs;
+ const normalizeStates = function () {
+ TIME && console.time('normalizeStates');
+ const cells = pack.cells,
+ burgs = pack.burgs;
for (const i of cells.i) {
if (cells.h[i] < 20 || cells.burg[i]) continue; // do not overwrite burgs
- 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 => cells.state[c] !== cells.state[i]);
+ 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) => cells.state[c] !== cells.state[i]);
if (adversaries.length < 2) continue;
- const buddies = neibs.filter(c => cells.state[c] === cells.state[i]);
+ const buddies = neibs.filter((c) => 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");
- }
+ TIME && console.timeEnd('normalizeStates');
+ };
// Resets the cultures of all burgs and states to their
// cell or center cell's (respectively) culture.
@@ -391,30 +454,32 @@
TIME && console.time('updateCulturesForBurgsAndStates');
// Assign the culture associated with the burgs cell.
- pack.burgs = pack.burgs.map( (burg, index) => {
+ pack.burgs = pack.burgs.map((burg, index) => {
// Ignore metadata burg
- if(index === 0) {
+ if (index === 0) {
return burg;
}
return {...burg, culture: pack.cells.culture[burg.cell]};
});
// Assign the culture associated with the states' center cell.
- pack.states = pack.states.map( (state, index) => {
+ pack.states = pack.states.map((state, index) => {
// Ignore neutrals state
- if(index === 0) {
+ if (index === 0) {
return state;
}
return {...state, culture: pack.cells.culture[state.center]};
});
TIME && console.timeEnd('updateCulturesForBurgsAndStates');
- }
+ };
// calculate and draw curved state labels for a list of states
- const drawStateLabels = function(list) {
- TIME && console.time("drawStateLabels");
- const cells = pack.cells, features = pack.features, states = pack.states;
+ const drawStateLabels = function (list) {
+ TIME && console.time('drawStateLabels');
+ const cells = pack.cells,
+ features = pack.features,
+ states = pack.states;
const paths = []; // text paths
lineGen.curve(d3.curveBundle.beta(1));
@@ -424,26 +489,33 @@
const visualCenter = findCell(s.pole[0], s.pole[1]);
const start = cells.state[visualCenter] === s.i ? visualCenter : s.center;
const hull = getHull(start, s.i, s.cells / 10);
- const points = [...hull].map(v => pack.vertices.p[v]);
+ const points = [...hull].map((v) => pack.vertices.p[v]);
const delaunay = Delaunator.from(points);
const voronoi = new Voronoi(delaunay, points, points.length);
const chain = connectCenters(voronoi.vertices, s.pole[1]);
- const relaxed = chain.map(i => voronoi.vertices.p[i]).filter((p, i) => i%15 === 0 || i+1 === chain.length);
+ const relaxed = chain.map((i) => voronoi.vertices.p[i]).filter((p, i) => i % 15 === 0 || i + 1 === chain.length);
paths.push([s.i, relaxed]);
function getHull(start, state, maxLake) {
- const queue = [start], hull = new Set();
+ const queue = [start],
+ hull = new Set();
while (queue.length) {
const q = queue.pop();
- const nQ = cells.c[q].filter(c => cells.state[c] === state);
+ const nQ = cells.c[q].filter((c) => cells.state[c] === state);
- cells.c[q].forEach(function(c, d) {
- 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;}
- const nC = cells.c[c].filter(n => cells.state[n] === state);
- const intersected = common(nQ, nC).length
- if (hull.size > 20 && !intersected && !passableLake) {hull.add(cells.v[q][d]); return;}
+ cells.c[q].forEach(function (c, d) {
+ 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;
+ }
+ const nC = cells.c[c].filter((n) => cells.state[n] === state);
+ const intersected = common(nQ, nC).length;
+ if (hull.size > 20 && !intersected && !passableLake) {
+ hull.add(cells.v[q][d]);
+ return;
+ }
if (used[c]) return;
used[c] = 1;
queue.push(c);
@@ -455,24 +527,27 @@
function connectCenters(c, y) {
// check if vertex is inside the area
- const inside = c.p.map(function(p) {
+ const inside = c.p.map(function (p) {
if (p[0] <= 0 || p[1] <= 0 || p[0] >= graphWidth || p[1] >= graphHeight) return false; // out of the screen
return used[findCell(p[0], p[1])];
});
- 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];
- const h = c.p.length < 200 ? 0 : c.p.length < 600 ? .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 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 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 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
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
- const cost = [], from = [];
+ const cost = [],
+ from = [];
queue.queue({e: start, p: 0});
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;
for (const v of c.v[n]) {
@@ -494,80 +569,92 @@
}
return chain;
}
-
}
- void function drawLabels() {
- const g = labels.select("#states"), t = defs.select("#textPaths");
- const displayed = layerIsOn("toggleLabels");
+ void (function drawLabels() {
+ const g = labels.select('#states'),
+ t = defs.select('#textPaths');
+ const displayed = layerIsOn('toggleLabels');
if (!displayed) toggleLabels();
if (!list) {
// remove all labels and textpaths
- g.selectAll("text").remove();
+ g.selectAll('text').remove();
t.selectAll("path[id*='stateLabel']").remove();
}
- const example = g.append("text").attr("x", 0).attr("x", 0).text("Average");
+ const example = g.append('text').attr('x', 0).attr('x', 0).text('Average');
const letterLength = example.node().getComputedTextLength() / 7; // average length of 1 letter
- paths.forEach(p => {
+ paths.forEach((p) => {
const id = p[0];
const s = states[p[0]];
if (list) {
- t.select("#textPath_stateLabel"+id).remove();
- g.select("#stateLabel"+id).remove();
+ t.select('#textPath_stateLabel' + id).remove();
+ g.select('#stateLabel' + id).remove();
}
- 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 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 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) {
// only short name will fit
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) {
// full name will fit in one line
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 {
// try miltilined label
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
if (pathLength && pathLength < lines[0].length) {
const points = p[1];
- const f = points[0], l = points[points.length-1];
- const dx = l[0] - f[0], dy = l[1] - f[1];
- const mod = Math.abs(letterLength * lines[0].length / dx) / 2;
+ const f = points[0],
+ l = points[points.length - 1];
+ 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[points.length-1] = [rn(l[0] + dx * mod), rn(l[1] + dy * mod)];
- textPath.attr("d", round(lineGen(points)));
+ points[points.length - 1] = [rn(l[0] + dx * mod), rn(l[1] + dy * mod)];
+ textPath.attr('d', round(lineGen(points)));
}
- example.attr("font-size", ratio+"%");
+ example.attr('font-size', ratio + '%');
const top = (lines.length - 1) / -2; // y offset
const spans = lines.map((l, d) => {
example.text(l);
const left = example.node().getBBox().width / -2; // x offset
- return `${l}`;
+ return `${l}`;
});
- const el = g.append("text").attr("id", "stateLabel"+id)
- .append("textPath").attr("xlink:href", "#textPath_stateLabel"+id)
- .attr("startOffset", "50%").attr("font-size", ratio+"%").node();
+ const el = g
+ .append('text')
+ .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;
// 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 c2 = () => +cs[findCell(b.x + b.width / 2, b.y)] === id;
const c3 = () => +cs[findCell(b.x + b.width, b.y)] === id;
@@ -581,22 +668,23 @@
example.text(name);
const left = example.node().getBBox().width / -2; // x offset
el.innerHTML = `${name}`;
- ratio = Math.max(Math.min(rn(pathLength / name.length * 60), 130), 40);
- el.setAttribute("font-size", ratio+"%");
+ ratio = Math.max(Math.min(rn((pathLength / name.length) * 60), 130), 40);
+ el.setAttribute('font-size', ratio + '%');
});
example.remove();
if (!displayed) toggleLabels();
- }()
+ })();
- TIME && console.timeEnd("drawStateLabels");
- }
+ TIME && console.timeEnd('drawStateLabels');
+ };
// calculate states data like area, population etc.
- const collectStatistics = function() {
- TIME && console.time("collectStatistics");
- const cells = pack.cells, states = pack.states;
- states.forEach(s => {
+ const collectStatistics = function () {
+ TIME && console.time('collectStatistics');
+ const cells = pack.cells,
+ states = pack.states;
+ states.forEach((s) => {
if (s.removed) return;
s.cells = s.area = s.burgs = s.rural = s.urban = 0;
s.neighbors = new Set();
@@ -607,7 +695,7 @@
const s = cells.state[i];
// check for neighboring states
- cells.c[i].filter(c => cells.h[c] >= 20 && cells.state[c] !== s).forEach(c => states[s].neighbors.add(cells.state[c]));
+ cells.c[i].filter((c) => cells.h[c] >= 20 && cells.state[c] !== s).forEach((c) => states[s].neighbors.add(cells.state[c]));
// collect stats
states[s].cells += 1;
@@ -620,159 +708,175 @@
}
// convert neighbors Set object into array
- states.forEach(s => {
+ states.forEach((s) => {
if (!s.neighbors) return;
s.neighbors = Array.from(s.neighbors);
});
- TIME && console.timeEnd("collectStatistics");
- }
+ TIME && console.timeEnd('collectStatistics');
+ };
- const assignColors = function() {
- TIME && console.time("assignColors");
- const colors = ["#66c2a5", "#fc8d62", "#8da0cb", "#e78ac3", "#a6d854", "#ffd92f"]; // d3.schemeSet2;
+ const assignColors = function () {
+ TIME && console.time('assignColors');
+ const colors = ['#66c2a5', '#fc8d62', '#8da0cb', '#e78ac3', '#a6d854', '#ffd92f']; // d3.schemeSet2;
// assin basic color using greedy coloring algorithm
- pack.states.forEach(s => {
+ pack.states.forEach((s) => {
if (!s.i || s.removed) return;
const neibs = s.neighbors;
- s.color = colors.find(c => neibs.every(n => pack.states[n].color !== c));
+ s.color = colors.find((c) => neibs.every((n) => pack.states[n].color !== c));
if (!s.color) s.color = getRandomColor();
colors.push(colors.shift());
});
// randomize each already used color a bit
- colors.forEach(c => {
- const sameColored = pack.states.filter(s => s.color === c);
+ colors.forEach((c) => {
+ const sameColored = pack.states.filter((s) => s.color === c);
sameColored.forEach((s, d) => {
if (!d) return;
s.color = getMixedColor(s.color);
});
});
- TIME && console.timeEnd("assignColors");
- }
+ TIME && console.timeEnd('assignColors');
+ };
// generate historical conflicts of each state
- const generateCampaigns = function() {
- const wars = {"War":6, "Conflict":2, "Campaign":4, "Invasion":2, "Rebellion":2, "Conquest":2, "Intervention":1, "Expedition":1, "Crusade":1};
+ const generateCampaigns = function () {
+ 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;
const n = s.neighbors.length ? s.neighbors : [0];
- s.campaigns = n.map(i => {
- const name = i && P(.8) ? pack.states[i].name : Names.getCultureShort(s.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);
+ s.campaigns = n
+ .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 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 Diplomatic Relationships
- const generateDiplomacy = function() {
- TIME && console.time("generateDiplomacy");
- const cells = pack.cells, states = pack.states;
- const chronicle = states[0].diplomacy = [];
- const valid = states.filter(s => s.i && !states.removed);
+ const generateDiplomacy = function () {
+ TIME && console.time('generateDiplomacy');
+ const cells = pack.cells,
+ states = pack.states;
+ 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
+ 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
+ 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)); // avarage state area
+ const areaMean = d3.mean(valid.map((s) => s.area)); // avarage state area
// generic relations
- for (let f=1; f < states.length; f++) {
+ for (let f = 1; f < states.length; f++) {
if (states[f].removed) continue;
- if (states[f].diplomacy.includes("Vassal")) {
+ if (states[f].diplomacy.includes('Vassal')) {
// Vassals copy relations from their Suzerains
- const suzerain = states[f].diplomacy.indexOf("Vassal");
+ const suzerain = states[f].diplomacy.indexOf('Vassal');
- for (let i=1; i < states.length; i++) {
+ 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 (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;
+ 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++) {
+ 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");
+ 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 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);
+ 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(.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;
+ 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++) {
+ 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
+ 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, dp = states[defender].area * states[defender].expansionism;
- if (ap < dp * gauss(1.6, .8, 0, 10, 2)) continue; // defender is too strong
- const an = states[attacker].name, dn = states[defender].name; // names
- const attackers = [attacker], defenders = [defender]; // attackers and defenders array
+ 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;
+ if (ap < dp * gauss(1.6, 0.8, 0, 10, 2)) continue; // defender is too strong
+ 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;
// start a war
- const war = [`${an}-${trimVowels(dn)}ian War`,`${an} declared a war on its rival ${dn}`];
+ const war = [`${an}-${trimVowels(dn)}ian War`, `${an} declared a war on its rival ${dn}`];
const end = options.year;
const start = end - gauss(2, 2, 0, 5);
states[attacker].campaigns.push({name: `${trimVowels(dn)}ian War`, start, end});
states[defender].campaigns.push({name: `${trimVowels(an)}ian War`, start, end});
// 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`);
- }});
+ 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`);
- }});
+ 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
+ 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, .8, 0, 10, 2))) {
- const reason = states[d].diplomacy.includes("Enemy") ? `Being already at war,` : `Frightened by ${an},`;
+ 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";
+ dd[d] = states[d].diplomacy[defender] = 'Suspicion';
return;
}
defenders.push(d);
@@ -780,75 +884,87 @@
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`);
- });
+ 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;
+ if (r !== 'Ally' || states[d].diplomacy.includes('Vassal') || defenders.includes(d)) return;
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;}
- 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;};
+ 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`);
- });
+ 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"));
+ 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");
+ TIME && console.timeEnd('generateDiplomacy');
//console.table(states.map(s => s.diplomacy));
- }
+ };
// select a forms for listed or all valid states
- const defineStateForms = function(list) {
- TIME && console.time("defineStateForms");
- const states = pack.states.filter(s => s.i && !s.removed);
+ const defineStateForms = function (list) {
+ TIME && console.time('defineStateForms');
+ const states = pack.states.filter((s) => s.i && !s.removed);
if (states.length < 1) return;
- const generic = {Monarchy:25, Republic:2, Union:1};
- const naval = {Monarchy:25, Republic:8, Union:3};
+ 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 ** .4) - 2, 0)];
- const expTiers = pack.states.map(s => {
- let tier = Math.min(Math.floor(s.area / median * 2.6), 4);
+ 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, Oligarchy: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 theocracy = {Theocracy: 20, Brotherhood:1, Thearchy:2, See:1};
- const anarchy = {"Free Territory":2, Council:3, Commune:1, Community:1};
+ 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 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 anarchy = {'Free Territory': 2, Council: 3, Commune: 1, Community: 1};
for (const s of states) {
if (list && !list.includes(s.i)) continue;
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 isAnarchy = P(.01 - expTiers[s.i]/500);
+ const isTheocracy = (religion && pack.religions[religion].expansion === 'state') || (P(0.1) && ['Organized', 'Cult'].includes(pack.religions[religion].type));
+ const isAnarchy = P(0.01 - expTiers[s.i] / 500);
- if (isTheocracy) s.form = "Theocracy";
- else if (isAnarchy) s.form = "Anarchy";
- else s.form = s.type === "Naval" ? rw(naval) : rw(generic);
+ 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);
s.fullName = getFullName(s);
}
@@ -856,130 +972,158 @@
function selectForm(s) {
const base = pack.cultures[s.culture].base;
- if (s.form === "Monarchy") {
+ if (s.form === 'Monarchy') {
const form = monarchy[expTiers[s.i]];
// 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 dutchies on borderland
- if (P(.3) && s.diplomacy.includes("Vassal")) return "Protectorate"; // some vassals
+ 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(0.3) && s.diplomacy.includes('Vassal')) return 'Protectorate'; // some vassals
}
- if (base === 16 && (form === "Empire" || form === "Kingdom")) return "Sultanate"; // Turkic
- if (base === 5 && (form === "Empire" || form === "Kingdom")) return "Tsardom"; // Ruthenian
- if ([16, 31].includes(base) && (form === "Empire" || form === "Kingdom")) return "Khaganate"; // Turkic, Mongolian
- 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
+ if (base === 16 && (form === 'Empire' || form === 'Kingdom')) return 'Sultanate'; // Turkic
+ if (base === 5 && (form === 'Empire' || form === 'Kingdom')) return 'Tsardom'; // Ruthenian
+ if ([16, 31].includes(base) && (form === 'Empire' || form === 'Kingdom')) return 'Khaganate'; // Turkic, Mongolian
+ 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") {
+ if (s.form === 'Republic') {
// Default name is from weighted array, special case for small states with only 1 burg
if (expTiers[s.i] < 2 && s.burgs === 1) {
if (trimVowels(s.name) === trimVowels(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);
}
- if (s.form === "Union") return rw(union);
- if (s.form === "Anarchy") return rw(anarchy);
+ if (s.form === 'Union') return rw(union);
+ if (s.form === 'Anarchy') return rw(anarchy);
- if (s.form === "Theocracy") {
- if (P(.5) && [0, 1, 2, 3, 4, 6, 8, 9, 13, 15, 20].includes(base)) return "Diocese"; // Euporean
- if (P(.9) && [7, 5].includes(base)) return "Eparchy"; // Greek, Ruthenian
- if (P(.9) && [21, 16].includes(base)) return "Imamah"; // Nigerian, Turkish
- if (P(.8) && [18, 17, 28].includes(base)) return "Caliphate"; // Arabic, Berber, Swahili
+ if (s.form === 'Theocracy') {
+ if (P(0.5) && [0, 1, 2, 3, 4, 6, 8, 9, 13, 15, 20].includes(base)) return 'Diocese'; // Euporean
+ if (P(0.9) && [7, 5].includes(base)) return 'Eparchy'; // Greek, Ruthenian
+ if (P(0.9) && [21, 16].includes(base)) return 'Imamah'; // Nigerian, Turkish
+ if (P(0.8) && [18, 17, 28].includes(base)) return 'Caliphate'; // Arabic, Berber, Swahili
return rw(theocracy);
}
}
- TIME && console.timeEnd("defineStateForms");
- }
+ 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 adjForms = [
+ 'Empire',
+ 'Sultanate',
+ 'Khaganate',
+ 'Shogunate',
+ 'Caliphate',
+ 'Despotate',
+ 'Theocracy',
+ 'Oligarchy',
+ 'Union',
+ 'Confederation',
+ 'Trade Company',
+ 'League',
+ 'Tetrarchy',
+ 'Triumvirate',
+ 'Diarchy',
+ 'Horde',
+ 'Marches'
+ ];
- const getFullName = function(s) {
+ const getFullName = function (s) {
if (!s.formName) return s.name;
- if (!s.name && s.formName) return "The " + s.formName;
- const adjName = adjForms.includes(s.formName) && !(/-| /).test(s.name);
+ if (!s.name && s.formName) return 'The ' + s.formName;
+ const adjName = adjForms.includes(s.formName) && !/-| /.test(s.name);
return adjName ? `${getAdjective(s.name)} ${s.formName}` : `${s.formName} of ${s.name}`;
- }
+ };
- const generateProvinces = function(regenerate) {
- TIME && console.time("generateProvinces");
+ const generateProvinces = function (regenerate) {
+ TIME && console.time('generateProvinces');
const localSeed = regenerate ? Math.floor(Math.random() * 1e9).toString() : seed;
Math.random = aleaPRNG(localSeed);
- const cells = pack.cells, states = pack.states, burgs = pack.burgs;
- const provinces = pack.provinces = [0];
+ const cells = pack.cells,
+ states = pack.states,
+ burgs = pack.burgs;
+ const provinces = (pack.provinces = [0]);
cells.province = new Uint16Array(cells.i.length); // cell state
const percentage = +provincesInput.value;
- if (states.length < 2 || !percentage) {states.forEach(s => s.provinces = []); return;} // no provinces
- const max = percentage == 100 ? 1000 : gauss(20, 5, 5, 100) * percentage ** .5; // max growth
+ if (states.length < 2 || !percentage) {
+ 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 = {
- Monarchy: {County:11, Earldom:3, Shire:1, Landgrave:1, Margrave:1, Barony:1},
- Republic: {Province:6, Department:2, Governorate:2, District:1, Canton:1, Prefecture:1},
- Theocracy: {Parish:3, Deanery:1},
- Union: {Province:1, State:1, Canton:1, Republic:1, County:1, Council: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}
- }
+ Monarchy: {County: 11, Earldom: 3, Shire: 1, Landgrave: 1, Margrave: 1, Barony: 1},
+ Republic: {Province: 6, Department: 2, Governorate: 2, District: 1, Canton: 1, Prefecture: 1},
+ Theocracy: {Parish: 3, Deanery: 1},
+ Union: {Province: 1, State: 1, Canton: 1, Republic: 1, County: 1, Council: 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}
+ };
// generate provinces for a selected burgs
Math.random = aleaPRNG(localSeed);
- states.forEach(s => {
+ states.forEach((s) => {
s.provinces = [];
if (!s.i || s.removed) return;
- const stateBurgs = burgs.filter(b => b.state === s.i && !b.removed)
- .sort((a, b) => b.population * gauss(1, .2, .5, 1.5, 3) - a.population)
+ const stateBurgs = burgs
+ .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);
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]);
- for (let i=0; i < provincesNumber; i++) {
+ for (let i = 0; i < provincesNumber; i++) {
const province = provinces.length;
s.provinces.push(province);
const center = stateBurgs[i].cell;
const burg = stateBurgs[i].i;
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 formName = rw(form);
form[formName] += 10;
- const fullName = name + " " + formName;
+ const fullName = name + ' ' + formName;
const color = getMixedColor(s.color);
- const kinship = nameByBurg ? .8 : .4;
+ const kinship = nameByBurg ? 0.8 : 0.4;
const type = getType(center, burg.port);
const coa = COA.generate(stateBurgs[i].coa, kinship, null, type);
coa.shield = COA.getShield(c, s.i);
- provinces.push({i:province, state:s.i, center, burg, name, formName, fullName, color, coa});
+ provinces.push({i: province, state: s.i, center, burg, name, formName, fullName, color, coa});
}
});
// expand generated provinces
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const cost = [];
- provinces.forEach(function(p) {
+ provinces.forEach(function (p) {
if (!p.i || p.removed) return;
cells.province[p.center] = p.i;
- queue.queue({e:p.center, p:0, province:p.i, state:p.state});
+ queue.queue({e: p.center, p: 0, province: p.i, state: p.state});
cost[p.center] = 1;
});
while (queue.length) {
- const next = queue.dequeue(), n = next.e, p = next.p, province = next.province, state = next.state;
- cells.c[n].forEach(function(e) {
+ const next = queue.dequeue(),
+ n = next.e,
+ p = next.p,
+ province = next.province,
+ state = next.state;
+ cells.c[n].forEach(function (e) {
const land = cells.h[e] >= 20;
if (!land && !cells.t[e]) return; // cannot pass deep ocean
if (land && cells.state[e] !== state) return;
@@ -990,7 +1134,7 @@
if (!cost[e] || totalCost < cost[e]) {
if (land) cells.province[e] = province; // assign province to a cell
cost[e] = totalCost;
- queue.queue({e, p:totalCost, province, state});
+ queue.queue({e, p: totalCost, province, state});
}
});
}
@@ -998,80 +1142,85 @@
// justify provinces shapes a bit
for (const i of cells.i) {
if (cells.burg[i]) continue; // do not overwrite burgs
- const neibs = cells.c[i].filter(c => cells.state[c] === cells.state[i]).map(c => cells.province[c]);
- const adversaries = neibs.filter(c => c !== cells.province[i]);
+ const neibs = cells.c[i].filter((c) => cells.state[c] === cells.state[i]).map((c) => cells.province[c]);
+ const adversaries = neibs.filter((c) => c !== cells.province[i]);
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;
- 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);
if (buddies >= max) continue;
cells.province[i] = adversaries[competitors.indexOf(max)];
}
// add "wild" provinces if some cells don't have a province assigned
- const noProvince = Array.from(cells.i).filter(i => cells.state[i] && !cells.province[i]); // cells without province assigned
- states.forEach(s => {
+ const noProvince = Array.from(cells.i).filter((i) => cells.state[i] && !cells.province[i]); // cells without province assigned
+ states.forEach((s) => {
if (!s.provinces.length) return;
- let stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !cells.province[i]);
+ let stateNoProvince = noProvince.filter((i) => cells.state[i] === s.i && !cells.province[i]);
while (stateNoProvince.length) {
// add new province
const province = provinces.length;
- const burgCell = stateNoProvince.find(i => cells.burg[i]);
+ const burgCell = stateNoProvince.find((i) => cells.burg[i]);
const center = burgCell ? burgCell : stateNoProvince[0];
const burg = burgCell ? cells.burg[burgCell] : 0;
cells.province[center] = province;
// expand province
- const cost = []; cost[center] = 1;
- queue.queue({e:center, p:0});
+ const cost = [];
+ cost[center] = 1;
+ queue.queue({e: center, p: 0});
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;
const land = cells.h[e] >= 20;
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;
if (totalCost > max) return;
if (!cost[e] || totalCost < cost[e]) {
if (land && cells.state[e] === s.i) cells.province[e] = province; // assign province to a cell
cost[e] = totalCost;
- queue.queue({e, p:totalCost});
+ queue.queue({e, p: totalCost});
}
});
}
// generate "wild" province name
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 f = pack.features[cells.f[center]];
- const provCells = stateNoProvince.filter(i => cells.province[i] === province);
- 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 colony = !singleIsle && !isleGroup && P(.5) && !isPassable(s.center, center);
- const formName = singleIsle ? "Island" : isleGroup ? "Islands" : colony ? "Colony" : rw(forms["Wild"]);
- const fullName = name + " " + formName;
+ const provCells = stateNoProvince.filter((i) => cells.province[i] === province);
+ 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 colony = !singleIsle && !isleGroup && P(0.5) && !isPassable(s.center, center);
+ const formName = singleIsle ? 'Island' : isleGroup ? 'Islands' : colony ? 'Colony' : rw(forms['Wild']);
+ const fullName = name + ' ' + formName;
const color = getMixedColor(s.color);
- const dominion = colony ? P(.95) : singleIsle || isleGroup ? P(.7) : P(.3);
- const kinship = dominion ? 0 : .4;
+ const dominion = colony ? P(0.95) : singleIsle || isleGroup ? P(0.7) : P(0.3);
+ const kinship = dominion ? 0 : 0.4;
const type = getType(center, burgs[burg]?.port);
const coa = COA.generate(s.coa, kinship, dominion, type);
coa.shield = COA.getShield(c, s.i);
- provinces.push({i:province, state:s.i, center, burg, name, formName, fullName, color, coa});
+ provinces.push({i: province, state: s.i, center, burg, name, formName, fullName, color, coa});
s.provinces.push(province);
// check if there is a land way within the same state between two cells
function isPassable(from, to) {
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) {
const current = queue.pop();
if (current === to) return true; // way is found
- cells.c[current].forEach(c => {
+ cells.c[current].forEach((c) => {
if (used[c] || cells.h[c] < 20 || cells.state[c] !== state) return;
queue.push(c);
used[c] = 1;
@@ -1081,15 +1230,29 @@
}
// re-check
- stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !cells.province[i]);
+ stateNoProvince = noProvince.filter((i) => cells.state[i] === s.i && !cells.province[i]);
}
});
- TIME && console.timeEnd("generateProvinces");
- }
+ TIME && console.timeEnd('generateProvinces');
+ };
- return {generate, expandStates, normalizeStates, assignColors,
- drawBurgs, specifyBurgs, defineBurgFeatures, getType, drawStateLabels, collectStatistics,
- generateCampaigns, generateDiplomacy, defineStateForms, getFullName, generateProvinces, updateCultures};
-
-})));
+ return {
+ generate,
+ expandStates,
+ normalizeStates,
+ assignColors,
+ drawBurgs,
+ specifyBurgs,
+ defineBurgFeatures,
+ getType,
+ drawStateLabels,
+ collectStatistics,
+ generateCampaigns,
+ generateDiplomacy,
+ defineStateForms,
+ getFullName,
+ generateProvinces,
+ updateCultures
+ };
+});
diff --git a/modules/resources-generator.js b/modules/resources-generator.js
index 9f67e2c2..781e4d00 100644
--- a/modules/resources-generator.js
+++ b/modules/resources-generator.js
@@ -3,7 +3,7 @@
})(this, function () {
'use strict';
- // TO-DO
+ // TODO
// apply logic on heightmap edit
// apply logic on burgs regeneration
// apply logic on population recalculation
@@ -13,48 +13,47 @@
let cells, cellId;
const getDefault = function () {
- // model: cells eligibility function; chance: chance to get rosource in model-eligible cell
return [
- {i: 1, name: 'Wood', category: 'Construction', icon: 'resource-wood', color: '#966F33', value: 5, chance: 10, model: 'Any_forest', bonus: {fleet: 2, defence: 1}},
- {i: 2, name: 'Stone', category: 'Construction', icon: 'resource-stone', color: '#979EA2', value: 4, chance: 7, model: 'Hills', bonus: {prestige: 1, defence: 2}},
- {i: 3, name: 'Marble', category: 'Construction', icon: 'resource-marble', color: '#d6d0bf', value: 15, chance: 1, model: 'Mountains', bonus: {prestige: 2}},
- {i: 4, name: 'Iron', category: 'Ore', icon: 'resource-iron', color: '#5D686E', value: 8, chance: 8, model: 'Mountains_and_wetlands', bonus: {artillery: 1, infantry: 1, defence: 1}},
- {i: 5, name: 'Copper', category: 'Ore', icon: 'resource-copper', color: '#b87333', value: 10, chance: 3, model: 'Mountains', bonus: {artillery: 2, defence: 1, prestige: 1}},
- {i: 6, name: 'Lead', category: 'Ore', icon: 'resource-lead', color: '#454343', value: 8, chance: 3, model: 'Mountains', bonus: {artillery: 1, defence: 1}},
- {i: 7, name: 'Silver', category: 'Ore', icon: 'resource-silver', color: '#C0C0C0', value: 15, chance: 3, model: 'Mountains', bonus: {prestige: 2}},
- {i: 8, name: 'Gold', category: 'Ore', icon: 'resource-gold', color: '#d4af37', value: 30, chance: 1, model: 'Headwaters', bonus: {prestige: 3}},
- {i: 9, name: 'Grain', category: 'Food', icon: 'resource-grain', color: '#F5DEB3', value: 1, chance: 15, model: 'Biome_habitability', bonus: {population: 4}},
- {i: 10, name: 'Cattle', category: 'Food', icon: 'resource-cattle', color: '#56b000', value: 2, chance: 10, model: 'Pastures_and_temperate_forest', bonus: {population: 2}},
- {i: 11, name: 'Fish', category: 'Food', icon: 'resource-fish', color: '#7fcdff', value: 1, chance: 5, model: 'Marine_and_rivers', bonus: {population: 2}},
+ {i: 1, name: 'Wood', category: 'Construction', icon: 'resource-wood', color: '#966F33', value: 2, chance: 4, model: 'Any_forest', bonus: {fleet: 2, defence: 1}},
+ {i: 2, name: 'Stone', category: 'Construction', icon: 'resource-stone', color: '#979EA2', value: 2, chance: 4, model: 'Hills', bonus: {prestige: 1, defence: 2}},
+ {i: 3, name: 'Marble', category: 'Construction', icon: 'resource-marble', color: '#d6d0bf', value: 7, chance: 1, model: 'Mountains', bonus: {prestige: 2}},
+ {i: 4, name: 'Iron', category: 'Ore', icon: 'resource-iron', color: '#5D686E', value: 4, chance: 4, model: 'Mountains_and_wetlands', bonus: {artillery: 1, infantry: 1, defence: 1}},
+ {i: 5, name: 'Copper', category: 'Ore', icon: 'resource-copper', color: '#b87333', value: 5, chance: 3, model: 'Mountains', bonus: {artillery: 2, defence: 1, prestige: 1}},
+ {i: 6, name: 'Lead', category: 'Ore', icon: 'resource-lead', color: '#454343', value: 4, chance: 3, model: 'Mountains', bonus: {artillery: 1, defence: 1}},
+ {i: 7, name: 'Silver', category: 'Ore', icon: 'resource-silver', color: '#C0C0C0', value: 8, chance: 3, model: 'Mountains', bonus: {prestige: 2}},
+ {i: 8, name: 'Gold', category: 'Ore', icon: 'resource-gold', color: '#d4af37', value: 15, chance: 1, model: 'Headwaters', bonus: {prestige: 3}},
+ {i: 9, name: 'Grain', category: 'Food', icon: 'resource-grain', color: '#F5DEB3', value: 1, chance: 4, model: 'More_habitable', bonus: {population: 4}},
+ {i: 10, name: 'Cattle', category: 'Food', icon: 'resource-cattle', color: '#56b000', value: 2, chance: 4, model: 'Pastures_and_temperate_forest', bonus: {population: 2}},
+ {i: 11, name: 'Fish', category: 'Food', icon: 'resource-fish', color: '#7fcdff', value: 1, chance: 2, model: 'Marine_and_rivers', bonus: {population: 2}},
{i: 12, name: 'Game', category: 'Food', icon: 'resource-game', color: '#c38a8a', value: 2, chance: 3, model: 'Any_forest', bonus: {archers: 2, population: 1}},
- {i: 13, name: 'Wine', category: 'Food', icon: 'resource-wine', color: '#963e48', value: 3, chance: 4, model: 'Tropical_forests', bonus: {population: 1, prestige: 1}},
- {i: 14, name: 'Olives', category: 'Food', icon: 'resource-olives', color: '#BDBD7D', value: 3, chance: 4, model: 'Tropical_forests', bonus: {population: 1}},
- {i: 15, name: 'Honey', category: 'Food', icon: 'resource-honey', color: '#DCBC66', value: 4, chance: 3, model: 'Temperate_and_boreal_forests', bonus: {population: 1}},
- {i: 16, name: 'Salt', category: 'Food', icon: 'resource-salt', color: '#E5E4E5', value: 5, chance: 4, model: 'Arid_land_and_salt_lakes', bonus: {population: 1, defence: 1}},
- {i: 17, name: 'Dates', category: 'Food', icon: 'resource-dates', color: '#dbb2a3', value: 3, chance: 3, model: 'Hot_desert', bonus: {population: 1}},
- {i: 18, name: 'Horses', category: 'Supply', icon: 'resource-horses', color: '#ba7447', value: 10, chance: 6, model: 'Grassland_and_cold_desert', bonus: {cavalry: 2}},
- {i: 19, name: 'Elephants', category: 'Supply', icon: 'resource-elephants', color: '#C5CACD', value: 15, chance: 2, model: 'Hot_biomes', bonus: {cavalry: 1}},
- {i: 20, name: 'Camels', category: 'Supply', icon: 'resource-camels', color: '#C19A6B', value: 13, chance: 4, model: 'Deserts', bonus: {cavalry: 1}},
- {i: 21, name: 'Hemp', category: 'Material', icon: 'resource-hemp', color: '#069a06', value: 2, chance: 4, model: 'Deciduous_forests', bonus: {fleet: 2}},
- {i: 22, name: 'Pearls', category: 'Luxury', icon: 'resource-pearls', color: '#EAE0C8', value: 35, chance: 3, model: 'Tropical_waters', bonus: {prestige: 1}},
- {i: 23, name: 'Gemstones', category: 'Luxury', icon: 'resource-gemstones', color: '#e463e4', value: 35, chance: 2, model: 'Mountains', bonus: {prestige: 1}},
- {i: 24, name: 'Dyes', category: 'Luxury', icon: 'resource-dyes', color: '#fecdea', value: 15, chance: 0.5, model: 'Habitable_biome_or_marine', bonus: {prestige: 1}},
- {i: 25, name: 'Incense', category: 'Luxury', icon: 'resource-incense', color: '#ebe5a7', value: 25, chance: 2, model: 'Hot_desert_and_tropical_forest', bonus: {prestige: 2}},
- {i: 26, name: 'Silk', category: 'Luxury', icon: 'resource-silk', color: '#e0f0f8', value: 30, chance: 1, model: 'Tropical_rainforest', bonus: {prestige: 2}},
- {i: 27, name: 'Spices', category: 'Luxury', icon: 'resource-spices', color: '#e99c75', value: 30, chance: 2, model: 'Tropical_rainforest', bonus: {prestige: 2}},
- {i: 28, name: 'Amber', category: 'Luxury', icon: 'resource-amber', color: '#e68200', value: 15, chance: 2, model: 'Foresty_seashore', bonus: {prestige: 1}},
- {i: 29, name: 'Furs', category: 'Material', icon: 'resource-furs', color: '#8a5e51', value: 13, chance: 2, model: 'Boreal_forests', bonus: {prestige: 1}},
- {i: 30, name: 'Sheep', category: 'Material', icon: 'resource-sheeps', color: '#53b574', value: 2, chance: 5, model: 'Pastures_and_temperate_forest', bonus: {infantry: 1}},
- {i: 31, name: 'Slaves', category: 'Supply', icon: 'resource-slaves', color: '#757575', value: 10, chance: 3, model: 'Less_habitable_seashore', bonus: {population: 2}},
- {i: 32, name: 'Tar', category: 'Material', icon: 'resource-tar', color: '#727272', value: 3, chance: 3, model: 'Any_forest', bonus: {fleet: 1}},
- {i: 33, name: 'Saltpeter', category: 'Material', icon: 'resource-saltpeter', color: '#e6e3e3', value: 8, chance: 2, model: 'Biome_habitability', bonus: {artillery: 3}},
- {i: 34, name: 'Coal', category: 'Material', icon: 'resource-coal', color: '#36454f', value: 2, chance: 7, model: 'Hills', bonus: {artillery: 2}},
- {i: 35, name: 'Oil', category: 'Material', icon: 'resource-oil', color: '#565656', value: 5, chance: 2, model: 'Less_habitable_biomes', bonus: {artillery: 1}},
- {i: 36, name: 'Tropical timber', category: 'Luxury', icon: 'resource-tropicalTimber', color: '#a45a52', value: 20, chance: 2, model: 'Tropical_rainforest', bonus: {prestige: 1}},
- {i: 37, name: 'Whales', category: 'Food', icon: 'resource-whales', color: '#cccccc', value: 2, chance: 2, model: 'Arctic_waters', bonus: {population: 1}},
+ {i: 13, name: 'Wine', category: 'Food', icon: 'resource-wine', color: '#963e48', value: 2, chance: 3, model: 'Tropical_forests', bonus: {population: 1, prestige: 1}},
+ {i: 14, name: 'Olives', category: 'Food', icon: 'resource-olives', color: '#BDBD7D', value: 2, chance: 3, model: 'Tropical_forests', bonus: {population: 1}},
+ {i: 15, name: 'Honey', category: 'Food', icon: 'resource-honey', color: '#DCBC66', value: 2, chance: 3, model: 'Temperate_and_boreal_forests', bonus: {population: 1}},
+ {i: 16, name: 'Salt', category: 'Food', icon: 'resource-salt', color: '#E5E4E5', value: 3, chance: 3, model: 'Arid_land_and_salt_lakes', bonus: {population: 1, defence: 1}},
+ {i: 17, name: 'Dates', category: 'Food', icon: 'resource-dates', color: '#dbb2a3', value: 2, chance: 2, model: 'Hot_desert', bonus: {population: 1}},
+ {i: 18, name: 'Horses', category: 'Supply', icon: 'resource-horses', color: '#ba7447', value: 5, chance: 4, model: 'Grassland_and_cold_desert', bonus: {cavalry: 2}},
+ {i: 19, name: 'Elephants', category: 'Supply', icon: 'resource-elephants', color: '#C5CACD', value: 7, chance: 2, model: 'Hot_biomes', bonus: {cavalry: 1}},
+ {i: 20, name: 'Camels', category: 'Supply', icon: 'resource-camels', color: '#C19A6B', value: 7, chance: 3, model: 'Deserts', bonus: {cavalry: 1}},
+ {i: 21, name: 'Hemp', category: 'Material', icon: 'resource-hemp', color: '#069a06', value: 2, chance: 3, model: 'Deciduous_forests', bonus: {fleet: 2}},
+ {i: 22, name: 'Pearls', category: 'Luxury', icon: 'resource-pearls', color: '#EAE0C8', value: 16, chance: 2, model: 'Tropical_waters', bonus: {prestige: 1}},
+ {i: 23, name: 'Gemstones', category: 'Luxury', icon: 'resource-gemstones', color: '#e463e4', value: 17, chance: 2, model: 'Mountains', bonus: {prestige: 1}},
+ {i: 24, name: 'Dyes', category: 'Luxury', icon: 'resource-dyes', color: '#fecdea', value: 6, chance: 0.5, model: 'Habitable_biome_or_marine', bonus: {prestige: 1}},
+ {i: 25, name: 'Incense', category: 'Luxury', icon: 'resource-incense', color: '#ebe5a7', value: 12, chance: 2, model: 'Hot_desert_and_tropical_forest', bonus: {prestige: 2}},
+ {i: 26, name: 'Silk', category: 'Luxury', icon: 'resource-silk', color: '#e0f0f8', value: 15, chance: 1, model: 'Tropical_rainforest', bonus: {prestige: 2}},
+ {i: 27, name: 'Spices', category: 'Luxury', icon: 'resource-spices', color: '#e99c75', value: 15, chance: 2, model: 'Tropical_rainforest', bonus: {prestige: 2}},
+ {i: 28, name: 'Amber', category: 'Luxury', icon: 'resource-amber', color: '#e68200', value: 7, chance: 2, model: 'Foresty_seashore', bonus: {prestige: 1}},
+ {i: 29, name: 'Furs', category: 'Material', icon: 'resource-furs', color: '#8a5e51', value: 6, chance: 2, model: 'Boreal_forests', bonus: {prestige: 1}},
+ {i: 30, name: 'Sheep', category: 'Material', icon: 'resource-sheeps', color: '#53b574', value: 2, chance: 3, model: 'Pastures_and_temperate_forest', bonus: {infantry: 1}},
+ {i: 31, name: 'Slaves', category: 'Supply', icon: 'resource-slaves', color: '#757575', value: 5, chance: 2, model: 'Less_habitable_seashore', bonus: {population: 2}},
+ {i: 32, name: 'Tar', category: 'Material', icon: 'resource-tar', color: '#727272', value: 2, chance: 3, model: 'Any_forest', bonus: {fleet: 1}},
+ {i: 33, name: 'Saltpeter', category: 'Material', icon: 'resource-saltpeter', color: '#e6e3e3', value: 3, chance: 2, model: 'Less_habitable_biomes', bonus: {artillery: 3}},
+ {i: 34, name: 'Coal', category: 'Material', icon: 'resource-coal', color: '#36454f', value: 2, chance: 3, model: 'Hills', bonus: {artillery: 2}},
+ {i: 35, name: 'Oil', category: 'Material', icon: 'resource-oil', color: '#565656', value: 3, chance: 2, model: 'Less_habitable_biomes', bonus: {artillery: 1}},
+ {i: 36, name: 'Tropical timber', category: 'Luxury', icon: 'resource-tropicalTimber', color: '#a45a52', value: 10, chance: 2, model: 'Tropical_rainforest', bonus: {prestige: 1}},
+ {i: 37, name: 'Whales', category: 'Food', icon: 'resource-whales', color: '#cccccc', value: 2, chance: 3, model: 'Arctic_waters', bonus: {population: 1}},
{i: 38, name: 'Sugar', category: 'Food', icon: 'resource-sugar', color: '#7abf87', value: 3, chance: 3, model: 'Tropical_rainforest', bonus: {population: 1}},
- {i: 39, name: 'Tea', category: 'Luxury', icon: 'resource-tea', color: '#d0f0c0', value: 10, chance: 3, model: 'Hilly_tropical_rainforest', bonus: {prestige: 1}},
- {i: 40, name: 'Tobacco', category: 'Luxury', icon: 'resource-tobacco', color: '#6D5843', value: 10, chance: 2, model: 'Tropical_rainforest', bonus: {prestige: 1}}
+ {i: 39, name: 'Tea', category: 'Luxury', icon: 'resource-tea', color: '#d0f0c0', value: 5, chance: 3, model: 'Hilly_tropical_rainforest', bonus: {prestige: 1}},
+ {i: 40, name: 'Tobacco', category: 'Luxury', icon: 'resource-tobacco', color: '#6D5843', value: 5, chance: 2, model: 'Tropical_rainforest', bonus: {prestige: 1}}
];
};
@@ -66,8 +65,8 @@
Mountains: 'minHeight(60) || (minHeight(40) && nth(10))',
Mountains_and_wetlands: 'minHeight(60) || (biome(12) && nth(8))',
Headwaters: 'river() && minHeight(40)',
- Biome_habitability: 'habitability()',
- Marine_and_rivers: 'type("ocean", "freshwater", "salt") || (river() && shore(1, 2))',
+ More_habitable: 'minHabitability(20) && habitability()',
+ Marine_and_rivers: 'shore(-1) && (type("ocean", "freshwater", "salt") || (river() && shore(1, 2)))',
Pastures_and_temperate_forest: '(biome(3, 4) && !elevation()) || (biome(6) && random(70)) || (biome(5) && nth(5))',
Tropical_forests: 'biome(5, 7)',
Arid_land_and_salt_lakes: 'type("salt", "dry") || (biome(1, 2) && random(70)) || (biome(12) && nth(10))',
@@ -80,19 +79,19 @@
Tropical_waters: 'shore(-1) && minTemp(18)',
Hilly_tropical_rainforest: 'minHeight(40) && biome(7)',
Subtropical_waters: 'shore(-1) && minTemp(14)',
- Habitable_biome_or_marine: 'shore(-1) || habitable()',
+ Habitable_biome_or_marine: 'shore(-1) || minHabitability(1)',
Foresty_seashore: 'shore(1) && biome(6, 7, 8, 9)',
Boreal_forests: 'biome(9) || (biome(10) && nth(2)) || (biome(6, 8) && nth(5)) || (biome(12) && nth(10))',
- Less_habitable_seashore: 'shore(1) && habitable() && !habitability()',
- Less_habitable_biomes: 'habitable() && !habitability()',
- Arctic_waters: 'biome(0) && maxTemp(7)'
+ Less_habitable_seashore: 'shore(1) && minHabitability(1) && !habitability()',
+ Less_habitable_biomes: 'minHabitability(1) && !habitability()',
+ Arctic_waters: 'shore(-1) && biome(0) && maxTemp(7)'
};
const methods = {
random: (number) => number >= 100 || (number > 0 && number / 100 > Math.random()),
nth: (number) => !(cellId % number),
- habitable: () => biomesData.habitability[pack.cells.biome[cellId]],
- habitability: () => biomesData.habitability[cells.biome[cellId]] / 100 > Math.random(),
+ minHabitability: (min) => biomesData.habitability[pack.cells.biome[cellId]] >= min,
+ habitability: () => biomesData.habitability[cells.biome[cellId]] > Math.random() * 100,
elevation: () => pack.cells.h[cellId] / 100 > Math.random(),
biome: (...biomes) => biomes.includes(pack.cells.biome[cellId]),
minHeight: (heigh) => pack.cells.h[cellId] >= heigh,
@@ -106,7 +105,7 @@
const allMethods = '{' + Object.keys(methods).join(', ') + '}';
const generate = function () {
- console.time('generateResources');
+ TIME && console.time('generateResources');
cells = pack.cells;
cells.resource = new Uint8Array(cells.i.length); // resources array [0, 255]
const resourceMaxCells = Math.ceil((200 * cells.i.length) / 5000);
@@ -128,7 +127,7 @@
for (const resource of pack.resources) {
if (resource.cells >= resourceMaxCells) continue;
- if (resource.cells >= resource.chance && rnd > resource.chance) continue;
+ if (resource.cells ? rnd > resource.chance : Math.random() * 100 > resource.chance) continue;
if (!resource.fn({...methods})) continue;
cells.resource[i] = resource.i;
@@ -138,7 +137,7 @@
}
pack.resources.sort((a, b) => (a.i > b.i ? 1 : -1)).forEach((r) => delete r.fn);
- console.timeEnd('generateResources');
+ TIME && console.timeEnd('generateResources');
};
const getStroke = (color) => d3.color(color).darker(2).hex();
diff --git a/modules/ui/layers.js b/modules/ui/layers.js
index 4b89a8d2..79284510 100644
--- a/modules/ui/layers.js
+++ b/modules/ui/layers.js
@@ -1655,6 +1655,7 @@ function toggleResources(event) {
function drawResources() {
console.time('drawResources');
const someArePinned = pack.resources.some((resource) => resource.pinned);
+ const drawCircle = +goods.attr('data-circle');
let resourcesHTML = '';
for (const i of pack.cells.i) {
@@ -1664,11 +1665,17 @@ function drawResources() {
const [x, y] = pack.cells.p[i];
const stroke = Resources.getStroke(resource.color);
+ if (!drawCircle) {
+ resourcesHTML += ``;
+ continue;
+ }
+
resourcesHTML += `
`;
}
+
goods.html(resourcesHTML);
console.timeEnd('drawResources');
}
diff --git a/modules/ui/style.js b/modules/ui/style.js
index 03ce684f..096fd6a9 100644
--- a/modules/ui/style.js
+++ b/modules/ui/style.js
@@ -251,6 +251,13 @@ function selectStyleElement() {
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr('stroke-width') || 1;
}
+ if (sel === 'goods') {
+ styleStrokeWidth.style.display = 'block';
+ styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr('stroke-width') || '';
+ styleResources.style.display = 'block';
+ styleResourcesCircle.checked = +el.attr('data-circle');
+ }
+
// update group options
styleGroupSelect.options.length = 0; // remove all options
if (['routes', 'labels', 'coastline', 'lakes', 'anchors', 'burgIcons', 'borders'].includes(sel)) {
@@ -677,6 +684,12 @@ styleEmblemsStateSizeInput.addEventListener('input', drawEmblems);
styleEmblemsProvinceSizeInput.addEventListener('input', drawEmblems);
styleEmblemsBurgSizeInput.addEventListener('input', drawEmblems);
+styleResourcesCircle.addEventListener('change', function () {
+ goods.attr('data-circle', +this.checked);
+ goods.selectAll('*').remove();
+ drawResources();
+});
+
// request a URL to image to be used as a texture
function textureProvideURL() {
alertMessage.innerHTML = `Provide an image URL to be used as a texture:
@@ -913,7 +926,7 @@ function applyDefaultStyle() {
fogging.attr('opacity', 0.98).attr('fill', '#30426f');
emblems.attr('opacity', 0.9).attr('stroke-width', 1).attr('filter', null);
- goods.attr('opacity', 1).attr('fill', '#000').attr('stroke', '#000').attr('stroke-width', 0.32).attr('filter', 'url(#dropShadow01)');
+ goods.attr('opacity', 1).attr('data-circle', 1).attr('fill', '#000').attr('stroke', '#000').attr('stroke-width', 0.32).attr('filter', 'url(#dropShadow01)');
}
// apply style settings in JSON
|