diff --git a/index.css b/index.css
index eb1e82fc..af2ec591 100644
--- a/index.css
+++ b/index.css
@@ -190,20 +190,12 @@ t,
font-size: 0.8em;
}
-#statesBody {
- stroke-width: 3;
-}
-
#statesHalo {
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
-#provincesBody {
- stroke-width: 0.2;
-}
-
#statesBody,
#provincesBody,
#relig,
@@ -220,6 +212,11 @@ t,
mask: url(#land);
}
+#statesBody,
+#provincesBody {
+ stroke-width: 3;
+}
+
#borders {
stroke-linejoin: round;
fill: none;
diff --git a/index.html b/index.html
index ef4d1660..096a3e7b 100644
--- a/index.html
+++ b/index.html
@@ -8030,7 +8030,8 @@
-
+
+
diff --git a/main.js b/main.js
index 4263318e..c2aba7a0 100644
--- a/main.js
+++ b/main.js
@@ -650,7 +650,8 @@ async function generate(options) {
Routes.generate();
Religions.generate();
BurgsAndStates.defineStateForms();
- BurgsAndStates.generateProvinces();
+ Provinces.generate();
+ Provinces.getPoles();
BurgsAndStates.defineBurgFeatures();
drawStates();
diff --git a/modules/burgs-and-states.js b/modules/burgs-and-states.js
index 86eca6d7..b08e1564 100644
--- a/modules/burgs-and-states.js
+++ b/modules/burgs-and-states.js
@@ -498,26 +498,19 @@ window.BurgsAndStates = (() => {
});
};
- // Resets the cultures of all burgs and states to their
- // cell or center cell's (respectively) culture.
+ // Resets the cultures of all burgs and states to their cell or center cell's (respectively) culture
const updateCultures = () => {
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) => {
- // Ignore metadata burg
- if (index === 0) {
- return burg;
- }
+ if (index === 0) return burg;
return {...burg, culture: pack.cells.culture[burg.cell]};
});
- // Assign the culture associated with the states' center cell.
+ // Assign the culture associated with the states' center cell
pack.states = pack.states.map((state, index) => {
- // Ignore neutrals state
- if (index === 0) {
- return state;
- }
+ if (index === 0) return state;
return {...state, culture: pack.cells.culture[state.center]};
});
@@ -961,247 +954,6 @@ window.BurgsAndStates = (() => {
return adjName ? `${getAdjective(state.name)} ${state.formName}` : `${state.formName} of ${state.name}`;
};
- const generateProvinces = (regenerate = false, regenerateInLockedStates = false) => {
- TIME && console.time("generateProvinces");
- const localSeed = regenerate ? generateSeed() : seed;
- Math.random = aleaPRNG(localSeed);
-
- const {cells, states, burgs} = pack;
- const provinces = [0];
- const provinceIds = new Uint16Array(cells.i.length);
-
- const isProvinceLocked = province => province.lock || (!regenerateInLockedStates && states[province.state]?.lock);
- const isProvinceCellLocked = cell => provinceIds[cell] && isProvinceLocked(provinces[provinceIds[cell]]);
-
- if (regenerate) {
- pack.provinces.forEach(province => {
- if (!province.i || province.removed || !isProvinceLocked(province)) return;
-
- const newId = provinces.length;
- for (const i of cells.i) {
- if (cells.province[i] === province.i) provinceIds[i] = newId;
- }
-
- province.i = newId;
- provinces.push(province);
- });
- }
-
- const provincesRatio = +byId("provincesRatio").value;
- const max = provincesRatio == 100 ? 1000 : gauss(20, 5, 5, 100) * provincesRatio ** 0.5; // max growth
-
- const forms = {
- Monarchy: {County: 22, Earldom: 6, Shire: 2, Landgrave: 2, Margrave: 2, Barony: 2, Captaincy: 1, Seneschalty: 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 selected burgs
- states.forEach(s => {
- s.provinces = [];
- if (!s.i || s.removed) return;
- if (provinces.length) s.provinces = provinces.filter(p => p.state === s.i).map(p => p.i); // locked provinces ids
- if (s.lock && !regenerateInLockedStates) return; // don't regenerate provinces of a locked state
-
- const stateBurgs = burgs
- .filter(b => b.state === s.i && !b.removed && !provinceIds[b.cell])
- .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 * provincesRatio) / 100), 2);
-
- const form = Object.assign({}, forms[s.form]);
-
- for (let i = 0; i < provincesNumber; i++) {
- const provinceId = provinces.length;
- const center = stateBurgs[i].cell;
- const burg = stateBurgs[i].i;
- const c = stateBurgs[i].culture;
- 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 color = getMixedColor(s.color);
- 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);
-
- s.provinces.push(provinceId);
- provinces.push({i: provinceId, 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(p => {
- if (!p.i || p.removed || isProvinceLocked(p)) return;
- provinceIds[p.center] = p.i;
- queue.queue({e: p.center, p: 0, province: p.i, state: p.state});
- cost[p.center] = 1;
- });
-
- while (queue.length) {
- const {e, p, province, state} = queue.dequeue();
-
- cells.c[e].forEach(e => {
- if (isProvinceCellLocked(e)) return; // do not overwrite cell of locked provinces
-
- const land = cells.h[e] >= 20;
- if (!land && !cells.t[e]) return; // cannot pass deep ocean
- if (land && cells.state[e] !== state) return;
- const evevation = cells.h[e] >= 70 ? 100 : cells.h[e] >= 50 ? 30 : cells.h[e] >= 20 ? 10 : 100;
- const totalCost = p + evevation;
-
- if (totalCost > max) return;
- if (!cost[e] || totalCost < cost[e]) {
- if (land) provinceIds[e] = province; // assign province to a cell
- cost[e] = totalCost;
- queue.queue({e, p: totalCost, province, state});
- }
- });
- }
-
- // justify provinces shapes a bit
- for (const i of cells.i) {
- if (cells.burg[i]) continue; // do not overwrite burgs
- if (isProvinceCellLocked(i)) continue; // do not overwrite cell of locked provinces
-
- const neibs = cells.c[i]
- .filter(c => cells.state[c] === cells.state[i] && !isProvinceCellLocked(c))
- .map(c => provinceIds[c]);
- const adversaries = neibs.filter(c => c !== provinceIds[i]);
- if (adversaries.length < 2) continue;
-
- const buddies = neibs.filter(c => c === provinceIds[i]).length;
- if (buddies.length > 2) continue;
-
- const competitors = adversaries.map(p => adversaries.reduce((s, v) => (v === p ? s + 1 : s), 0));
- const max = d3.max(competitors);
- if (buddies >= max) continue;
-
- provinceIds[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] && !provinceIds[i]); // cells without province assigned
- states.forEach(s => {
- if (!s.i || s.removed) return;
- if (s.lock && !regenerateInLockedStates) return;
- if (!s.provinces.length) return;
-
- const coreProvinceNames = s.provinces.map(p => provinces[p]?.name);
- const colonyNamePool = [s.name, ...coreProvinceNames].filter(name => name && !/new/i.test(name));
- const getColonyName = () => {
- if (colonyNamePool.length < 1) return null;
-
- const index = rand(colonyNamePool.length - 1);
- const spliced = colonyNamePool.splice(index, 1);
- return spliced[0] ? `New ${spliced[0]}` : null;
- };
-
- let stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !provinceIds[i]);
- while (stateNoProvince.length) {
- // add new province
- const provinceId = provinces.length;
- const burgCell = stateNoProvince.find(i => cells.burg[i]);
- const center = burgCell ? burgCell : stateNoProvince[0];
- const burg = burgCell ? cells.burg[burgCell] : 0;
- provinceIds[center] = provinceId;
-
- // expand province
- const cost = [];
- cost[center] = 1;
- queue.queue({e: center, p: 0});
- while (queue.length) {
- const {e, p} = queue.dequeue();
-
- cells.c[e].forEach(nextCellId => {
- if (provinceIds[nextCellId]) return;
- const land = cells.h[nextCellId] >= 20;
- if (cells.state[nextCellId] && cells.state[nextCellId] !== s.i) return;
- const ter = land ? (cells.state[nextCellId] === s.i ? 3 : 20) : cells.t[nextCellId] ? 10 : 30;
- const totalCost = p + ter;
-
- if (totalCost > max) return;
- if (!cost[nextCellId] || totalCost < cost[nextCellId]) {
- if (land && cells.state[nextCellId] === s.i) provinceIds[nextCellId] = provinceId; // assign province to a cell
- cost[nextCellId] = totalCost;
- queue.queue({e: nextCellId, p: totalCost});
- }
- });
- }
-
- // generate "wild" province name
- const c = cells.culture[center];
- const f = pack.features[cells.f[center]];
- const color = getMixedColor(s.color);
-
- const provCells = stateNoProvince.filter(i => provinceIds[i] === provinceId);
- 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 name = (() => {
- const colonyName = colony && P(0.8) && getColonyName();
- if (colonyName) return colonyName;
- if (burgCell && P(0.5)) return burgs[burg].name;
- return Names.getState(Names.getCultureShort(c), c);
- })();
-
- const formName = (() => {
- if (singleIsle) return "Island";
- if (isleGroup) return "Islands";
- if (colony) return "Colony";
- return rw(forms["Wild"]);
- })();
-
- const fullName = name + " " + formName;
-
- 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: provinceId, state: s.i, center, burg, name, formName, fullName, color, coa});
- s.provinces.push(provinceId);
-
- // 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];
- while (queue.length) {
- const current = queue.pop();
- if (current === to) return true; // way is found
- cells.c[current].forEach(c => {
- if (used[c] || cells.h[c] < 20 || cells.state[c] !== state) return;
- queue.push(c);
- used[c] = 1;
- });
- }
- return false; // way is not found
- }
-
- // re-check
- stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !provinceIds[i]);
- }
- });
-
- cells.province = provinceIds;
- pack.provinces = provinces;
-
- TIME && console.timeEnd("generateProvinces");
- };
-
return {
generate,
expandStates,
@@ -1218,7 +970,6 @@ window.BurgsAndStates = (() => {
generateDiplomacy,
defineStateForms,
getFullName,
- generateProvinces,
updateCultures
};
})();
diff --git a/modules/dynamic/auto-update.js b/modules/dynamic/auto-update.js
index a11faa31..de93a8a1 100644
--- a/modules/dynamic/auto-update.js
+++ b/modules/dynamic/auto-update.js
@@ -52,7 +52,8 @@ export function resolveVersionConflicts(mapVersion) {
BurgsAndStates.generateDiplomacy();
BurgsAndStates.defineStateForms();
drawStates();
- BurgsAndStates.generateProvinces();
+ Provinces.generate();
+ Provinces.getPoles();
drawBorders();
if (!layerIsOn("toggleBorders")) $("#borders").fadeOut();
if (!layerIsOn("toggleStates")) regions.attr("display", "none").selectAll("path").remove();
@@ -940,4 +941,10 @@ export function resolveVersionConflicts(mapVersion) {
zones.style("display", null).selectAll("*").remove();
if (layerIsOn("toggleZones")) drawZones();
}
+
+ if (isOlderThan("1.103.0")) {
+ // v1.103.00 separated pole of inaccessibility detection from layer rendering
+ BurgsAndStates.getPoles();
+ Provinces.getPoles();
+ }
}
diff --git a/modules/dynamic/editors/states-editor.js b/modules/dynamic/editors/states-editor.js
index 2e6c6ea6..d6a14cef 100644
--- a/modules/dynamic/editors/states-editor.js
+++ b/modules/dynamic/editors/states-editor.js
@@ -839,7 +839,8 @@ function recalculateStates(must) {
if (!must && !statesAutoChange.checked) return;
BurgsAndStates.expandStates();
- BurgsAndStates.generateProvinces();
+ Provinces.generate();
+ Provinces.getPoles();
if (!layerIsOn("toggleStates")) toggleStates();
else drawStates();
if (!layerIsOn("toggleBorders")) toggleBorders();
diff --git a/modules/provinces-generator.js b/modules/provinces-generator.js
new file mode 100644
index 00000000..340562e9
--- /dev/null
+++ b/modules/provinces-generator.js
@@ -0,0 +1,257 @@
+"use strict";
+
+window.Provinces = (function () {
+ const forms = {
+ Monarchy: {County: 22, Earldom: 6, Shire: 2, Landgrave: 2, Margrave: 2, Barony: 2, Captaincy: 1, Seneschalty: 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}
+ };
+
+ const generate = (regenerate = false, regenerateLockedStates = false) => {
+ TIME && console.time("generateProvinces");
+ const localSeed = regenerate ? generateSeed() : seed;
+ Math.random = aleaPRNG(localSeed);
+
+ const {cells, states, burgs} = pack;
+ const provinces = [0]; // 0 index is reserved for "no province"
+ const provinceIds = new Uint16Array(cells.i.length);
+
+ const isProvinceLocked = province => province.lock || (!regenerateLockedStates && states[province.state]?.lock);
+ const isProvinceCellLocked = cell => provinceIds[cell] && isProvinceLocked(provinces[provinceIds[cell]]);
+
+ if (regenerate) {
+ pack.provinces.forEach(province => {
+ if (!province.i || province.removed || !isProvinceLocked(province)) return;
+
+ const newId = provinces.length;
+ for (const i of cells.i) {
+ if (cells.province[i] === province.i) provinceIds[i] = newId;
+ }
+
+ province.i = newId;
+ provinces.push(province);
+ });
+ }
+
+ const provincesRatio = +byId("provincesRatio").value;
+ const max = provincesRatio == 100 ? 1000 : gauss(20, 5, 5, 100) * provincesRatio ** 0.5; // max growth
+
+ // generate provinces for selected burgs
+ states.forEach(s => {
+ s.provinces = [];
+ if (!s.i || s.removed) return;
+ if (provinces.length) s.provinces = provinces.filter(p => p.state === s.i).map(p => p.i); // locked provinces ids
+ if (s.lock && !regenerateLockedStates) return; // don't regenerate provinces of a locked state
+
+ const stateBurgs = burgs
+ .filter(b => b.state === s.i && !b.removed && !provinceIds[b.cell])
+ .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 * provincesRatio) / 100), 2);
+
+ const form = Object.assign({}, forms[s.form]);
+
+ for (let i = 0; i < provincesNumber; i++) {
+ const provinceId = provinces.length;
+ const center = stateBurgs[i].cell;
+ const burg = stateBurgs[i].i;
+ const c = stateBurgs[i].culture;
+ 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 color = getMixedColor(s.color);
+ const kinship = nameByBurg ? 0.8 : 0.4;
+ const type = BurgsAndStates.getType(center, burg.port);
+ const coa = COA.generate(stateBurgs[i].coa, kinship, null, type);
+ coa.shield = COA.getShield(c, s.i);
+
+ s.provinces.push(provinceId);
+ provinces.push({i: provinceId, 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(p => {
+ if (!p.i || p.removed || isProvinceLocked(p)) return;
+ provinceIds[p.center] = p.i;
+ queue.queue({e: p.center, p: 0, province: p.i, state: p.state});
+ cost[p.center] = 1;
+ });
+
+ while (queue.length) {
+ const {e, p, province, state} = queue.dequeue();
+
+ cells.c[e].forEach(e => {
+ if (isProvinceCellLocked(e)) return; // do not overwrite cell of locked provinces
+
+ const land = cells.h[e] >= 20;
+ if (!land && !cells.t[e]) return; // cannot pass deep ocean
+ if (land && cells.state[e] !== state) return;
+ const evevation = cells.h[e] >= 70 ? 100 : cells.h[e] >= 50 ? 30 : cells.h[e] >= 20 ? 10 : 100;
+ const totalCost = p + evevation;
+
+ if (totalCost > max) return;
+ if (!cost[e] || totalCost < cost[e]) {
+ if (land) provinceIds[e] = province; // assign province to a cell
+ cost[e] = totalCost;
+ queue.queue({e, p: totalCost, province, state});
+ }
+ });
+ }
+
+ // justify provinces shapes a bit
+ for (const i of cells.i) {
+ if (cells.burg[i]) continue; // do not overwrite burgs
+ if (isProvinceCellLocked(i)) continue; // do not overwrite cell of locked provinces
+
+ const neibs = cells.c[i]
+ .filter(c => cells.state[c] === cells.state[i] && !isProvinceCellLocked(c))
+ .map(c => provinceIds[c]);
+ const adversaries = neibs.filter(c => c !== provinceIds[i]);
+ if (adversaries.length < 2) continue;
+
+ const buddies = neibs.filter(c => c === provinceIds[i]).length;
+ if (buddies.length > 2) continue;
+
+ const competitors = adversaries.map(p => adversaries.reduce((s, v) => (v === p ? s + 1 : s), 0));
+ const max = d3.max(competitors);
+ if (buddies >= max) continue;
+
+ provinceIds[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] && !provinceIds[i]); // cells without province assigned
+ states.forEach(s => {
+ if (!s.i || s.removed) return;
+ if (s.lock && !regenerateLockedStates) return;
+ if (!s.provinces.length) return;
+
+ const coreProvinceNames = s.provinces.map(p => provinces[p]?.name);
+ const colonyNamePool = [s.name, ...coreProvinceNames].filter(name => name && !/new/i.test(name));
+ const getColonyName = () => {
+ if (colonyNamePool.length < 1) return null;
+
+ const index = rand(colonyNamePool.length - 1);
+ const spliced = colonyNamePool.splice(index, 1);
+ return spliced[0] ? `New ${spliced[0]}` : null;
+ };
+
+ let stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !provinceIds[i]);
+ while (stateNoProvince.length) {
+ // add new province
+ const provinceId = provinces.length;
+ const burgCell = stateNoProvince.find(i => cells.burg[i]);
+ const center = burgCell ? burgCell : stateNoProvince[0];
+ const burg = burgCell ? cells.burg[burgCell] : 0;
+ provinceIds[center] = provinceId;
+
+ // expand province
+ const cost = [];
+ cost[center] = 1;
+ queue.queue({e: center, p: 0});
+ while (queue.length) {
+ const {e, p} = queue.dequeue();
+
+ cells.c[e].forEach(nextCellId => {
+ if (provinceIds[nextCellId]) return;
+ const land = cells.h[nextCellId] >= 20;
+ if (cells.state[nextCellId] && cells.state[nextCellId] !== s.i) return;
+ const ter = land ? (cells.state[nextCellId] === s.i ? 3 : 20) : cells.t[nextCellId] ? 10 : 30;
+ const totalCost = p + ter;
+
+ if (totalCost > max) return;
+ if (!cost[nextCellId] || totalCost < cost[nextCellId]) {
+ if (land && cells.state[nextCellId] === s.i) provinceIds[nextCellId] = provinceId; // assign province to a cell
+ cost[nextCellId] = totalCost;
+ queue.queue({e: nextCellId, p: totalCost});
+ }
+ });
+ }
+
+ // generate "wild" province name
+ const c = cells.culture[center];
+ const f = pack.features[cells.f[center]];
+ const color = getMixedColor(s.color);
+
+ const provCells = stateNoProvince.filter(i => provinceIds[i] === provinceId);
+ 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 name = (() => {
+ const colonyName = colony && P(0.8) && getColonyName();
+ if (colonyName) return colonyName;
+ if (burgCell && P(0.5)) return burgs[burg].name;
+ return Names.getState(Names.getCultureShort(c), c);
+ })();
+
+ const formName = (() => {
+ if (singleIsle) return "Island";
+ if (isleGroup) return "Islands";
+ if (colony) return "Colony";
+ return rw(forms["Wild"]);
+ })();
+
+ const fullName = name + " " + formName;
+
+ const dominion = colony ? P(0.95) : singleIsle || isleGroup ? P(0.7) : P(0.3);
+ const kinship = dominion ? 0 : 0.4;
+ const type = BurgsAndStates.getType(center, burgs[burg]?.port);
+ const coa = COA.generate(s.coa, kinship, dominion, type);
+ coa.shield = COA.getShield(c, s.i);
+
+ provinces.push({i: provinceId, state: s.i, center, burg, name, formName, fullName, color, coa});
+ s.provinces.push(provinceId);
+
+ // 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];
+ while (queue.length) {
+ const current = queue.pop();
+ if (current === to) return true; // way is found
+ cells.c[current].forEach(c => {
+ if (used[c] || cells.h[c] < 20 || cells.state[c] !== state) return;
+ queue.push(c);
+ used[c] = 1;
+ });
+ }
+ return false; // way is not found
+ }
+
+ // re-check
+ stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !provinceIds[i]);
+ }
+ });
+
+ cells.province = provinceIds;
+ pack.provinces = provinces;
+
+ TIME && console.timeEnd("generateProvinces");
+ };
+
+ // calculate pole of inaccessibility for each province
+ const getPoles = () => {
+ const getType = cellId => pack.cells.province[cellId];
+ const poles = getPolesOfInaccessibility(getType);
+
+ pack.provinces.forEach(province => {
+ if (!province.i || province.removed) return;
+ province.pole = poles[province.i] || [0, 0];
+ });
+ };
+
+ return {generate, getPoles};
+})();
diff --git a/modules/ui/heightmap-editor.js b/modules/ui/heightmap-editor.js
index a88c698c..cd49f52c 100644
--- a/modules/ui/heightmap-editor.js
+++ b/modules/ui/heightmap-editor.js
@@ -249,7 +249,8 @@ function editHeightmap(options) {
Routes.generate();
Religions.generate();
BurgsAndStates.defineStateForms();
- BurgsAndStates.generateProvinces();
+ Provinces.generate();
+ Provinces.getPoles();
BurgsAndStates.defineBurgFeatures();
drawStates();
diff --git a/modules/ui/layers.js b/modules/ui/layers.js
index fb2b73ed..efe85b89 100644
--- a/modules/ui/layers.js
+++ b/modules/ui/layers.js
@@ -1002,7 +1002,8 @@ function drawStates() {
const color = states[index].color;
bodyPaths.push(
- /* html */ ``
+ /* html */ ``,
+ /* html */ ``
);
if (renderHalo) {
@@ -1150,10 +1151,7 @@ function toggleProvinces(event) {
drawProvinces();
if (event && isCtrlClick(event)) editStyle("provs");
} else {
- if (event && isCtrlClick(event)) {
- editStyle("provs");
- return;
- }
+ if (event && isCtrlClick(event)) return editStyle("provs");
provs.selectAll("*").remove();
turnButtonOff("toggleProvinces");
}
@@ -1161,128 +1159,34 @@ function toggleProvinces(event) {
function drawProvinces() {
TIME && console.time("drawProvinces");
- const labelsOn = provs.attr("data-labels") == 1;
- provs.selectAll("*").remove();
+ const {cells, provinces} = pack;
- const provinces = pack.provinces;
- const {body, gap} = getProvincesVertices();
+ const bodyPaths = new Array(provinces.length - 1);
+ const isolines = getIsolines(cellId => cells.province[cellId], {fill: true, waterGap: true});
+ for (const [index, {fill, waterGap}] of isolines) {
+ const color = provinces[index].color;
+ bodyPaths.push(
+ /* html */ ``,
+ /* html */ ``
+ );
+ }
- const g = provs.append("g").attr("id", "provincesBody");
- const bodyData = body.map((p, i) => [p.length > 10 ? p : null, i, provinces[i].color]).filter(d => d[0]);
- g.selectAll("path")
- .data(bodyData)
- .enter()
- .append("path")
- .attr("d", d => d[0])
- .attr("fill", d => d[2])
- .attr("stroke", "none")
- .attr("id", d => "province" + d[1]);
- const gapData = gap.map((p, i) => [p.length > 10 ? p : null, i, provinces[i].color]).filter(d => d[0]);
- g.selectAll(".path")
- .data(gapData)
- .enter()
- .append("path")
- .attr("d", d => d[0])
- .attr("fill", "none")
- .attr("stroke", d => d[2])
- .attr("id", d => "province-gap" + d[1]);
+ const labels = provinces
+ .filter(p => p.i && !p.removed)
+ .map(p => {
+ const [x, y] = p.pole || cells.p[p.center];
+ return /* html */ `${p.name}`;
+ });
- const labels = provs.append("g").attr("id", "provinceLabels");
- labels.style("display", `${labelsOn ? "block" : "none"}`);
- const labelData = provinces.filter(p => p.i && !p.removed && p.pole);
- labels
- .selectAll(".path")
- .data(labelData)
- .enter()
- .append("text")
- .attr("x", d => d.pole[0])
- .attr("y", d => d.pole[1])
- .attr("id", d => "provinceLabel" + d.i)
- .text(d => d.name);
+ byId("provs").innerHTML = /* html */ `
+ ${bodyPaths.join("")}
+ ${labels.join("")}
+ `;
+ byId("provinceLabels").style.display = byId("provs").dataset.labels === "1" ? "block" : "none";
TIME && console.timeEnd("drawProvinces");
}
-function getProvincesVertices() {
- const cells = pack.cells,
- vertices = pack.vertices,
- provinces = pack.provinces,
- n = cells.i.length;
- const used = new Uint8Array(cells.i.length);
- const vArray = new Array(provinces.length); // store vertices array
- const body = new Array(provinces.length).fill(""); // store path around each province
- const gap = new Array(provinces.length).fill(""); // store path along water for each province to fill the gaps
-
- for (const i of cells.i) {
- if (!cells.province[i] || used[i]) continue;
- const p = cells.province[i];
- const onborder = cells.c[i].some(n => cells.province[n] !== p);
- if (!onborder) continue;
-
- const borderWith = cells.c[i].map(c => cells.province[c]).find(n => n !== p);
- const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.province[i] === borderWith));
- const chain = connectVertices(vertex, p, borderWith);
- if (chain.length < 3) continue;
- const points = chain.map(v => vertices.p[v[0]]);
- if (!vArray[p]) vArray[p] = [];
- vArray[p].push(points);
- body[p] += "M" + points.join("L");
- gap[p] +=
- "M" +
- vertices.p[chain[0][0]] +
- chain.reduce(
- (r, v, i, d) =>
- !i ? r : !v[2] ? r + "L" + vertices.p[v[0]] : d[i + 1] && !d[i + 1][2] ? r + "M" + vertices.p[v[0]] : r,
- ""
- );
- }
-
- // find province visual center
- vArray.forEach((ar, i) => {
- const sorted = ar.sort((a, b) => b.length - a.length); // sort by points number
- provinces[i].pole = polylabel(sorted, 1.0); // pole of inaccessibility
- });
-
- return {body, gap};
-
- // connect vertices to chain
- function connectVertices(start, t, province) {
- const chain = []; // vertices chain to form a path
- let land = vertices.c[start].some(c => cells.h[c] >= 20 && cells.province[c] !== t);
- function check(i) {
- province = cells.province[i];
- land = cells.h[i] >= 20;
- }
-
- for (let i = 0, current = start; i === 0 || (current !== start && i < 20000); i++) {
- const prev = chain[chain.length - 1] ? chain[chain.length - 1][0] : -1; // previous vertex in chain
- chain.push([current, province, land]); // add current vertex to sequence
- const c = vertices.c[current]; // cells adjacent to vertex
- c.filter(c => cells.province[c] === t).forEach(c => (used[c] = 1));
- const c0 = c[0] >= n || cells.province[c[0]] !== t;
- const c1 = c[1] >= n || cells.province[c[1]] !== t;
- const c2 = c[2] >= n || cells.province[c[2]] !== t;
- const v = vertices.v[current]; // neighboring vertices
- if (v[0] !== prev && c0 !== c1) {
- current = v[0];
- check(c0 ? c[0] : c[1]);
- } else if (v[1] !== prev && c1 !== c2) {
- current = v[1];
- check(c1 ? c[1] : c[2]);
- } else if (v[2] !== prev && c0 !== c2) {
- current = v[2];
- check(c2 ? c[2] : c[0]);
- }
- if (current === chain[chain.length - 1][0]) {
- ERROR && console.error("Next vertex is not found");
- break;
- }
- }
- chain.push([start, province, land]); // add starting vertex to sequence to close the path
- return chain;
- }
-}
-
function toggleGrid(event) {
if (!gridOverlay.selectAll("*").size()) {
turnButtonOn("toggleGrid");
@@ -1832,7 +1736,6 @@ function drawEmblems() {
const sizeProvinces = getProvinceEmblemsSize();
const provinceCOAs = validProvinces.map(province => {
- if (!province.pole) getProvincesVertices();
const [x, y] = province.pole || pack.cells.p[province.center];
const size = province.coa.size || 1;
const shift = (sizeProvinces * size) / 2;
diff --git a/modules/ui/provinces-editor.js b/modules/ui/provinces-editor.js
index f8a88cfb..2846a054 100644
--- a/modules/ui/provinces-editor.js
+++ b/modules/ui/provinces-editor.js
@@ -8,7 +8,7 @@ function editProvinces() {
if (layerIsOn("toggleCultures")) toggleCultures();
provs.selectAll("text").call(d3.drag().on("drag", dragLabel)).classed("draggable", true);
- const body = document.getElementById("provincesBodySection");
+ const body = byId("provincesBodySection");
refreshProvincesEditor();
if (modules.editProvinces) return;
@@ -23,22 +23,22 @@ function editProvinces() {
});
// add listeners
- document.getElementById("provincesEditorRefresh").addEventListener("click", refreshProvincesEditor);
- document.getElementById("provincesEditStyle").addEventListener("click", () => editStyle("provs"));
- document.getElementById("provincesFilterState").addEventListener("change", provincesEditorAddLines);
- document.getElementById("provincesPercentage").addEventListener("click", togglePercentageMode);
- document.getElementById("provincesChart").addEventListener("click", showChart);
- document.getElementById("provincesToggleLabels").addEventListener("click", toggleLabels);
- document.getElementById("provincesExport").addEventListener("click", downloadProvincesData);
- document.getElementById("provincesRemoveAll").addEventListener("click", removeAllProvinces);
- document.getElementById("provincesManually").addEventListener("click", enterProvincesManualAssignent);
- document.getElementById("provincesManuallyApply").addEventListener("click", applyProvincesManualAssignent);
- document.getElementById("provincesManuallyCancel").addEventListener("click", () => exitProvincesManualAssignment());
- document.getElementById("provincesRelease").addEventListener("click", triggerProvincesRelease);
- document.getElementById("provincesAdd").addEventListener("click", enterAddProvinceMode);
- document.getElementById("provincesRecolor").addEventListener("click", recolorProvinces);
+ byId("provincesEditorRefresh").on("click", refreshProvincesEditor);
+ byId("provincesEditStyle").on("click", () => editStyle("provs"));
+ byId("provincesFilterState").on("change", provincesEditorAddLines);
+ byId("provincesPercentage").on("click", togglePercentageMode);
+ byId("provincesChart").on("click", showChart);
+ byId("provincesToggleLabels").on("click", toggleLabels);
+ byId("provincesExport").on("click", downloadProvincesData);
+ byId("provincesRemoveAll").on("click", removeAllProvinces);
+ byId("provincesManually").on("click", enterProvincesManualAssignent);
+ byId("provincesManuallyApply").on("click", applyProvincesManualAssignent);
+ byId("provincesManuallyCancel").on("click", () => exitProvincesManualAssignment());
+ byId("provincesRelease").on("click", triggerProvincesRelease);
+ byId("provincesAdd").on("click", enterAddProvinceMode);
+ byId("provincesRecolor").on("click", recolorProvinces);
- body.addEventListener("click", function (ev) {
+ body.on("click", function (ev) {
if (customization) return;
const el = ev.target,
cl = el.classList,
@@ -58,7 +58,7 @@ function editProvinces() {
else if (cl.contains("icon-lock") || cl.contains("icon-lock-open")) updateLockStatus(p, cl);
});
- body.addEventListener("change", function (ev) {
+ body.on("change", function (ev) {
const el = ev.target,
cl = el.classList,
line = el.parentNode,
@@ -100,7 +100,7 @@ function editProvinces() {
}
function updateFilter() {
- const stateFilter = document.getElementById("provincesFilterState");
+ const stateFilter = byId("provincesFilterState");
const selectedState = stateFilter.value || 1;
stateFilter.options.length = 0; // remove all options
stateFilter.options.add(new Option(`all`, -1, false, selectedState == -1));
@@ -111,7 +111,7 @@ function editProvinces() {
// add line for each province
function provincesEditorAddLines() {
const unit = " " + getAreaUnit();
- const selectedState = +document.getElementById("provincesFilterState").value;
+ const selectedState = +byId("provincesFilterState").value;
let filtered = pack.provinces.filter(p => p.i && !p.removed); // all valid burgs
if (selectedState != -1) filtered = filtered.filter(p => p.state === selectedState); // filtered by state
body.innerHTML = "";
@@ -194,9 +194,9 @@ function editProvinces() {
byId("provincesFooterPopulation").dataset.population = totalPopulation;
body.querySelectorAll("div.states").forEach(el => {
- el.addEventListener("click", selectProvinceOnLineClick);
- el.addEventListener("mouseenter", ev => provinceHighlightOn(ev));
- el.addEventListener("mouseleave", ev => provinceHighlightOff(ev));
+ el.on("click", selectProvinceOnLineClick);
+ el.on("mouseenter", ev => provinceHighlightOn(ev));
+ el.on("mouseleave", ev => provinceHighlightOff(ev));
});
if (body.dataset.type === "percentage") {
@@ -306,7 +306,7 @@ function editProvinces() {
const {cell: center, culture} = burgs[burgId];
const color = getRandomColor();
const coa = province.coa;
- const coaEl = document.getElementById("provinceCOA" + provinceId);
+ const coaEl = byId("provinceCOA" + provinceId);
if (coaEl) coaEl.id = "stateCOA" + newStateId;
emblems.select(`#provinceEmblems > use[data-i='${provinceId}']`).remove();
@@ -482,7 +482,7 @@ function editProvinces() {
unfog("focusProvince" + p);
const coaId = "provinceCOA" + p;
- if (document.getElementById(coaId)) document.getElementById(coaId).remove();
+ if (byId(coaId)) byId(coaId).remove();
emblems.select(`#provinceEmblems > use[data-i='${p}']`).remove();
pack.provinces[p] = {i: p, removed: true};
@@ -504,13 +504,13 @@ function editProvinces() {
function editProvinceName(province) {
const p = pack.provinces[province];
- document.getElementById("provinceNameEditor").dataset.province = province;
- document.getElementById("provinceNameEditorShort").value = p.name;
+ byId("provinceNameEditor").dataset.province = province;
+ byId("provinceNameEditorShort").value = p.name;
applyOption(provinceNameEditorSelectForm, p.formName);
- document.getElementById("provinceNameEditorFull").value = p.fullName;
+ byId("provinceNameEditorFull").value = p.fullName;
const cultureId = pack.cells.culture[p.center];
- document.getElementById("provinceCultureDisplay").innerText = pack.cultures[cultureId].name;
+ byId("provinceCultureDisplay").innerText = pack.cultures[cultureId].name;
$("#provinceNameEditor").dialog({
resizable: false,
@@ -531,22 +531,22 @@ function editProvinces() {
modules.editProvinceName = true;
// add listeners
- document.getElementById("provinceNameEditorShortCulture").addEventListener("click", regenerateShortNameCulture);
- document.getElementById("provinceNameEditorShortRandom").addEventListener("click", regenerateShortNameRandom);
- document.getElementById("provinceNameEditorAddForm").addEventListener("click", addCustomForm);
- document.getElementById("provinceNameEditorFullRegenerate").addEventListener("click", regenerateFullName);
+ byId("provinceNameEditorShortCulture").on("click", regenerateShortNameCulture);
+ byId("provinceNameEditorShortRandom").on("click", regenerateShortNameRandom);
+ byId("provinceNameEditorAddForm").on("click", addCustomForm);
+ byId("provinceNameEditorFullRegenerate").on("click", regenerateFullName);
function regenerateShortNameCulture() {
const province = +provinceNameEditor.dataset.province;
const culture = pack.cells.culture[pack.provinces[province].center];
const name = Names.getState(Names.getCultureShort(culture), culture);
- document.getElementById("provinceNameEditorShort").value = name;
+ byId("provinceNameEditorShort").value = name;
}
function regenerateShortNameRandom() {
const base = rand(nameBases.length - 1);
const name = Names.getState(Names.getBase(base), undefined, base);
- document.getElementById("provinceNameEditorShort").value = name;
+ byId("provinceNameEditorShort").value = name;
}
function addCustomForm() {
@@ -558,9 +558,9 @@ function editProvinces() {
}
function regenerateFullName() {
- const short = document.getElementById("provinceNameEditorShort").value;
- const form = document.getElementById("provinceNameEditorSelectForm").value;
- document.getElementById("provinceNameEditorFull").value = getFullName();
+ const short = byId("provinceNameEditorShort").value;
+ const form = byId("provinceNameEditorSelectForm").value;
+ byId("provinceNameEditorFull").value = getFullName();
function getFullName() {
if (!form) return short;
@@ -570,9 +570,9 @@ function editProvinces() {
}
function applyNameChange(p) {
- p.name = document.getElementById("provinceNameEditorShort").value;
- p.formName = document.getElementById("provinceNameEditorSelectForm").value;
- p.fullName = document.getElementById("provinceNameEditorFull").value;
+ p.name = byId("provinceNameEditorShort").value;
+ p.formName = byId("provinceNameEditorSelectForm").value;
+ p.fullName = byId("provinceNameEditorFull").value;
provs.select("#provinceLabel" + p.i).text(p.name);
refreshProvincesEditor();
}
@@ -651,7 +651,7 @@ function editProvinces() {
.attr("height", height)
.attr("font-size", "10px");
const graph = svg.append("g").attr("transform", `translate(10, 0)`);
- document.getElementById("provincesTreeType").addEventListener("change", updateChart);
+ byId("provincesTreeType").on("change", updateChart);
treeLayout(root);
@@ -688,7 +688,7 @@ function editProvinces() {
function hideInfo(ev) {
provinceHighlightOff(ev);
- if (!document.getElementById("provinceInfo")) return;
+ if (!byId("provinceInfo")) return;
provinceInfo.innerHTML = "";
d3.select(ev.target).select("rect").classed("selected", 0);
}
@@ -816,7 +816,7 @@ function editProvinces() {
stateBorders.select("path").attr("stroke", "#000").attr("stroke-width", 1.2);
customization = 11;
- provs.select("g#provincesBody").append("g").attr("id", "temp");
+ provs.select("g#provincesBody").append("g").attr("id", "temp").attr("stroke-width", 0.3);
provs
.select("g#provincesBody")
.append("g")
@@ -826,7 +826,7 @@ function editProvinces() {
.attr("stroke-width", 1);
document.querySelectorAll("#provincesBottom > *").forEach(el => (el.style.display = "none"));
- document.getElementById("provincesManuallyButtons").style.display = "inline-block";
+ byId("provincesManuallyButtons").style.display = "inline-block";
provincesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
provincesHeader.querySelector("div[data-sortby='state']").style.left = "7.7em";
@@ -952,8 +952,11 @@ function editProvinces() {
if (!layerIsOn("toggleBorders")) toggleBorders();
else drawBorders();
+
+ Provinces.getPoles();
if (!layerIsOn("toggleProvinces")) toggleProvinces();
else drawProvinces();
+
exitProvincesManualAssignment();
refreshProvincesEditor();
}
@@ -970,7 +973,7 @@ function editProvinces() {
debug.selectAll("path.selected").remove();
document.querySelectorAll("#provincesBottom > *").forEach(el => (el.style.display = "inline-block"));
- document.getElementById("provincesManuallyButtons").style.display = "none";
+ byId("provincesManuallyButtons").style.display = "none";
provincesEditor.querySelectorAll(".hide:not(.show)").forEach(el => el.classList.remove("hidden"));
provincesHeader.querySelector("div[data-sortby='state']").style.left = "22em";
@@ -1049,7 +1052,7 @@ function editProvinces() {
if (!layerIsOn("toggleProvinces")) toggleProvinces();
else drawProvinces();
collectStatistics();
- document.getElementById("provincesFilterState").value = state;
+ byId("provincesFilterState").value = state;
provincesEditorAddLines();
}
@@ -1062,7 +1065,7 @@ function editProvinces() {
}
function recolorProvinces() {
- const state = +document.getElementById("provincesFilterState").value;
+ const state = +byId("provincesFilterState").value;
pack.provinces.forEach(p => {
if (!p || p.removed) return;
diff --git a/modules/ui/tools.js b/modules/ui/tools.js
index 9b3a8e9d..aa3b831a 100644
--- a/modules/ui/tools.js
+++ b/modules/ui/tools.js
@@ -158,7 +158,8 @@ function regenerateStates() {
BurgsAndStates.generateCampaigns();
BurgsAndStates.generateDiplomacy();
BurgsAndStates.defineStateForms();
- BurgsAndStates.generateProvinces(true);
+ Provinces.generate(true);
+ Provinces.getPoles();
layerIsOn("toggleStates") ? drawStates() : toggleStates();
layerIsOn("toggleBorders") ? drawBorders() : toggleBorders();
@@ -333,7 +334,8 @@ function recreateStates() {
function regenerateProvinces() {
unfog();
- BurgsAndStates.generateProvinces(true, true);
+ Provinces.generate(true, true);
+ Provinces.getPoles();
drawBorders();
if (layerIsOn("toggleProvinces")) drawProvinces();
diff --git a/versioning.js b/versioning.js
index 403ae70c..c5f2a404 100644
--- a/versioning.js
+++ b/versioning.js
@@ -12,7 +12,7 @@
*
* Example: 1.102.0 -> Major version 1, Minor version 102, Patch version 0
*/
-const VERSION = "1.101.01";
+const VERSION = "1.103.00";
{
document.title += " v" + VERSION;