Generator uses pop-up window to download files. Please ensure your browser does not block popups.
diff --git a/package.json b/package.json
index 5754d5fc..417e5623 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
"@rollup/plugin-node-resolve": "^13.3.0",
"@rollup/plugin-replace": "^4.0.0",
"@types/d3": "^5.9.0",
+ "@types/d3-array": "^3.0.3",
"@types/delaunator": "^5.0.0",
"@types/jquery": "^3.5.14",
"@types/jqueryui": "^1.12.16",
@@ -35,6 +36,7 @@
},
"dependencies": {
"d3": "5.8.0",
+ "d3-array": "^3.2.0",
"delaunator": "^5.0.0",
"flatqueue": "^2.0.3",
"lineclip": "^1.1.5",
diff --git a/src/dialogs/dialogs/heightmap-editor.js b/src/dialogs/dialogs/heightmap-editor.js
index c464a867..6d8a678c 100644
--- a/src/dialogs/dialogs/heightmap-editor.js
+++ b/src/dialogs/dialogs/heightmap-editor.js
@@ -1190,7 +1190,7 @@ export function open(options) {
.on("click", mapClicked);
const colors = pallete.map(p => `rgb(${p[0]}, ${p[1]}, ${p[2]})`);
- d3.select("#colorsUnassigned")
+ d3.select("#colorsUnassignedContainer")
.selectAll("div")
.data(colors)
.enter()
@@ -1253,25 +1253,23 @@ export function open(options) {
this.setAttribute("data-height", height);
});
- if (selectedColor.parentNode.id === "colorsUnassigned") {
- colorsAssigned.appendChild(selectedColor);
+ if (selectedColor.parentNode.id === "colorsUnassignedContainer") {
+ colorsAssignedContainer.appendChild(selectedColor);
colorsAssigned.style.display = "block";
- byId("colorsUnassignedNumber").innerHTML = colorsUnassigned.childElementCount - 2;
- byId("colorsAssignedNumber").innerHTML = colorsAssigned.childElementCount - 2;
+ byId("colorsUnassignedNumber").innerHTML = colorsUnassignedContainer.childElementCount - 2;
+ byId("colorsAssignedNumber").innerHTML = colorsAssignedContainer.childElementCount - 2;
}
}
// auto assign color based on luminosity or hue
function autoAssing(type) {
- let unassigned = colorsUnassigned.querySelectorAll("div");
+ let unassigned = colorsUnassignedContainer.querySelectorAll("div");
if (!unassigned.length) {
heightsFromImage(+convertColors.value);
- unassigned = colorsUnassigned.querySelectorAll("div");
- if (!unassigned.length) {
- tip("No unassigned colors. Please load an image and click the button again", false, "error");
- return;
- }
+ unassigned = colorsUnassignedContainer.querySelectorAll("div");
+ if (!unassigned.length)
+ return tip("No unassigned colors. Please load an image and click the button again", false, "error");
}
const getHeightByHue = function (color) {
@@ -1315,18 +1313,18 @@ export function open(options) {
} // if color is already added, remove it
el.style.backgroundColor = el.dataset.color = colorTo;
el.dataset.height = height;
- colorsAssigned.appendChild(el);
+ colorsAssignedContainer.appendChild(el);
assinged[height] = true;
});
// sort assigned colors by height
- Array.from(colorsAssigned.children)
+ Array.from(colorsAssignedContainer.children)
.sort((a, b) => +a.dataset.height - +b.dataset.height)
- .forEach(line => colorsAssigned.appendChild(line));
+ .forEach(line => colorsAssignedContainer.appendChild(line));
colorsAssigned.style.display = "block";
colorsUnassigned.style.display = "none";
- byId("colorsAssignedNumber").innerHTML = colorsAssigned.childElementCount - 2;
+ byId("colorsAssignedNumber").innerHTML = colorsAssignedContainer.childElementCount - 2;
}
function setConvertColorsNumber() {
@@ -1346,7 +1344,8 @@ export function open(options) {
}
function applyConversion() {
- if (colorsAssigned.childElementCount < 3) return tip("Please do the assignment first", false, "error");
+ if (colorsAssignedContainer.childElementCount < 3)
+ return tip("Please assign colors to heights first", false, "error");
viewbox
.select("#heights")
diff --git a/src/dialogs/dialogs/label-editor.js b/src/dialogs/dialogs/label-editor.js
index 6c7aaf10..cf242fb4 100644
--- a/src/dialogs/dialogs/label-editor.js
+++ b/src/dialogs/dialogs/label-editor.js
@@ -14,6 +14,8 @@ export function open({el}) {
closeDialogs();
if (!layerIsOn("toggleLabels")) toggleLayer("toggleLabels");
+ const lineGen = d3.line().curve(d3.curveBundle.beta(1));
+
const textPath = el.parentNode;
const text = textPath.parentNode;
elSelected = d3.select(text).call(d3.drag().on("start", dragLabel)).classed("draggable", true);
@@ -123,8 +125,6 @@ export function open({el}) {
redrawLabelPath();
}
- const lineGen = d3.line().curve(d3.curveBundle.beta(1));
-
function redrawLabelPath() {
const path = byId("textPath_" + elSelected.attr("id"));
const points = [];
@@ -308,26 +308,12 @@ export function open({el}) {
function changeText() {
const input = byId("labelText").value;
const el = elSelected.select("textPath").node();
- const example = d3
- .select(elSelected.node().parentNode)
- .append("text")
- .attr("x", 0)
- .attr("x", 0)
- .attr("font-size", el.getAttribute("font-size"))
- .node();
const lines = input.split("|");
const top = (lines.length - 1) / -2; // y offset
- const inner = lines
- .map((l, d) => {
- example.innerHTML = l;
- const left = example.getBBox().width / -2; // x offset
- return `
${l}`;
- })
- .join("");
+ const inner = lines.map((l, d) => `
${l}`).join("");
el.innerHTML = inner;
- example.remove();
if (elSelected.attr("id").slice(0, 10) === "stateLabel")
tip("Use States Editor to change an actual state name, not just a label", false, "warning");
diff --git a/src/dialogs/dialogs/lake-editor.js b/src/dialogs/dialogs/lake-editor.js
index 71be417f..76961958 100644
--- a/src/dialogs/dialogs/lake-editor.js
+++ b/src/dialogs/dialogs/lake-editor.js
@@ -65,7 +65,7 @@ export function open({el}) {
const heights = lakeCells.map(i => cells.h[i]);
byId("lakeElevation").value = getHeight(l.height);
- byId("lakeAvarageDepth").value = getHeight(d3.mean(heights), true);
+ byId("lakeAverageDepth").value = getHeight(d3.mean(heights), true);
byId("lakeMaxDepth").value = getHeight(d3.min(heights), true);
byId("lakeFlux").value = l.flux;
diff --git a/src/index.css b/src/index.css
index a626de1c..7ceaada1 100644
--- a/src/index.css
+++ b/src/index.css
@@ -169,6 +169,7 @@ a {
font-size: 0.8em;
}
+#provincesBody,
#statesBody {
stroke-width: 3;
}
@@ -179,10 +180,6 @@ a {
stroke-linejoin: round;
}
-#provincesBody {
- stroke-width: 0.2;
-}
-
#statesBody,
#provincesBody,
#relig,
@@ -243,7 +240,7 @@ i.icon-lock {
}
#labels {
- text-anchor: start;
+ text-anchor: middle;
dominant-baseline: central;
cursor: pointer;
}
@@ -1144,12 +1141,17 @@ div#regimentSelectorBody > div > div {
filter: sepia(1) hue-rotate(200deg);
}
+.colorsContainer {
+ display: grid;
+ grid-template-columns: repeat(5, 1fr);
+ grid-column-gap: 0.3em;
+ grid-row-gap: 0.2em;
+}
+
.color-div {
width: 3em;
- height: 1em;
- display: inline-block;
- margin: 0 0.16em;
- border: 1px #c5c5c5 groove;
+ height: 1.5em;
+ border: 1px #999 solid;
cursor: pointer;
}
diff --git a/src/layers/renderers/drawEmblems.js b/src/layers/renderers/drawEmblems.js
index d571c681..06f1ca1e 100644
--- a/src/layers/renderers/drawEmblems.js
+++ b/src/layers/renderers/drawEmblems.js
@@ -1,6 +1,5 @@
import * as d3 from "d3";
-import {getProvincesVertices} from "./drawProvinces";
import {minmax, rn} from "utils/numberUtils";
import {byId} from "utils/shorthands";
@@ -42,7 +41,7 @@ export function drawEmblems() {
const sizeProvinces = getProvinceEmblemsSize();
const provinceCOAs = validProvinces.map(province => {
- if (!province.pole) getProvincesVertices();
+ if (!province.pole) throw "Pole is not defined";
const [x, y] = province.pole || pack.cells.p[province.center];
const size = province.coaSize || 1;
const shift = (sizeProvinces * size) / 2;
diff --git a/src/layers/renderers/drawLabels.ts b/src/layers/renderers/drawLabels/drawBurgLabels.ts
similarity index 80%
rename from src/layers/renderers/drawLabels.ts
rename to src/layers/renderers/drawLabels/drawBurgLabels.ts
index a7537c1d..a3e0eb8f 100644
--- a/src/layers/renderers/drawLabels.ts
+++ b/src/layers/renderers/drawLabels/drawBurgLabels.ts
@@ -1,15 +1,11 @@
-export function drawLabels() {
- drawBurgLabels();
- // TODO: draw other labels
+import * as d3 from "d3";
- window.Zoom.invoke();
-}
-
-function drawBurgLabels() {
+export function drawBurgLabels(burgs: TBurgs) {
// remove old data
+ const burgLabels = d3.select("#burgLabels");
burgLabels.selectAll("text").remove();
- const validBurgs = pack.burgs.filter(burg => burg.i && !(burg as IBurg).removed) as IBurg[];
+ const validBurgs = burgs.filter(burg => burg.i && !(burg as IBurg).removed) as IBurg[];
// capitals
const capitals = validBurgs.filter(burg => burg.capital);
diff --git a/src/layers/renderers/drawLabels/drawStateLabels.ts b/src/layers/renderers/drawLabels/drawStateLabels.ts
new file mode 100644
index 00000000..25cfc795
--- /dev/null
+++ b/src/layers/renderers/drawLabels/drawStateLabels.ts
@@ -0,0 +1,306 @@
+import * as d3 from "d3";
+
+import {findCell} from "utils/graphUtils";
+import {isLake, isState} from "utils/typeUtils";
+import {round, splitInTwo} from "utils/stringUtils";
+import {minmax, rn} from "utils/numberUtils";
+
+// increase step to 15 or 30 to make it faster and more horyzontal
+// decrease step to 5 to improve accuracy
+const ANGLE_STEP = 9;
+const raycast = precalculateAngles(ANGLE_STEP);
+
+const INITIAL_DISTANCE = 10;
+const DISTANCE_STEP = 15;
+const MAX_ITERATIONS = 100;
+
+export function drawStateLabels(
+ features: TPackFeatures,
+ featureIds: Uint16Array,
+ stateIds: Uint16Array,
+ states: TStates
+) {
+ /* global: findCell, graphWidth, graphHeight */
+ console.time("drawStateLabels");
+
+ const labelPaths = getLabelPaths(features, featureIds, stateIds, states);
+ drawLabelPath(stateIds, states, labelPaths);
+
+ console.timeEnd("drawStateLabels");
+}
+
+function getLabelPaths(features: TPackFeatures, featureIds: Uint16Array, stateIds: Uint16Array, states: TStates) {
+ const labelPaths: [number, TPoints][] = [];
+
+ for (const state of states) {
+ if (!isState(state)) continue;
+
+ const offset = getOffsetWidth(state.cells);
+ const maxLakeSize = state.cells / 50;
+ const [x0, y0] = state.pole;
+
+ const offsetPoints = new Map(
+ (offset ? raycast : []).map(({angle, x: x1, y: y1}) => {
+ const [x, y] = [x0 + offset * x1, y0 + offset * y1];
+ return [angle, {x, y}];
+ })
+ );
+
+ const distances = raycast.map(({angle, x: dx, y: dy, modifier}) => {
+ let distanceMin: number;
+
+ const distance1 = getMaxDistance(state.i, {x: x0, y: y0}, dx, dy, maxLakeSize);
+
+ if (offset) {
+ const point2 = offsetPoints.get(angle - 90 < 0 ? angle + 270 : angle - 90)!;
+ const distance2 = getMaxDistance(state.i, point2, dx, dy, maxLakeSize);
+
+ const point3 = offsetPoints.get(angle + 90 >= 360 ? angle - 270 : angle + 90)!;
+ const distance3 = getMaxDistance(state.i, point3, dx, dy, maxLakeSize);
+
+ distanceMin = Math.min(distance1, distance2, distance3);
+ } else {
+ distanceMin = distance1;
+ }
+
+ const [x, y] = [x0 + distanceMin * dx, y0 + distanceMin * dy];
+ return {angle, distance: distanceMin * modifier, x, y};
+ });
+
+ const {
+ angle,
+ x: x1,
+ y: y1
+ } = distances.reduce(
+ (acc, {angle, distance, x, y}) => {
+ if (distance > acc.distance) return {angle, distance, x, y};
+ return acc;
+ },
+ {angle: 0, distance: 0, x: 0, y: 0}
+ );
+
+ const oppositeAngle = angle >= 180 ? angle - 180 : angle + 180;
+ const {x: x2, y: y2} = distances.reduce(
+ (acc, {angle, distance, x, y}) => {
+ const angleDif = getAnglesDif(angle, oppositeAngle);
+ const score = distance * getAngleModifier(angleDif);
+ if (score > acc.score) return {angle, score, x, y};
+ return acc;
+ },
+ {angle: 0, score: 0, x: 0, y: 0}
+ );
+
+ const pathPoints: TPoints = [[x1, y1], state.pole, [x2, y2]];
+ if (x1 > x2) pathPoints.reverse();
+ labelPaths.push([state.i, pathPoints]);
+ }
+
+ return labelPaths;
+
+ function getMaxDistance(stateId: number, point: {x: number; y: number}, dx: number, dy: number, maxLakeSize: number) {
+ let distance = INITIAL_DISTANCE;
+
+ for (let i = 0; i < MAX_ITERATIONS; i++) {
+ const [x, y] = [point.x + distance * dx, point.y + distance * dy];
+ const cellId = findCell(x, y, DISTANCE_STEP);
+
+ // drawPoint([x, y], {color: cellId && isPassable(cellId) ? "blue" : "red", radius: 0.8});
+
+ if (!cellId || !isPassable(cellId)) break;
+ distance += DISTANCE_STEP;
+ }
+
+ return distance;
+
+ function isPassable(cellId: number) {
+ const feature = features[featureIds[cellId]];
+ if (isLake(feature) && feature.cells <= maxLakeSize) return true;
+ return stateIds[cellId] === stateId;
+ }
+ }
+}
+
+function drawLabelPath(stateIds: Uint16Array, states: TStates, labelPaths: [number, TPoints][]) {
+ const mode = options.stateLabelsMode || "auto";
+ const lineGen = d3.line().curve(d3.curveBundle.beta(1));
+
+ const textGroup = d3.select("g#labels > g#states");
+ const pathGroup = d3.select("defs > g#deftemp > g#textPaths");
+
+ const testLabel = textGroup.append("text").attr("x", 0).attr("x", 0).text("Example");
+ const letterLength = testLabel.node()!.getComputedTextLength() / 7; // approximate length of 1 letter
+ testLabel.remove();
+
+ for (const [stateId, pathPoints] of labelPaths) {
+ const state = states[stateId];
+ if (!isState(state)) throw new Error("State must not be neutral");
+ if (pathPoints.length < 2) throw new Error("Label path must have at least 2 points");
+
+ textGroup.select("#textPath_stateLabel" + stateId).remove();
+ pathGroup.select("#stateLabel" + stateId).remove();
+
+ const textPath = pathGroup
+ .append("path")
+ .attr("d", round(lineGen(pathPoints)!))
+ .attr("id", "textPath_stateLabel" + stateId);
+
+ // drawPath(round(lineGen(pathPoints)!), {stroke: "red", strokeWidth: 1});
+
+ const pathLength = textPath.node()!.getTotalLength() / letterLength; // path length in letters
+ const [lines, ratio] = getLinesAndRatio(mode, state.name, state.fullName, pathLength);
+
+ // prolongate path if it's too short
+ const longestLineLength = d3.max(lines.map(({length}) => length))!;
+ if (pathLength && pathLength < longestLineLength) {
+ const [x1, y1] = pathPoints.at(0)!;
+ const [x2, y2] = pathPoints.at(-1)!;
+ const [dx, dy] = [(x2 - x1) / 2, (y2 - y1) / 2];
+
+ const mod = longestLineLength / pathLength;
+ pathPoints[0] = [x1 + dx - dx * mod, y1 + dy - dy * mod];
+ pathPoints[pathPoints.length - 1] = [x2 - dx + dx * mod, y2 - dy + dy * mod];
+
+ textPath.attr("d", round(lineGen(pathPoints)!));
+ // drawPath(round(lineGen(pathPoints)!), {stroke: "blue", strokeWidth: 0.4});
+ }
+
+ const textElement = textGroup
+ .append("text")
+ .attr("id", "stateLabel" + stateId)
+ .append("textPath")
+ .attr("startOffset", "50%")
+ .attr("font-size", ratio + "%")
+ .node()!;
+
+ const top = (lines.length - 1) / -2; // y offset
+ const spans = lines.map((line, index) => `
${line}`);
+ textElement.insertAdjacentHTML("afterbegin", spans.join(""));
+
+ const {width, height} = textElement.getBBox();
+ textElement.setAttribute("href", "#textPath_stateLabel" + stateId);
+
+ if (mode === "full" || lines.length === 1) continue;
+
+ // check if label fits state boundaries. If no, replace it with short name
+ const [[x1, y1], [x2, y2]] = [pathPoints.at(0)!, pathPoints.at(-1)!];
+ const angleRad = Math.atan2(y2 - y1, x2 - x1);
+
+ const isInsideState = checkIfInsideState(textElement, angleRad, width / 2, height / 2, stateIds, stateId);
+ if (isInsideState) continue;
+
+ // replace name to one-liner
+ const text = pathLength > state.fullName.length * 1.8 ? state.fullName : state.name;
+ textElement.innerHTML = `
${text}`;
+
+ const correctedRatio = minmax(rn((pathLength / text.length) * 50), 40, 130);
+ textElement.setAttribute("font-size", correctedRatio + "%");
+ // textElement.setAttribute("fill", "blue");
+ }
+}
+
+// point offset to reduce label overlap with state borders
+function getOffsetWidth(cellsNumber: number) {
+ if (cellsNumber < 80) return 0;
+ if (cellsNumber < 140) return 5;
+ if (cellsNumber < 200) return 15;
+ if (cellsNumber < 300) return 20;
+ if (cellsNumber < 500) return 25;
+ return 30;
+}
+
+// difference between two angles in range [0, 180]
+function getAnglesDif(angle1: number, angle2: number) {
+ return 180 - Math.abs(Math.abs(angle1 - angle2) - 180);
+}
+
+// score multiplier based on angle difference betwee left and right sides
+function getAngleModifier(angleDif: number) {
+ if (angleDif === 0) return 1;
+ if (angleDif <= 15) return 0.95;
+ if (angleDif <= 30) return 0.9;
+ if (angleDif <= 45) return 0.6;
+ if (angleDif <= 60) return 0.3;
+ if (angleDif <= 90) return 0.1;
+ return 0; // >90
+}
+
+function precalculateAngles(step: number) {
+ const angles = [];
+ const RAD = Math.PI / 180;
+
+ for (let angle = 0; angle < 360; angle += step) {
+ const x = Math.cos(angle * RAD);
+ const y = Math.sin(angle * RAD);
+ const angleDif = 90 - Math.abs((angle % 180) - 90);
+ const modifier = 1 - angleDif / 120; // [0.25, 1]
+ angles.push({angle, modifier, x, y});
+ }
+
+ return angles;
+}
+
+function getLinesAndRatio(
+ mode: "auto" | "short" | "full",
+ name: string,
+ fullName: string,
+ pathLength: number
+): [string[], number] {
+ // short name
+ if (mode === "short" || (mode === "auto" && pathLength <= name.length)) {
+ const lines = splitInTwo(name);
+ const longestLineLength = d3.max(lines.map(({length}) => length))!;
+ const ratio = pathLength / longestLineLength;
+ return [lines, minmax(rn(ratio * 60), 50, 150)];
+ }
+
+ // full name: one line
+ if (pathLength > fullName.length * 2) {
+ const lines = [fullName];
+ const ratio = pathLength / lines[0].length;
+ return [lines, minmax(rn(ratio * 70), 70, 170)];
+ }
+
+ // full name: two lines
+ const lines = splitInTwo(fullName);
+ const longestLineLength = d3.max(lines.map(({length}) => length))!;
+ const ratio = pathLength / longestLineLength;
+ return [lines, minmax(rn(ratio * 60), 70, 150)];
+}
+
+// check whether multi-lined label is mostly inside the state. If no, replace it with short name label
+function checkIfInsideState(
+ textElement: SVGTextPathElement,
+ angleRad: number,
+ halfwidth: number,
+ halfheight: number,
+ stateIds: Uint16Array,
+ stateId: number
+) {
+ const bbox = textElement.getBBox();
+ const [cx, cy] = [bbox.x + bbox.width / 2, bbox.y + bbox.height / 2];
+
+ const points: TPoints = [
+ [-halfwidth, -halfheight],
+ [+halfwidth, -halfheight],
+ [+halfwidth, halfheight],
+ [-halfwidth, halfheight],
+ [0, halfheight],
+ [0, -halfheight]
+ ];
+
+ const sin = Math.sin(angleRad);
+ const cos = Math.cos(angleRad);
+ const rotatedPoints: TPoints = points.map(([x, y]) => [cx + x * cos - y * sin, cy + x * sin + y * cos]);
+
+ // drawPolyline([...rotatedPoints.slice(0, 4), rotatedPoints[0]], {stroke: "#333"});
+
+ let pointsInside = 0;
+ for (const [x, y] of rotatedPoints) {
+ const isInside = stateIds[findCell(x, y)] === stateId;
+ if (isInside) pointsInside++;
+ // drawPoint([x, y], {color: isInside ? "green" : "red"});
+ if (pointsInside > 4) return true;
+ }
+
+ return false;
+}
diff --git a/src/layers/renderers/drawLabels/index.ts b/src/layers/renderers/drawLabels/index.ts
new file mode 100644
index 00000000..85fe8176
--- /dev/null
+++ b/src/layers/renderers/drawLabels/index.ts
@@ -0,0 +1,12 @@
+import {drawBurgLabels} from "./drawBurgLabels";
+import {drawStateLabels} from "./drawStateLabels";
+
+export function drawLabels() {
+ /* global */ const {cells, features, states, burgs} = pack;
+
+ drawStateLabels(features, cells.f, cells.state, states);
+ drawBurgLabels(burgs);
+ // TODO: draw other labels
+
+ window.Zoom.invoke();
+}
diff --git a/src/layers/renderers/drawProvinces.js b/src/layers/renderers/drawProvinces.js
deleted file mode 100644
index 10b4de42..00000000
--- a/src/layers/renderers/drawProvinces.js
+++ /dev/null
@@ -1,122 +0,0 @@
-import polylabel from "polylabel";
-
-export function drawProvinces() {
- const labelsOn = provs.attr("data-labels") == 1;
- provs.selectAll("*").remove();
-
- const provinces = pack.provinces;
- const {body, gap} = getProvincesVertices();
-
- 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 = 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);
-}
-
-export 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;
- }
-}
diff --git a/src/layers/renderers/drawProvinces.ts b/src/layers/renderers/drawProvinces.ts
new file mode 100644
index 00000000..f6c884fc
--- /dev/null
+++ b/src/layers/renderers/drawProvinces.ts
@@ -0,0 +1,45 @@
+import {pick} from "utils/functionUtils";
+import {byId} from "utils/shorthands";
+import {isProvince} from "utils/typeUtils";
+import {getPaths} from "./utils/getVertexPaths";
+
+export function drawProvinces() {
+ /* global */ const {cells, vertices, features, provinces} = pack;
+
+ const paths = getPaths({
+ getType: (cellId: number) => cells.province[cellId],
+ cells: pick(cells, "c", "v", "b", "h", "f"),
+ vertices,
+ features,
+ options: {fill: true, waterGap: true, halo: false}
+ });
+
+ const getColor = (i: string) => (provinces[Number(i)] as IProvince).color;
+
+ const getLabels = () => {
+ const renderLabels = byId("provs")!.getAttribute("data-labels") === "1";
+ if (!renderLabels) return [];
+
+ return provinces.filter(isProvince).map(({i, pole: [x, y], name}) => {
+ return `
${name}`;
+ });
+ };
+
+ const htmlPaths = paths.map(([index, {fill, waterGap}]) => {
+ const color = getColor(index);
+
+ return /* html */ `
+
+
+ `;
+ });
+
+ byId("provs")!.innerHTML = /* html*/ `
+
+ ${htmlPaths.join("")}
+
+
+ ${getLabels().join("")}
+
+ `;
+}
diff --git a/src/modules/burgs-and-states.js b/src/modules/burgs-and-states.js
index 190322ea..a65061da 100644
--- a/src/modules/burgs-and-states.js
+++ b/src/modules/burgs-and-states.js
@@ -833,7 +833,7 @@ window.BurgsAndStates = (function () {
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)); // average state area
// generic relations
for (let f = 1; f < states.length; f++) {
diff --git a/src/modules/ui/namesbase-editor.js b/src/modules/ui/namesbase-editor.js
index 3638aa6d..d37cccf2 100644
--- a/src/modules/ui/namesbase-editor.js
+++ b/src/modules/ui/namesbase-editor.js
@@ -75,10 +75,8 @@ export function editNamesbase() {
function updateInputs() {
const base = +document.getElementById("namesbaseSelect").value;
- if (!nameBases[base]) {
- tip(`Namesbase ${base} is not defined`, false, "error");
- return;
- }
+ if (!nameBases[base]) return tip(`Namesbase ${base} is not defined`, false, "error");
+
document.getElementById("namesbaseTextarea").value = nameBases[base].b;
document.getElementById("namesbaseName").value = nameBases[base].name;
document.getElementById("namesbaseMin").value = nameBases[base].min;
@@ -104,20 +102,23 @@ export function editNamesbase() {
function updateNamesData() {
const base = +document.getElementById("namesbaseSelect").value;
- const b = document.getElementById("namesbaseTextarea").value;
- if (b.split(",").length < 3) {
- tip("The names data provided is too short of incorrect", false, "error");
- return;
- }
- nameBases[base].b = b;
+ const rawInput = document.getElementById("namesbaseTextarea").value;
+ if (rawInput.split(",").length < 3) return tip("The names data provided is too short of incorrect", false, "error");
+
+ const namesData = rawInput.replace(/[/|]/g, "");
+ nameBases[base].b = namesData;
Names.updateChain(base);
}
function updateBaseName() {
const base = +document.getElementById("namesbaseSelect").value;
const select = document.getElementById("namesbaseSelect");
- select.options[namesbaseSelect.selectedIndex].innerHTML = this.value;
- nameBases[base].name = this.value;
+
+ const rawName = this.value;
+ const name = rawName.replace(/[/|]/g, "");
+
+ select.options[namesbaseSelect.selectedIndex].innerHTML = name;
+ nameBases[base].name = name;
}
function updateBaseMin() {
diff --git a/src/modules/ui/options.js b/src/modules/ui/options.js
index fcb5dd6c..e3262bf0 100644
--- a/src/modules/ui/options.js
+++ b/src/modules/ui/options.js
@@ -485,10 +485,17 @@ function changeDialogsTheme(themeColor, transparency) {
}
function changeZoomExtent(value) {
- const min = Math.max(+byId("zoomExtentMin").value, 0.01);
- const max = Math.min(+byId("zoomExtentMax").value, 200);
- Zoom.scaleExtent([min, max]);
+ const zoomExtentMin = byId("zoomExtentMin");
+ const zoomExtentMax = byId("zoomExtentMax");
+ if (+zoomExtentMin.value > +zoomExtentMax.value) {
+ [zoomExtentMin.value, zoomExtentMax.value] = [zoomExtentMax.value, zoomExtentMin.value];
+ }
+ const min = Math.max(+zoomExtentMin.value, 0.01);
+ const max = Math.min(+zoomExtentMax.value, 200);
+ zoomExtentMin.value = min;
+ zoomExtentMax.value = max;
+ zoom.scaleExtent([min, max]);
const scale = minmax(+value, 0.01, 200);
Zoom.scaleTo(svg, scale);
}
@@ -1056,6 +1063,7 @@ export function toggle3dOptions() {
const globe = byId("canvas3d").dataset.type === "viewGlobe";
options3dMesh.style.display = globe ? "none" : "block";
options3dGlobe.style.display = globe ? "block" : "none";
+ options3dOBJSave.style.display = globe ? "none" : "inline-block";
options3dScaleRange.value = options3dScaleNumber.value = ThreeD.options.scale;
options3dLightnessRange.value = options3dLightnessNumber.value = ThreeD.options.lightness * 100;
options3dSunX.value = ThreeD.options.sun.x;
diff --git a/src/scripts/generation/generation.ts b/src/scripts/generation/generation.ts
index 5d90ff9a..65fb65dd 100644
--- a/src/scripts/generation/generation.ts
+++ b/src/scripts/generation/generation.ts
@@ -67,10 +67,10 @@ async function generate(options?: IGenerationOptions) {
// renderLayer("heightmap");
// renderLayer("rivers");
// renderLayer("biomes");
- renderLayer("burgs");
- renderLayer("routes");
+ // renderLayer("burgs");
+ // renderLayer("routes");
renderLayer("states");
- // renderLayer("religions");
+ renderLayer("labels");
// pack.cells.route.forEach((route, index) => {
// if (route === 2) drawPoint(pack.cells.p[index], {color: "black"});
diff --git a/src/scripts/generation/pack/burgsAndStates/config.ts b/src/scripts/generation/pack/burgsAndStates/config.ts
index 8d038625..6c660f39 100644
--- a/src/scripts/generation/pack/burgsAndStates/config.ts
+++ b/src/scripts/generation/pack/burgsAndStates/config.ts
@@ -97,12 +97,15 @@ export const adjectivalForms = [
"Theocracy",
"Oligarchy",
"Union",
+ "Federation",
"Confederation",
"Trade Company",
"League",
"Tetrarchy",
"Triumvirate",
"Diarchy",
+ "Khanate",
+ "Khaganate",
"Horde",
"Marches"
];
diff --git a/src/scripts/generation/pack/burgsAndStates/defineStateForm.ts b/src/scripts/generation/pack/burgsAndStates/defineStateForm.ts
index 0610edd0..a434434e 100644
--- a/src/scripts/generation/pack/burgsAndStates/defineStateForm.ts
+++ b/src/scripts/generation/pack/burgsAndStates/defineStateForm.ts
@@ -40,7 +40,7 @@ export function defineStateForm(
const generic = {Monarchy: 25, Republic: 2, Union: 1};
const naval = {Monarchy: 6, Republic: 2, Union: 1};
-function defineForm(type: TCultureType, areaTier: AreaTiers) {
+function defineForm(type: TCultureType, areaTier: AreaTiers): TStateForm {
const isAnarchy = P((1 - areaTier / 5) / 100); // [1% - 0.2%] chance
if (isAnarchy) return "Anarchy";
diff --git a/src/scripts/generation/pack/burgsAndStates/expandStates.ts b/src/scripts/generation/pack/burgsAndStates/expandStates.ts
index 64bdc3aa..c1f40925 100644
--- a/src/scripts/generation/pack/burgsAndStates/expandStates.ts
+++ b/src/scripts/generation/pack/burgsAndStates/expandStates.ts
@@ -6,6 +6,42 @@ import {minmax} from "utils/numberUtils";
import {ELEVATION, FOREST_BIOMES, MIN_LAND_HEIGHT, DISTANCE_FIELD} from "config/generation";
import type {TStateData} from "./createStateData";
+const costs = {
+ SAME_CULTURE: -9,
+ DIFFERENT_CULTURES: 100,
+
+ MAX_SUITABILITY: 20,
+ UNINHABITED_LAND: 5000,
+ NATIVE_BIOME_FIXED: 10,
+
+ GENERIC_WATER_CROSSING: 1000,
+ NOMADS_WATER_CROSSING: 10000,
+ NAVAL_WATER_CROSSING: 300,
+ LAKE_STATES_LAKE_CROSSING: 10,
+ GENERIC_MOUNTAINS_CROSSING: 2200,
+ GENERIC_HILLS_CROSSING: 300,
+ HIGHLAND_STATE_LOWLANDS: 1100,
+ HIGHLAND_STATE_HIGHTLAND: 0,
+
+ RIVER_STATE_RIVER_CROSSING: 0,
+ RIVER_STATE_NO_RIVER: 100,
+ RIVER_CROSSING_MIN: 20,
+ RIVER_CROSSING_MAX: 100,
+
+ GENERIC_LAND_COAST: 20,
+ MARITIME_LAND_COAST: 0,
+ NOMADS_LAND_COAST: 60,
+ GENERIC_LANDLOCKED: 0,
+ NAVAL_LANDLOCKED: 30
+};
+
+const multipliers = {
+ HUNTERS_NON_NATIVE_BIOME: 2,
+ NOMADS_FOREST_BIOMES: 3,
+ GENERIC_NON_NATIVE_BIOME: 1,
+ GENERIC_DEEP_WATER: 2
+};
+
// growth algorithm to assign cells to states
export function expandStates(
capitalCells: Map
,
@@ -30,39 +66,6 @@ export function expandStates(
queue.push({cellId, stateId}, 0);
}
- // expansion costs (less is better)
- const SAME_CULTURE_BONUS = -9;
- const DIFFERENT_CULTURES_FEE = 100;
-
- const MAX_SUITABILITY_COST = 20;
- const UNINHABITED_LAND_FEE = 5000;
-
- const NATIVE_BIOME_FIXED_COST = 10;
- const HUNTERS_NON_NATIVE_BIOME_FEE_MULTIPLIER = 2;
- const NOMADS_FOREST_BIOMES_FEE_MULTIPLIER = 3;
- const GENERIC_NON_NATIVE_BIOME_FEE_MULTIPLIER = 1;
-
- const GENERIC_DEEP_WATER_FEE_MULTIPLIER = 2;
- const GENERIC_WATER_CROSSING_FEE = 1000;
- const NOMADS_WATER_CROSSING_FEE = 10000;
- const NAVAL_WATER_CROSSING_FEE = 300;
- const LAKE_STATES_LAKE_CROSSING_FEE = 10;
- const GENERIC_MOUNTAINS_CROSSING_FEE = 2200;
- const GENERIC_HILLS_CROSSING_FEE = 300;
- const HIGHLAND_STATE_LOWLANDS_FEE = 1100;
- const HIGHLAND_STATE_HIGHTLAND_COST = 0;
-
- const RIVER_STATE_RIVER_CROSSING_COST = 0;
- const RIVER_STATE_NO_RIVER_COST = 100;
- const RIVER_CROSSING_MIN_COST = 20;
- const RIVER_CROSSING_MAX_COST = 100;
-
- const GENERIC_LAND_COAST_FEE = 20;
- const MARITIME_LAND_COAST_FEE = 0;
- const NOMADS_LAND_COAST_FEE = 60;
- const GENERIC_LANDLOCKED_FEE = 0;
- const NAVAL_LANDLOCKED_FEE = 30;
-
const statesMap = new Map(statesData.map(stateData => [stateData.i, stateData]));
while (queue.length) {
@@ -100,7 +103,7 @@ export function expandStates(
return normalizeStates(stateIds, capitalCells, cells.c, cells.h);
function getCultureCost(cellId: number, stateCulture: number) {
- return cells.culture[cellId] === stateCulture ? SAME_CULTURE_BONUS : DIFFERENT_CULTURES_FEE;
+ return cells.culture[cellId] === stateCulture ? costs.SAME_CULTURE : costs.DIFFERENT_CULTURES;
}
function getPopulationCost(cellId: number) {
@@ -108,19 +111,19 @@ export function expandStates(
if (isWater) return 0;
const suitability = cells.s[cellId];
- if (suitability) return Math.max(MAX_SUITABILITY_COST - suitability, 0);
+ if (suitability) return Math.max(costs.MAX_SUITABILITY - suitability, 0);
- return UNINHABITED_LAND_FEE;
+ return costs.UNINHABITED_LAND;
}
function getBiomeCost(cellId: number, capitalBiome: number, type: TCultureType) {
const biome = cells.biome[cellId];
- if (biome === capitalBiome) return NATIVE_BIOME_FIXED_COST;
+ if (biome === capitalBiome) return costs.NATIVE_BIOME_FIXED;
const defaultCost = biomesData.cost[biome];
- if (type === "Hunting") return defaultCost * HUNTERS_NON_NATIVE_BIOME_FEE_MULTIPLIER;
- if (type === "Nomadic" && FOREST_BIOMES.includes(biome)) return defaultCost * NOMADS_FOREST_BIOMES_FEE_MULTIPLIER;
- return defaultCost * GENERIC_NON_NATIVE_BIOME_FEE_MULTIPLIER;
+ if (type === "Hunting") return defaultCost * multipliers.HUNTERS_NON_NATIVE_BIOME;
+ if (type === "Nomadic" && FOREST_BIOMES.includes(biome)) return defaultCost * multipliers.NOMADS_FOREST_BIOMES;
+ return defaultCost * multipliers.GENERIC_NON_NATIVE_BIOME;
}
function getHeightCost(cellId: number, type: TCultureType) {
@@ -131,12 +134,12 @@ export function expandStates(
const feature = features[cells.f[cellId]];
if (feature === 0) throw new Error(`No feature for cell ${cellId}`);
const isDeepWater = cells.t[cellId] > DISTANCE_FIELD.WATER_COAST;
- const multiplier = isDeepWater ? GENERIC_DEEP_WATER_FEE_MULTIPLIER : 1;
+ const multiplier = isDeepWater ? multipliers.GENERIC_DEEP_WATER : 1;
- if (type === "Lake" && feature.type === "lake") return LAKE_STATES_LAKE_CROSSING_FEE * multiplier;
- if (type === "Naval") return NAVAL_WATER_CROSSING_FEE * multiplier;
- if (type === "Nomadic") return NOMADS_WATER_CROSSING_FEE * multiplier;
- return GENERIC_WATER_CROSSING_FEE * multiplier;
+ if (type === "Lake" && feature.type === "lake") return costs.LAKE_STATES_LAKE_CROSSING * multiplier;
+ if (type === "Naval") return costs.NAVAL_WATER_CROSSING * multiplier;
+ if (type === "Nomadic") return costs.NOMADS_WATER_CROSSING * multiplier;
+ return costs.GENERIC_WATER_CROSSING * multiplier;
}
const isLowlands = height <= ELEVATION.FOOTHILLS;
@@ -144,22 +147,22 @@ export function expandStates(
const isMountains = height >= ELEVATION.MOUNTAINS;
if (type === "Highland") {
- if (isLowlands) return HIGHLAND_STATE_LOWLANDS_FEE;
- return HIGHLAND_STATE_HIGHTLAND_COST;
+ if (isLowlands) return costs.HIGHLAND_STATE_LOWLANDS;
+ return costs.HIGHLAND_STATE_HIGHTLAND;
}
- if (isMountains) return GENERIC_MOUNTAINS_CROSSING_FEE;
- if (isHills) return GENERIC_HILLS_CROSSING_FEE;
+ if (isMountains) return costs.GENERIC_MOUNTAINS_CROSSING;
+ if (isHills) return costs.GENERIC_HILLS_CROSSING;
return 0;
}
function getRiverCost(cellId: number, type: TCultureType) {
const isRiver = cells.r[cellId] !== 0;
- if (type === "River") return isRiver ? RIVER_STATE_RIVER_CROSSING_COST : RIVER_STATE_NO_RIVER_COST;
+ if (type === "River") return isRiver ? costs.RIVER_STATE_RIVER_CROSSING : costs.RIVER_STATE_NO_RIVER;
if (!isRiver) return 0;
const flux = cells.fl[cellId];
- return minmax(flux / 10, RIVER_CROSSING_MIN_COST, RIVER_CROSSING_MAX_COST);
+ return minmax(flux / 10, costs.RIVER_CROSSING_MIN, costs.RIVER_CROSSING_MAX);
}
function getTypeCost(cellId: number, type: TCultureType) {
@@ -168,15 +171,15 @@ export function expandStates(
const isLandCoast = t === DISTANCE_FIELD.LAND_COAST;
if (isLandCoast) {
- if (isMaritime) return MARITIME_LAND_COAST_FEE;
- if (type === "Nomadic") return NOMADS_LAND_COAST_FEE;
- return GENERIC_LAND_COAST_FEE;
+ if (isMaritime) return costs.MARITIME_LAND_COAST;
+ if (type === "Nomadic") return costs.NOMADS_LAND_COAST;
+ return costs.GENERIC_LAND_COAST;
}
const isLandlocked = t === DISTANCE_FIELD.LANDLOCKED;
if (isLandlocked) {
- if (type === "Naval") return NAVAL_LANDLOCKED_FEE;
- return GENERIC_LANDLOCKED_FEE;
+ if (type === "Naval") return costs.NAVAL_LANDLOCKED;
+ return costs.GENERIC_LANDLOCKED;
}
return 0;
@@ -193,7 +196,7 @@ function normalizeStates(
const normalizedStateIds = Uint16Array.from(stateIds);
- for (let cellId = 0; cellId > heights.length; cellId++) {
+ for (let cellId = 0; cellId < heights.length; cellId++) {
if (heights[cellId] < MIN_LAND_HEIGHT) continue;
const neibs = neibCells[cellId].filter(neib => heights[neib] >= MIN_LAND_HEIGHT);
diff --git a/src/scripts/generation/pack/burgsAndStates/generateBurgsAndStates.ts b/src/scripts/generation/pack/burgsAndStates/generateBurgsAndStates.ts
index d2d2dc8f..471a6bc1 100644
--- a/src/scripts/generation/pack/burgsAndStates/generateBurgsAndStates.ts
+++ b/src/scripts/generation/pack/burgsAndStates/generateBurgsAndStates.ts
@@ -1,4 +1,5 @@
import {WARN} from "config/logging";
+import {getPolesOfInaccessibility} from "scripts/getPolesOfInaccessibility";
import {pick} from "utils/functionUtils";
import {getInputNumber} from "utils/nodeUtils";
import {collectStatistics} from "./collectStatistics";
@@ -70,7 +71,13 @@ export function generateBurgsAndStates(
const statistics = collectStatistics({...cells, state: stateIds, burg: burgIds}, burgs);
const diplomacy = generateRelations(statesData, statistics, pick(cells, "f"));
- const {states, conflicts} = specifyStates(statesData, statistics, diplomacy, cultures, burgs);
+ const poles = getPolesOfInaccessibility({
+ vertices,
+ getType: (cellId: number) => stateIds[cellId],
+ cellNeighbors: cells.c,
+ cellVertices: cells.v
+ });
+ const {states, conflicts} = specifyStates(statesData, statistics, diplomacy, poles, cultures, burgs);
return {burgIds, stateIds, burgs, states, conflicts};
diff --git a/src/scripts/generation/pack/burgsAndStates/specifyBurgs.ts b/src/scripts/generation/pack/burgsAndStates/specifyBurgs.ts
index ce7b0b44..e21addff 100644
--- a/src/scripts/generation/pack/burgsAndStates/specifyBurgs.ts
+++ b/src/scripts/generation/pack/burgsAndStates/specifyBurgs.ts
@@ -32,24 +32,27 @@ export function specifyBurgs(
const burgs = [...capitals, ...towns].map((burgData, index) => {
const {cell, culture, capital} = burgData;
+ const isCapital = Boolean(capital);
const state = stateIds[cell];
- const port = definePort(cell, capital);
- const population = definePopulation(cell, capital, port);
+ const port = definePort(cell, isCapital);
+ const population = definePopulation(cell, isCapital, port);
const [x, y] = defineLocation(cell, port);
const type = defineType(cell, port, population);
const stateData = stateDataMap.get(state)!;
- const coa: ICoa = defineEmblem(culture, port, capital, type, cultures, stateData);
+ const coa: ICoa = defineEmblem(culture, port, isCapital, type, cultures, stateData);
- const burg: IBurg = {i: index + 1, ...burgData, state, port, population, x, y, type, coa};
+ const features = defineFeatures(population, isCapital);
+
+ const burg: IBurg = {i: index + 1, ...burgData, state, port, population, x, y, type, coa, ...features};
return burg;
});
TIME && console.timeEnd("specifyBurgs");
return [NO_BURG, ...burgs];
- function definePort(cellId: number, capital: Logical) {
+ function definePort(cellId: number, isCapital: boolean) {
if (!cells.haven[cellId]) return 0; // must be a coastal cell
if (temp[cells.g[cellId]] <= 0) return 0; // temperature must be above zero °C
@@ -59,17 +62,17 @@ export function specifyBurgs(
if (feature.cells < 2) return 0; // water body must have at least 2 cells
const isSafeHarbor = cells.harbor[cellId] === 1;
- if (!capital && !isSafeHarbor) return 0; // must be a capital or safe harbor
+ if (!isCapital && !isSafeHarbor) return 0; // must be a capital or safe harbor
return havenFeatureId;
}
// get population in points, where 1 point = 1000 people by default
- function definePopulation(cellId: number, capital: Logical, port: number) {
+ function definePopulation(cellId: number, isCapital: boolean, port: number) {
const basePopulation = cells.s[cellId] / 4;
const decimalPart = (cellId % 1000) / 1000;
- const capitalMultiplier = capital ? 1.3 : 1;
+ const capitalMultiplier = isCapital ? 1.3 : 1;
const portMultiplier = port ? 1.3 : 1;
const randomMultiplier = gauss(1, 1.5, 0.3, 10, 3);
@@ -123,12 +126,12 @@ export function specifyBurgs(
function defineEmblem(
cultureId: number,
port: number,
- capital: Logical,
+ isCapital: boolean,
type: TCultureType,
cultures: TCultures,
stateData: TStateData
) {
- const coaType = capital && P(0.2) ? "Capital" : type === "Generic" ? "City" : type;
+ const coaType = isCapital && P(0.2) ? "Capital" : type === "Generic" ? "City" : type;
const cultureShield = cultures[cultureId].shield;
if (!stateData) {
@@ -147,11 +150,29 @@ export function specifyBurgs(
function defineKinshipToStateEmblem() {
const baseKinship = 0.25;
- const capitalModifier = capital ? 0.1 : 0;
+ const capitalModifier = isCapital ? 0.1 : 0;
const portModifier = port ? -0.1 : 0;
const cultureModifier = cultureId === stateCultureId ? 0 : -0.25;
return baseKinship + capitalModifier + portModifier + cultureModifier;
}
}
+
+ // burg features used mainly in MFCG
+ function defineFeatures(population: number, isCapital: boolean) {
+ const citadel: Logical = isCapital || (population > 50 && P(0.75)) || P(0.5) ? 1 : 0;
+
+ const plaza: Logical =
+ population > 50 || (population > 30 && P(0.75)) || (population > 10 && P(0.5)) || P(0.25) ? 1 : 0;
+
+ const walls: Logical =
+ isCapital || population > 30 || (population > 20 && P(0.75)) || (population > 10 && P(0.5)) || P(0.2) ? 1 : 0;
+
+ const shanty: Logical =
+ population > 60 || (population > 40 && P(0.75)) || (population > 20 && walls && P(0.4)) ? 1 : 0;
+
+ const temple: Logical = population > 50 || (population > 35 && P(0.75)) || (population > 20 && P(0.5)) ? 1 : 0;
+
+ return {citadel, plaza, walls, shanty, temple};
+ }
}
diff --git a/src/scripts/generation/pack/burgsAndStates/specifyStates.ts b/src/scripts/generation/pack/burgsAndStates/specifyStates.ts
index ba9be9ee..3edbe3c2 100644
--- a/src/scripts/generation/pack/burgsAndStates/specifyStates.ts
+++ b/src/scripts/generation/pack/burgsAndStates/specifyStates.ts
@@ -14,6 +14,7 @@ export function specifyStates(
statesData: TStateData[],
statistics: TStateStatistics,
diplomacy: TDiplomacy,
+ poles: Dict,
cultures: TCultures,
burgs: TBurgs
): {states: TStates; conflicts: IConflict[]} {
@@ -41,6 +42,8 @@ export function specifyStates(
const name = defineStateName(center, capitalName, nameBase, formName);
const fullName = defineFullStateName(name, formName);
+ const pole = poles[i];
+
return {
name,
...stateData,
@@ -52,7 +55,8 @@ export function specifyStates(
burgs: burgsNumber,
...stats,
neighbors,
- relations
+ relations,
+ pole
};
});
diff --git a/src/scripts/generation/pack/cultures/expandCultures.ts b/src/scripts/generation/pack/cultures/expandCultures.ts
new file mode 100644
index 00000000..e93e938b
--- /dev/null
+++ b/src/scripts/generation/pack/cultures/expandCultures.ts
@@ -0,0 +1,106 @@
+import FlatQueue from "flatqueue";
+
+import {DISTANCE_FIELD, ELEVATION, FOREST_BIOMES, MIN_LAND_HEIGHT} from "config/generation";
+import {TIME} from "config/logging";
+import {getInputNumber} from "utils/nodeUtils";
+import {minmax} from "utils/numberUtils";
+import {isCulture} from "utils/typeUtils";
+
+const {LAND_COAST, LANDLOCKED, WATER_COAST} = DISTANCE_FIELD;
+const {MOUNTAINS, HILLS} = ELEVATION;
+
+// expand cultures across the map (Dijkstra-like algorithm)
+export function expandCultures(
+ cultures: TCultures,
+ features: TPackFeatures,
+ cells: Pick
+) {
+ TIME && console.time("expandCultures");
+
+ const cultureIds = new Uint16Array(cells.h.length); // cell cultures
+ const queue = new FlatQueue<{cellId: number; cultureId: number}>();
+
+ cultures.filter(isCulture).forEach(culture => {
+ queue.push({cellId: culture.center, cultureId: culture.i}, 0);
+ });
+
+ const cellsNumberFactor = cells.h.length / 1.6;
+ const maxExpansionCost = cellsNumberFactor * getInputNumber("neutralInput"); // limit cost for culture growth
+ const cost: number[] = [];
+
+ while (queue.length) {
+ const priority = queue.peekValue()!;
+ const {cellId, cultureId} = queue.pop()!;
+
+ const {type, expansionism, center} = getCulture(cultureId);
+ const cultureBiome = cells.biome[center];
+
+ cells.c[cellId].forEach(neibCellId => {
+ const biomeCost = getBiomeCost(neibCellId, cultureBiome, type);
+ const heightCost = getHeightCost(neibCellId, cells.h[neibCellId], type);
+ const riverCost = getRiverCost(cells.r[neibCellId], neibCellId, type);
+ const typeCost = getTypeCost(cells.t[neibCellId], type);
+
+ const totalCost = priority + (biomeCost + heightCost + riverCost + typeCost) / expansionism;
+ if (totalCost > maxExpansionCost) return;
+
+ if (!cost[neibCellId] || totalCost < cost[neibCellId]) {
+ if (cells.pop[neibCellId] > 0) cultureIds[neibCellId] = cultureId; // assign culture to populated cell
+ cost[neibCellId] = totalCost;
+ queue.push({cellId: neibCellId, cultureId}, totalCost);
+ }
+ });
+ }
+
+ TIME && console.timeEnd("expandCultures");
+ return cultureIds;
+
+ function getCulture(cultureId: number) {
+ const culture = cultures[cultureId];
+ if (!isCulture(culture)) throw new Error("Wilderness cannot expand");
+ return culture;
+ }
+
+ function getBiomeCost(cellId: number, cultureBiome: number, type: TCultureType) {
+ const biome = cells.biome[cellId];
+ if (cultureBiome === biome) return 10; // tiny penalty for native biome
+ if (type === "Hunting") return biomesData.cost[biome] * 5; // non-native biome penalty for hunters
+ if (type === "Nomadic" && FOREST_BIOMES.includes(biome)) return biomesData.cost[biome] * 10; // forest biome penalty for nomads
+ return biomesData.cost[biome] * 2; // general non-native biome penalty
+ }
+
+ function getHeightCost(cellId: number, height: number, type: TCultureType) {
+ if (height < MIN_LAND_HEIGHT) {
+ const feature = features[cells.f[cellId]];
+ const area = cells.area[cellId];
+
+ if (type === "Lake" && feature && feature.type === "lake") return 10; // almost lake crossing penalty for Lake cultures
+ if (type === "Naval") return area * 2; // low sea or lake crossing penalty for Naval cultures
+ if (type === "Nomadic") return area * 50; // giant sea or lake crossing penalty for Nomads
+ return area * 6; // general sea or lake crossing penalty
+ }
+
+ if (type === "Highland") {
+ if (height >= MOUNTAINS) return 0; // no penalty for highlanders on highlands
+ if (height < HILLS) return 3000; // giant penalty for highlanders on lowlands
+ return 100; // penalty for highlanders on hills
+ }
+
+ if (height >= MOUNTAINS) return 200; // general mountains crossing penalty
+ if (height >= HILLS) return 30; // general hills crossing penalty
+ return 0;
+ }
+
+ function getRiverCost(riverId: number, cellId: number, type: TCultureType) {
+ if (type === "River") return riverId ? 0 : 100; // penalty for river cultures
+ if (!riverId) return 0; // no penalty for others if there is no river
+ return minmax(cells.fl[cellId] / 10, 20, 100); // river penalty from 20 to 100 based on flux
+ }
+
+ function getTypeCost(t: number, type: TCultureType) {
+ if (t === LAND_COAST) return type === "Naval" || type === "Lake" ? 0 : type === "Nomadic" ? 60 : 20; // penalty for coastline
+ if (t === LANDLOCKED) return type === "Naval" || type === "Nomadic" ? 30 : 0; // low penalty for land level 2 for Navals and nomads
+ if (t !== WATER_COAST) return type === "Naval" || type === "Lake" ? 100 : 0; // penalty for mainland for navals
+ return 0;
+ }
+}
diff --git a/src/scripts/generation/pack/cultures.ts b/src/scripts/generation/pack/cultures/generateCultures.ts
similarity index 63%
rename from src/scripts/generation/pack/cultures.ts
rename to src/scripts/generation/pack/cultures/generateCultures.ts
index bf598782..198da7b2 100644
--- a/src/scripts/generation/pack/cultures.ts
+++ b/src/scripts/generation/pack/cultures/generateCultures.ts
@@ -1,24 +1,15 @@
import * as d3 from "d3";
-import FlatQueue from "flatqueue";
import {cultureSets, DEFAULT_SORT_STRING, TCultureSetName} from "config/cultureSets";
-import {
- DISTANCE_FIELD,
- ELEVATION,
- FOREST_BIOMES,
- HUNTING_BIOMES,
- MIN_LAND_HEIGHT,
- NOMADIC_BIOMES
-} from "config/generation";
+import {DISTANCE_FIELD, ELEVATION, HUNTING_BIOMES, NOMADIC_BIOMES} from "config/generation";
import {ERROR, TIME, WARN} from "config/logging";
import {getColors} from "utils/colorUtils";
import {abbreviate} from "utils/languageUtils";
import {getInputNumber, getInputValue, getSelectedOption} from "utils/nodeUtils";
-import {minmax, rn} from "utils/numberUtils";
+import {rn} from "utils/numberUtils";
import {biased, P, rand} from "utils/probabilityUtils";
import {byId} from "utils/shorthands";
import {defaultNameBases} from "config/namebases";
-import {isCulture} from "utils/typeUtils";
const {COA} = window;
@@ -33,19 +24,17 @@ const cultureTypeBaseExpansionism: {[key in TCultureType]: number} = {
};
const {MOUNTAINS, HILLS} = ELEVATION;
-const {LAND_COAST, LANDLOCKED, WATER_COAST} = DISTANCE_FIELD;
+const {LAND_COAST, LANDLOCKED} = DISTANCE_FIELD;
-export const generateCultures = function (
- features: TPackFeatures,
- cells: Pick<
- IPack["cells"],
- "p" | "i" | "g" | "t" | "h" | "haven" | "harbor" | "f" | "r" | "fl" | "s" | "pop" | "biome"
- >,
- temp: Int8Array
-): TCultures {
+type TCellsData = Pick<
+ IPack["cells"],
+ "p" | "i" | "g" | "t" | "h" | "haven" | "harbor" | "f" | "r" | "fl" | "s" | "pop" | "biome"
+>;
+
+export function generateCultures(features: TPackFeatures, cells: TCellsData, temp: Int8Array): TCultures {
TIME && console.time("generateCultures");
- const wildlands: TWilderness = {name: "Wildlands", i: 0, base: 1, origins: [null], shield: "round"};
+ const wildlands: TWilderness = {i: 0, name: "Wildlands", base: 1, origins: [null], shield: "round"};
const populatedCellIds = cells.i.filter(cellId => cells.pop[cellId] > 0);
const maxSuitability = d3.max(cells.s)!;
@@ -272,100 +261,4 @@ export const generateCultures = function (
ERROR && console.error(`Name base ${base} is not available, applying a fallback one`);
return base % nameBases.length;
}
-};
-
-// expand cultures across the map (Dijkstra-like algorithm)
-export const expandCultures = function (
- cultures: TCultures,
- features: TPackFeatures,
- cells: Pick
-) {
- TIME && console.time("expandCultures");
-
- const cultureIds = new Uint16Array(cells.h.length); // cell cultures
- const queue = new FlatQueue<{cellId: number; cultureId: number}>();
-
- cultures.filter(isCulture).forEach(culture => {
- queue.push({cellId: culture.center, cultureId: culture.i}, 0);
- });
-
- const cellsNumberFactor = cells.h.length / 1.6;
- const maxExpansionCost = cellsNumberFactor * getInputNumber("neutralInput"); // limit cost for culture growth
- const cost: number[] = [];
-
- while (queue.length) {
- const priority = queue.peekValue()!;
- const {cellId, cultureId} = queue.pop()!;
-
- const {type, expansionism, center} = getCulture(cultureId);
- const cultureBiome = cells.biome[center];
-
- cells.c[cellId].forEach(neibCellId => {
- const biomeCost = getBiomeCost(neibCellId, cultureBiome, type);
- const heightCost = getHeightCost(neibCellId, cells.h[neibCellId], type);
- const riverCost = getRiverCost(cells.r[neibCellId], neibCellId, type);
- const typeCost = getTypeCost(cells.t[neibCellId], type);
-
- const totalCost = priority + (biomeCost + heightCost + riverCost + typeCost) / expansionism;
- if (totalCost > maxExpansionCost) return;
-
- if (!cost[neibCellId] || totalCost < cost[neibCellId]) {
- if (cells.pop[neibCellId] > 0) cultureIds[neibCellId] = cultureId; // assign culture to populated cell
- cost[neibCellId] = totalCost;
- queue.push({cellId: neibCellId, cultureId}, totalCost);
- }
- });
- }
-
- TIME && console.timeEnd("expandCultures");
- return cultureIds;
-
- function getCulture(cultureId: number) {
- const culture = cultures[cultureId];
- if (!isCulture(culture)) throw new Error("Wilderness cannot expand");
- return culture;
- }
-
- function getBiomeCost(cellId: number, cultureBiome: number, type: TCultureType) {
- const biome = cells.biome[cellId];
- if (cultureBiome === biome) return 10; // tiny penalty for native biome
- if (type === "Hunting") return biomesData.cost[biome] * 5; // non-native biome penalty for hunters
- if (type === "Nomadic" && FOREST_BIOMES.includes(biome)) return biomesData.cost[biome] * 10; // forest biome penalty for nomads
- return biomesData.cost[biome] * 2; // general non-native biome penalty
- }
-
- function getHeightCost(cellId: number, height: number, type: TCultureType) {
- if (height < MIN_LAND_HEIGHT) {
- const feature = features[cells.f[cellId]];
- const area = cells.area[cellId];
-
- if (type === "Lake" && feature && feature.type === "lake") return 10; // almost lake crossing penalty for Lake cultures
- if (type === "Naval") return area * 2; // low sea or lake crossing penalty for Naval cultures
- if (type === "Nomadic") return area * 50; // giant sea or lake crossing penalty for Nomads
- return area * 6; // general sea or lake crossing penalty
- }
-
- if (type === "Highland") {
- if (height >= MOUNTAINS) return 0; // no penalty for highlanders on highlands
- if (height < HILLS) return 3000; // giant penalty for highlanders on lowlands
- return 100; // penalty for highlanders on hills
- }
-
- if (height >= MOUNTAINS) return 200; // general mountains crossing penalty
- if (height >= HILLS) return 30; // general hills crossing penalty
- return 0;
- }
-
- function getRiverCost(riverId: number, cellId: number, type: TCultureType) {
- if (type === "River") return riverId ? 0 : 100; // penalty for river cultures
- if (!riverId) return 0; // no penalty for others if there is no river
- return minmax(cells.fl[cellId] / 10, 20, 100); // river penalty from 20 to 100 based on flux
- }
-
- function getTypeCost(t: number, type: TCultureType) {
- if (t === LAND_COAST) return type === "Naval" || type === "Lake" ? 0 : type === "Nomadic" ? 60 : 20; // penalty for coastline
- if (t === LANDLOCKED) return type === "Naval" || type === "Nomadic" ? 30 : 0; // low penalty for land level 2 for Navals and nomads
- if (t !== WATER_COAST) return type === "Naval" || type === "Lake" ? 100 : 0; // penalty for mainland for navals
- return 0;
- }
-};
+}
diff --git a/src/scripts/generation/pack/lakes.ts b/src/scripts/generation/pack/lakes/lakes.ts
similarity index 94%
rename from src/scripts/generation/pack/lakes.ts
rename to src/scripts/generation/pack/lakes/lakes.ts
index 367ad85c..869eb402 100644
--- a/src/scripts/generation/pack/lakes.ts
+++ b/src/scripts/generation/pack/lakes/lakes.ts
@@ -14,7 +14,7 @@ export interface ILakeClimateData extends IPackFeatureLake {
enteringFlux?: number;
}
-export const getClimateData = function (
+export function getClimateData(
lakes: IPackFeatureLake[],
heights: Float32Array,
drainableLakes: Dict,
@@ -44,13 +44,9 @@ export const getClimateData = function (
});
return lakeData;
-};
+}
-export const mergeLakeData = function (
- features: TPackFeatures,
- lakeData: ILakeClimateData[],
- rivers: Pick[]
-) {
+export function mergeLakeData(features: TPackFeatures, lakeData: ILakeClimateData[], rivers: Pick[]) {
const updatedFeatures = features.map(feature => {
if (!feature) return 0;
if (feature.type !== "lake") return feature;
@@ -71,7 +67,7 @@ export const mergeLakeData = function (
});
return updatedFeatures as TPackFeatures;
-};
+}
function defineLakeGroup({
firstCell,
diff --git a/src/scripts/generation/pack/pack.ts b/src/scripts/generation/pack/pack.ts
index df1969ef..14d9568d 100644
--- a/src/scripts/generation/pack/pack.ts
+++ b/src/scripts/generation/pack/pack.ts
@@ -1,21 +1,15 @@
-import * as d3 from "d3";
-
-import {UINT16_MAX} from "config/constants";
-import {DISTANCE_FIELD, MIN_LAND_HEIGHT} from "config/generation";
-import {TIME} from "config/logging";
-import {calculateVoronoi} from "scripts/generation/graph";
import {markupPackFeatures} from "scripts/generation/markup";
import {rankCells} from "scripts/generation/pack/rankCells";
-import {createTypedArray} from "utils/arrayUtils";
import {pick} from "utils/functionUtils";
-import {rn} from "utils/numberUtils";
-import {generateCultures, expandCultures} from "./cultures";
-import {generateRivers} from "./rivers";
import {generateBurgsAndStates} from "./burgsAndStates/generateBurgsAndStates";
-import {generateRoutes} from "./generateRoutes";
+import {expandCultures} from "./cultures/expandCultures";
+import {generateCultures} from "./cultures/generateCultures";
+import {generateProvinces} from "./provinces/generateProvinces";
import {generateReligions} from "./religions/generateReligions";
+import {repackGrid} from "./repackGrid";
+import {generateRivers} from "./rivers/generateRivers";
+import {generateRoutes} from "./routes/generateRoutes";
-const {LAND_COAST, WATER_COAST, DEEPER_WATER} = DISTANCE_FIELD;
const {Biomes} = window;
export function createPack(grid: IGrid): IPack {
@@ -149,12 +143,17 @@ export function createPack(grid: IGrid): IPack {
}
});
- // BurgsAndStates.generateProvinces();
- // BurgsAndStates.defineBurgFeatures();
-
- // renderLayer("states");
- // renderLayer("borders");
- // BurgsAndStates.drawStateLabels();
+ const {provinceIds, provinces} = generateProvinces(states, burgs, cultures, mergedFeatures, vertices, {
+ i: cells.i,
+ c: cells.c,
+ v: cells.v,
+ h: heights,
+ t: distanceField,
+ f: featureIds,
+ culture: cultureIds,
+ state: stateIds,
+ burg: burgIds
+ });
// Rivers.specify();
// const updatedFeatures = generateLakeNames();
@@ -190,7 +189,7 @@ export function createPack(grid: IGrid): IPack {
state: stateIds,
route: cellRoutes,
religion: religionIds,
- province: new Uint16Array(cells.i.length)
+ province: provinceIds
},
features: mergedFeatures,
rivers: rawRivers, // "name" | "basin" | "type"
@@ -199,77 +198,9 @@ export function createPack(grid: IGrid): IPack {
burgs,
routes,
religions,
+ provinces,
events
};
return pack;
}
-
-// repack grid cells: discart deep water cells, add land cells along the coast
-function repackGrid(grid: IGrid) {
- TIME && console.time("repackGrid");
- const {cells: gridCells, points, features} = grid;
- const newCells: {p: TPoints; g: number[]; h: number[]} = {p: [], g: [], h: []}; // store new data
- const spacing2 = grid.spacing ** 2;
-
- for (const i of gridCells.i) {
- const height = gridCells.h[i];
- const type = gridCells.t[i];
-
- // exclude ocean points far from coast
- if (height < MIN_LAND_HEIGHT && type !== WATER_COAST && type !== DEEPER_WATER) continue;
-
- const feature = features[gridCells.f[i]];
- const isLake = feature && feature.type === "lake";
-
- // exclude non-coastal lake points
- if (type === DEEPER_WATER && (i % 4 === 0 || isLake)) continue;
-
- const [x, y] = points[i];
- addNewPoint(i, x, y, height);
-
- // add additional points for cells along coast
- if (type === LAND_COAST || type === WATER_COAST) {
- if (gridCells.b[i]) continue; // not for near-border cells
- gridCells.c[i].forEach(e => {
- if (i > e) return;
- if (gridCells.t[e] === type) {
- const dist2 = (y - points[e][1]) ** 2 + (x - points[e][0]) ** 2;
- if (dist2 < spacing2) return; // too close to each other
- const x1 = rn((x + points[e][0]) / 2, 1);
- const y1 = rn((y + points[e][1]) / 2, 1);
- addNewPoint(i, x1, y1, height);
- }
- });
- }
- }
-
- function addNewPoint(i: number, x: number, y: number, height: number) {
- newCells.p.push([x, y]);
- newCells.g.push(i);
- newCells.h.push(height);
- }
-
- const {cells, vertices} = calculateVoronoi(newCells.p, grid.boundary);
-
- function getCellArea(i: number) {
- const polygon = cells.v[i].map(v => vertices.p[v]);
- const area = Math.abs(d3.polygonArea(polygon));
- return Math.min(area, UINT16_MAX);
- }
-
- const pack = {
- vertices,
- cells: {
- ...cells,
- p: newCells.p,
- g: createTypedArray({maxValue: grid.points.length, from: newCells.g}),
- q: d3.quadtree(newCells.p.map(([x, y], i) => [x, y, i])) as unknown as Quadtree,
- h: new Uint8Array(newCells.h),
- area: createTypedArray({maxValue: UINT16_MAX, from: cells.i}).map(getCellArea)
- }
- };
-
- TIME && console.timeEnd("repackGrid");
- return pack;
-}
diff --git a/src/scripts/generation/pack/provinces/config.ts b/src/scripts/generation/pack/provinces/config.ts
new file mode 100644
index 00000000..a2ea85d9
--- /dev/null
+++ b/src/scripts/generation/pack/provinces/config.ts
@@ -0,0 +1,8 @@
+export const provinceForms = {
+ 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}
+};
diff --git a/src/scripts/generation/pack/provinces/expandProvinces.ts b/src/scripts/generation/pack/provinces/expandProvinces.ts
new file mode 100644
index 00000000..453d495b
--- /dev/null
+++ b/src/scripts/generation/pack/provinces/expandProvinces.ts
@@ -0,0 +1,86 @@
+import FlatQueue from "flatqueue";
+
+import {DISTANCE_FIELD, ELEVATION, MIN_LAND_HEIGHT} from "config/generation";
+import {gauss} from "utils/probabilityUtils";
+
+const {WATER_COAST} = DISTANCE_FIELD;
+const {MOUNTAINS, HILLS, LOWLANDS} = ELEVATION;
+
+export function expandProvinces(
+ percentage: number,
+ provinces: IProvince[],
+ cells: Pick
+) {
+ const provinceIds = new Uint16Array(cells.i.length);
+
+ const queue = new FlatQueue<{cellId: number; provinceId: number; stateId: number}>();
+ const cost: number[] = [];
+
+ const maxExpansionCost = percentage === 100 ? 1000 : gauss(20, 5, 5, 100) * percentage ** 0.5;
+
+ for (const {i: provinceId, center: cellId, state: stateId} of provinces) {
+ provinceIds[cellId] = provinceId;
+ cost[cellId] = 1;
+ queue.push({cellId, provinceId, stateId}, 0);
+ }
+
+ while (queue.length) {
+ const priority = queue.peekValue()!;
+ const {cellId, provinceId, stateId} = queue.pop()!;
+
+ cells.c[cellId].forEach(neibCellId => {
+ const isLand = cells.h[neibCellId] >= MIN_LAND_HEIGHT;
+ if (isLand && cells.state[neibCellId] !== stateId) return; // can expand only within state
+
+ const evevationCost = getElevationCost(cells.h[neibCellId], cells.t[neibCellId]);
+ const totalCost = priority + evevationCost;
+ if (totalCost > maxExpansionCost) return;
+
+ if (!cost[neibCellId] || totalCost < cost[neibCellId]) {
+ if (isLand) provinceIds[neibCellId] = provinceId; // assign province to cell
+ cost[neibCellId] = totalCost;
+
+ queue.push({cellId: neibCellId, provinceId, stateId}, totalCost);
+ }
+ });
+ }
+
+ return normalizeProvinces(provinceIds, cells.c, cells.state, cells.burg);
+}
+
+function getElevationCost(elevation: number, distance: number) {
+ if (elevation >= MOUNTAINS) return 100;
+ if (elevation >= HILLS) return 30;
+ if (elevation >= LOWLANDS) return 10;
+ if (elevation >= MIN_LAND_HEIGHT) return 5;
+ if (distance === WATER_COAST) return 100;
+
+ return 300; // deep water
+}
+
+function normalizeProvinces(
+ provinceIds: Uint16Array,
+ neibCells: number[][],
+ stateIds: Uint16Array,
+ burgIds: Uint16Array
+) {
+ const normalizedIds = Uint16Array.from(provinceIds);
+
+ for (let cellId = 0; cellId < neibCells.length; cellId++) {
+ if (!stateIds[cellId]) continue; // skip water or neutral cells
+ if (burgIds[cellId]) continue; // do not overwrite burgs
+
+ const neibs = neibCells[cellId].filter(neib => stateIds[neib] >= stateIds[cellId]);
+
+ const adversaries = neibs.filter(neib => normalizedIds[neib] !== normalizedIds[cellId]);
+ if (adversaries.length < 2) continue;
+
+ const buddies = neibs.filter(neib => normalizedIds[neib] === normalizedIds[cellId]);
+ if (buddies.length > 2) continue;
+
+ // change cells's province
+ if (adversaries.length > buddies.length) normalizedIds[cellId] = normalizedIds[adversaries[0]];
+ }
+
+ return normalizedIds;
+}
diff --git a/src/scripts/generation/pack/provinces/generateCoreProvinces.ts b/src/scripts/generation/pack/provinces/generateCoreProvinces.ts
new file mode 100644
index 00000000..84c804f9
--- /dev/null
+++ b/src/scripts/generation/pack/provinces/generateCoreProvinces.ts
@@ -0,0 +1,69 @@
+import {group} from "d3-array";
+
+import {brighter, getMixedColor} from "utils/colorUtils";
+import {gauss, P, rw} from "utils/probabilityUtils";
+import {isBurg, isState} from "utils/typeUtils";
+import {provinceForms} from "./config";
+
+const {COA, Names} = window;
+
+export function generateCoreProvinces(states: TStates, burgs: TBurgs, cultures: TCultures, percentage: number) {
+ const provinces = [] as IProvince[];
+
+ const validBurgs = burgs.filter(isBurg);
+ const burgsToStateMap = group(validBurgs, (burg: IBurg) => burg.state);
+
+ states.filter(isState).forEach(state => {
+ const stateBurgs = burgsToStateMap.get(state.i);
+ if (!stateBurgs || stateBurgs.length < 2) return; // at least 2 provinces are required
+
+ stateBurgs
+ .sort((a, b) => b.population * gauss(1, 0.2, 0.5, 1.5, 3) - a.population)
+ .sort((a, b) => b.capital - a.capital);
+
+ const provincesNumber = Math.max(Math.ceil((stateBurgs.length * percentage) / 100), 2);
+ const formsPool: Dict = structuredClone(provinceForms[state.form]);
+
+ for (let i = 0; i < provincesNumber; i++) {
+ const {i: burg, cell: center, culture: cultureId, coa: burgEmblem, name: burgName, type} = stateBurgs[i];
+
+ const nameByBurg = P(0.5);
+ const name = generateName(nameByBurg, burgName, cultureId, cultures);
+ const formName = rw(formsPool);
+ formsPool[formName] += 10; // increase chance to get the same form again
+
+ const fullName = name + " " + formName;
+ const color = brighter(getMixedColor(state.color, 0.2), 0.3);
+ const coa = generateEmblem(nameByBurg, burgEmblem, type, cultures, cultureId, state);
+
+ provinces.push({i: provinces.length + 1, name, formName, center, burg, state: state.i, fullName, color, coa});
+ }
+ });
+
+ return provinces;
+}
+
+function generateName(nameByBurg: boolean, burgName: string, cultureId: number, cultures: TCultures) {
+ if (nameByBurg) return burgName;
+
+ const base = cultures[cultureId].base;
+ return Names.getState(Names.getBaseShort(base), base);
+}
+
+function generateEmblem(
+ nameByBurg: boolean,
+ burgEmblem: ICoa | "string",
+ type: TCultureType,
+ cultures: TCultures,
+ cultureId: number,
+ state: IState
+) {
+ const kinship = nameByBurg ? 0.8 : 0.4;
+ const coa: ICoa = COA.generate(burgEmblem, kinship, null, type);
+
+ const cultureShield = cultures[cultureId].shield;
+ const stateShield = (state.coa as ICoa)?.shield;
+ coa.shield = COA.getShield(cultureShield, stateShield);
+
+ return coa;
+}
diff --git a/src/scripts/generation/pack/provinces/generateProvinces.ts b/src/scripts/generation/pack/provinces/generateProvinces.ts
new file mode 100644
index 00000000..d9c79c62
--- /dev/null
+++ b/src/scripts/generation/pack/provinces/generateProvinces.ts
@@ -0,0 +1,39 @@
+import {TIME} from "config/logging";
+import {getInputNumber} from "utils/nodeUtils";
+import {expandProvinces} from "./expandProvinces";
+import {generateCoreProvinces} from "./generateCoreProvinces";
+import {generateWildProvinces} from "./generateWildProvinces";
+import {specifyProvinces} from "./specifyProvinces";
+
+export function generateProvinces(
+ states: TStates,
+ burgs: TBurgs,
+ cultures: TCultures,
+ features: TPackFeatures,
+ vertices: IGraphVertices,
+ cells: Pick
+): {provinceIds: Uint16Array; provinces: TProvinces} {
+ TIME && console.time("generateProvinces");
+
+ const percentage = getInputNumber("provincesInput");
+ if (states.length < 2 || percentage === 0) return {provinceIds: new Uint16Array(cells.i.length), provinces: [0]};
+
+ const coreProvinces = generateCoreProvinces(states, burgs, cultures, percentage);
+ const provinceIds = expandProvinces(percentage, coreProvinces, cells);
+
+ const wildProvinces = generateWildProvinces({
+ states,
+ burgs,
+ cultures,
+ features,
+ coreProvinces,
+ provinceIds,
+ percentage,
+ cells
+ }); // mutates provinceIds
+
+ const provinces = specifyProvinces(provinceIds, coreProvinces, wildProvinces, vertices, cells.c, cells.v);
+
+ TIME && console.timeEnd("generateProvinces");
+ return {provinceIds, provinces};
+}
diff --git a/src/scripts/generation/pack/provinces/generateWildProvinces.ts b/src/scripts/generation/pack/provinces/generateWildProvinces.ts
new file mode 100644
index 00000000..f9f64c5e
--- /dev/null
+++ b/src/scripts/generation/pack/provinces/generateWildProvinces.ts
@@ -0,0 +1,200 @@
+import {group} from "d3-array";
+import FlatQueue from "flatqueue";
+
+import {DISTANCE_FIELD, MIN_LAND_HEIGHT} from "config/generation";
+import {unique} from "utils/arrayUtils";
+import {brighter, getMixedColor} from "utils/colorUtils";
+import {gauss, P, ra, rw} from "utils/probabilityUtils";
+import {isBurg, isState} from "utils/typeUtils";
+import {provinceForms} from "./config";
+
+const {COA, Names} = window;
+
+// add "wild" provinces if some cells don't have a province assigned
+export function generateWildProvinces({
+ states,
+ burgs,
+ cultures,
+ features,
+ coreProvinces,
+ provinceIds,
+ percentage,
+ cells
+}: {
+ states: TStates;
+ burgs: TBurgs;
+ cultures: TCultures;
+ features: TPackFeatures;
+ coreProvinces: IProvince[];
+ provinceIds: Uint16Array;
+ percentage: number;
+ cells: Pick;
+}) {
+ const noProvinceCells = Array.from(cells.i.filter(i => cells.state[i] && !provinceIds[i]));
+ const wildProvinces = [] as IProvince[];
+ const colonyNamesMap = createColonyNamesMap();
+
+ for (const state of states) {
+ if (!isState(state)) continue;
+
+ let noProvinceCellsInState = noProvinceCells.filter(i => cells.state[i] === state.i);
+ while (noProvinceCellsInState.length) {
+ const provinceId = coreProvinces.length + wildProvinces.length + 1;
+ const burgCell = noProvinceCellsInState.find(i => cells.burg[i]);
+ const center = burgCell || noProvinceCellsInState[0];
+ const cultureId = cells.culture[center];
+
+ const burgId = burgCell ? cells.burg[burgCell] : 0;
+ const burg = burgs[burgId];
+
+ const provinceCells = expandWildProvince(center, provinceId, state.i); // mutates provinceIds
+ const formName = getProvinceForm(center, provinceCells, state.center);
+ const name = getProvinceName(state.i, formName, burg, cultureId);
+ const fullName = name + " " + formName;
+
+ const coa = generateEmblem(formName, state, burg, cultureId);
+ const color = brighter(getMixedColor(state.color, 0.2), 0.3);
+
+ wildProvinces.push({i: provinceId, name, formName, center, burg: burgId, state: state.i, fullName, color, coa});
+
+ // re-check
+ noProvinceCellsInState = noProvinceCells.filter(i => cells.state[i] === state.i && !provinceIds[i]);
+ }
+ }
+
+ return wildProvinces;
+
+ function createColonyNamesMap() {
+ const stateProvincesMap = group(coreProvinces, (province: IProvince) => province.state);
+
+ const colonyNamesMap = new Map(
+ states.map(state => {
+ const stateProvinces = stateProvincesMap.get(state.i) || [];
+ const coreProvinceNames = stateProvinces.map(province => province.name);
+ const colonyNamePool = unique([state.name, ...coreProvinceNames].filter(name => name && !/new/i.test(name)));
+ return [state.i, colonyNamePool];
+ })
+ );
+
+ return colonyNamesMap;
+ }
+
+ function getColonyName(stateId: number) {
+ const namesPool = colonyNamesMap.get(stateId) || [];
+ if (namesPool.length < 1) return null;
+
+ const name = ra(namesPool);
+ colonyNamesMap.set(
+ stateId,
+ namesPool.filter(n => n !== name)
+ );
+
+ return `New ${name}`;
+ }
+
+ function getProvinceName(stateId: number, formName: string, burg: TNoBurg | IBurg, cultureId: number) {
+ const colonyName = formName === "Colony" && P(0.8) && getColonyName(stateId);
+ if (colonyName) return colonyName;
+
+ if (burg?.name && P(0.5)) return burg.name;
+
+ const base = cultures[cultureId].base;
+ return Names.getState(Names.getBaseShort(base), base);
+ }
+
+ function expandWildProvince(center: number, provinceId: number, stateId: number) {
+ const maxExpansionCost = percentage === 100 ? 1000 : gauss(20, 5, 5, 100) * percentage ** 0.5;
+
+ const provinceCells = [center];
+ provinceIds[center] = provinceId;
+
+ const queue = new FlatQueue();
+ const cost: number[] = [];
+ cost[center] = 1;
+ queue.push(center, 0);
+
+ while (queue.length) {
+ const priority = queue.peekValue()!;
+ const next = queue.pop()!;
+
+ cells.c[next].forEach(neibCellId => {
+ if (provinceIds[neibCellId]) return;
+ if (cells.state[neibCellId] !== stateId) return;
+
+ const isLand = cells.h[neibCellId] >= MIN_LAND_HEIGHT;
+ const cellCost = isLand ? 3 : cells.t[neibCellId] === DISTANCE_FIELD.WATER_COAST ? 10 : 30;
+ const totalCost = priority + cellCost;
+ if (totalCost > maxExpansionCost) return;
+
+ if (!cost[neibCellId] || totalCost < cost[neibCellId]) {
+ if (isLand && cells.state[neibCellId] === stateId) {
+ // assign province to a cell
+ provinceCells.push(neibCellId);
+ provinceIds[neibCellId] = provinceId;
+ }
+ cost[neibCellId] = totalCost;
+ queue.push(neibCellId, totalCost);
+ }
+ });
+ }
+
+ return provinceCells;
+ }
+
+ function getProvinceForm(center: number, provinceCells: number[], stateCenter: number) {
+ const feature = features[cells.f[center]];
+ if (feature === 0) throw new Error("Feature is not defined");
+
+ const provinceFeatures = unique(provinceCells.map(i => cells.f[i]));
+ const isWholeIsle = provinceCells.length === feature.cells && provinceFeatures.length === 1;
+ if (isWholeIsle) return "Island";
+
+ const isIsleGroup = provinceFeatures.every(featureId => (features[featureId] as TPackFeature)?.group === "isle");
+ if (isIsleGroup) return "Islands";
+
+ const isColony = P(0.5) && !isConnected(stateCenter, center);
+ if (isColony) return "Colony";
+
+ return rw(provinceForms["Wild"]);
+
+ // check if two cells are connected by land withing same state
+ function isConnected(from: number, to: number) {
+ if (cells.f[from] !== cells.f[to]) return false; // on different islands
+ const queue = [from];
+ const checked: Dict = {[from]: true};
+ const stateId = cells.state[from];
+
+ while (queue.length) {
+ const current = queue.pop()!;
+ if (current === to) return true;
+
+ for (const neibId of cells.c[current]) {
+ if (checked[neibId] || cells.state[neibId] !== stateId) continue;
+ queue.push(neibId);
+ checked[neibId] = true;
+ }
+ }
+ return false;
+ }
+ }
+
+ function generateEmblem(formName: string, state: IState, burg: TNoBurg | IBurg, cultureId: number) {
+ const dominion = P(getDominionChance(formName));
+ const kinship = dominion ? 0 : 0.4;
+ const coaType = isBurg(burg) ? burg.type : "Generic";
+ const coa = COA.generate(state.coa, kinship, dominion, coaType);
+
+ const cultureShield = cultures[cultureId].shield;
+ const stateShield = (state.coa as ICoa)?.shield;
+ coa.shield = COA.getShield(cultureShield, stateShield);
+
+ return coa;
+ }
+
+ function getDominionChance(formName: string) {
+ if (formName === "Colony") return 0.95;
+ if (formName === "Island") return 0.7;
+ if (formName === "Islands") return 0.7;
+ return 0.3;
+ }
+}
diff --git a/src/scripts/generation/pack/provinces/specifyProvinces.ts b/src/scripts/generation/pack/provinces/specifyProvinces.ts
new file mode 100644
index 00000000..96d88dc2
--- /dev/null
+++ b/src/scripts/generation/pack/provinces/specifyProvinces.ts
@@ -0,0 +1,20 @@
+import {getPolesOfInaccessibility} from "scripts/getPolesOfInaccessibility";
+
+export function specifyProvinces(
+ provinceIds: Uint16Array,
+ coreProvinces: IProvince[],
+ wildProvinces: IProvince[],
+ vertices: IGraphVertices,
+ cellNeighbors: number[][],
+ cellVertices: number[][]
+): TProvinces {
+ const getType = (cellId: number) => provinceIds[cellId];
+ const poles = getPolesOfInaccessibility({vertices, getType, cellNeighbors, cellVertices});
+
+ const provinces = [...coreProvinces, ...wildProvinces].map(province => {
+ const pole = poles[province.i];
+ return {...province, pole};
+ });
+
+ return [0, ...provinces];
+}
diff --git a/src/scripts/generation/pack/repackGrid.ts b/src/scripts/generation/pack/repackGrid.ts
new file mode 100644
index 00000000..48a32b1e
--- /dev/null
+++ b/src/scripts/generation/pack/repackGrid.ts
@@ -0,0 +1,79 @@
+import * as d3 from "d3";
+
+import {UINT16_MAX} from "config/constants";
+import {DISTANCE_FIELD, MIN_LAND_HEIGHT} from "config/generation";
+import {TIME} from "config/logging";
+import {createTypedArray} from "utils/arrayUtils";
+import {rn} from "utils/numberUtils";
+import {calculateVoronoi} from "../graph";
+
+const {LAND_COAST, WATER_COAST, DEEPER_WATER} = DISTANCE_FIELD;
+
+// repack grid cells: discart deep water cells, add land cells along the coast
+export function repackGrid(grid: IGrid) {
+ TIME && console.time("repackGrid");
+ const {cells: gridCells, points, features} = grid;
+ const newCells: {p: TPoints; g: number[]; h: number[]} = {p: [], g: [], h: []}; // store new data
+ const spacing2 = grid.spacing ** 2;
+
+ for (const i of gridCells.i) {
+ const height = gridCells.h[i];
+ const type = gridCells.t[i];
+
+ // exclude ocean points far from coast
+ if (height < MIN_LAND_HEIGHT && type !== WATER_COAST && type !== DEEPER_WATER) continue;
+
+ const feature = features[gridCells.f[i]];
+ const isLake = feature && feature.type === "lake";
+
+ // exclude non-coastal lake points
+ if (type === DEEPER_WATER && (i % 4 === 0 || isLake)) continue;
+
+ const [x, y] = points[i];
+ addNewPoint(i, x, y, height);
+
+ // add additional points for cells along coast
+ if (type === LAND_COAST || type === WATER_COAST) {
+ if (gridCells.b[i]) continue; // not for near-border cells
+ gridCells.c[i].forEach(e => {
+ if (i > e) return;
+ if (gridCells.t[e] === type) {
+ const dist2 = (y - points[e][1]) ** 2 + (x - points[e][0]) ** 2;
+ if (dist2 < spacing2) return; // too close to each other
+ const x1 = rn((x + points[e][0]) / 2, 1);
+ const y1 = rn((y + points[e][1]) / 2, 1);
+ addNewPoint(i, x1, y1, height);
+ }
+ });
+ }
+ }
+
+ function addNewPoint(i: number, x: number, y: number, height: number) {
+ newCells.p.push([x, y]);
+ newCells.g.push(i);
+ newCells.h.push(height);
+ }
+
+ const {cells, vertices} = calculateVoronoi(newCells.p, grid.boundary);
+
+ function getCellArea(i: number) {
+ const polygon = cells.v[i].map(v => vertices.p[v]);
+ const area = Math.abs(d3.polygonArea(polygon));
+ return Math.min(area, UINT16_MAX);
+ }
+
+ const pack = {
+ vertices,
+ cells: {
+ ...cells,
+ p: newCells.p,
+ g: createTypedArray({maxValue: grid.points.length, from: newCells.g}),
+ q: d3.quadtree(newCells.p.map(([x, y], i) => [x, y, i])) as unknown as Quadtree,
+ h: new Uint8Array(newCells.h),
+ area: createTypedArray({maxValue: UINT16_MAX, from: cells.i}).map(getCellArea)
+ }
+ };
+
+ TIME && console.timeEnd("repackGrid");
+ return pack;
+}
diff --git a/src/scripts/generation/pack/rivers.ts b/src/scripts/generation/pack/rivers/generateRivers.ts
similarity index 62%
rename from src/scripts/generation/pack/rivers.ts
rename to src/scripts/generation/pack/rivers/generateRivers.ts
index 8fe4bc21..b21b5fe5 100644
--- a/src/scripts/generation/pack/rivers.ts
+++ b/src/scripts/generation/pack/rivers/generateRivers.ts
@@ -1,13 +1,13 @@
import * as d3 from "d3";
-import {INFO, TIME, WARN} from "config/logging";
+import {TIME} from "config/logging";
import {rn} from "utils/numberUtils";
import {aleaPRNG} from "scripts/aleaPRNG";
-import {DISTANCE_FIELD, MAX_HEIGHT, MIN_LAND_HEIGHT} from "config/generation";
-import {getInputNumber} from "utils/nodeUtils";
+import {DISTANCE_FIELD, MIN_LAND_HEIGHT} from "config/generation";
import {pick} from "utils/functionUtils";
import {byId} from "utils/shorthands";
-import {mergeLakeData, getClimateData, ILakeClimateData} from "./lakes";
+import {mergeLakeData, getClimateData, ILakeClimateData} from "../lakes/lakes";
+import {resolveDepressions} from "./resolveDepressions";
const {Rivers} = window;
const {LAND_COAST} = DISTANCE_FIELD;
@@ -276,155 +276,10 @@ export function generateRivers(
}
// add distance to water value to land cells to make map less depressed
-const applyDistanceField = ({h, c, t}: Pick) => {
+function applyDistanceField({h, c, t}: Pick) {
return new Float32Array(h.length).map((_, index) => {
if (h[index] < MIN_LAND_HEIGHT || t[index] < LAND_COAST) return h[index];
const mean = d3.mean(c[index].map(c => t[c])) || 0;
return h[index] + t[index] / 100 + mean / 10000;
});
-};
-
-// depression filling algorithm (for a correct water flux modeling)
-const resolveDepressions = function (
- cells: Pick,
- features: TPackFeatures,
- initialCellHeights: Float32Array
-): [Float32Array, Dict] {
- TIME && console.time("resolveDepressions");
-
- const MAX_INTERATIONS = getInputNumber("resolveDepressionsStepsOutput");
- const checkLakeMaxIteration = MAX_INTERATIONS * 0.85;
- const elevateLakeMaxIteration = MAX_INTERATIONS * 0.75;
-
- const LAND_ELEVATION_INCREMENT = 0.1;
- const LAKE_ELEVATION_INCREMENT = 0.2;
-
- const lakes = features.filter(feature => feature && feature.type === "lake") as IPackFeatureLake[];
- lakes.sort((a, b) => a.height - b.height); // lowest lakes go first
-
- const getHeight = (i: number) => currentLakeHeights[cells.f[i]] || currentCellHeights[i];
- const getMinHeight = (cellsIds: number[]) => Math.min(...cellsIds.map(getHeight));
- const getMinLandHeight = (cellsIds: number[]) => Math.min(...cellsIds.map(i => currentCellHeights[i]));
-
- const landCells = cells.i.filter(i => initialCellHeights[i] >= MIN_LAND_HEIGHT && !cells.b[i]);
- landCells.sort((a, b) => initialCellHeights[a] - initialCellHeights[b]); // lowest cells go first
-
- const currentCellHeights = Float32Array.from(initialCellHeights);
- const currentLakeHeights = Object.fromEntries(lakes.map(({i, height}) => [i, height]));
- const currentDrainableLakes = checkLakesDrainability();
- const depressions: number[] = [];
-
- let bestDepressions = Infinity;
- let bestCellHeights: typeof currentCellHeights | null = null;
- let bestDrainableLakes: typeof currentDrainableLakes | null = null;
-
- for (let iteration = 0; depressions.at(-1) !== 0 && iteration < MAX_INTERATIONS; iteration++) {
- let depressionsLeft = 0;
-
- // elevate potentially drainable lakes
- if (iteration < checkLakeMaxIteration) {
- for (const lake of lakes) {
- if (currentDrainableLakes[lake.i] !== true) continue;
-
- const minShoreHeight = getMinLandHeight(lake.shoreline);
- if (minShoreHeight >= MAX_HEIGHT || currentLakeHeights[lake.i] > minShoreHeight) continue;
-
- if (iteration > elevateLakeMaxIteration) {
- // reset heights
- for (const shoreCellId of lake.shoreline) {
- currentCellHeights[shoreCellId] = initialCellHeights[shoreCellId];
- }
- currentLakeHeights[lake.i] = lake.height;
-
- currentDrainableLakes[lake.i] = false;
- continue;
- }
-
- currentLakeHeights[lake.i] = minShoreHeight + LAKE_ELEVATION_INCREMENT;
- depressionsLeft++;
- }
- }
-
- for (const cellId of landCells) {
- const minHeight = getMinHeight(cells.c[cellId]);
- if (minHeight >= MAX_HEIGHT || currentCellHeights[cellId] > minHeight) continue;
-
- currentCellHeights[cellId] = minHeight + LAND_ELEVATION_INCREMENT;
- depressionsLeft++;
- }
-
- depressions.push(depressionsLeft);
- if (depressionsLeft < bestDepressions) {
- bestDepressions = depressionsLeft;
- bestCellHeights = Float32Array.from(currentCellHeights);
- bestDrainableLakes = structuredClone(currentDrainableLakes);
- }
- }
-
- TIME && console.timeEnd("resolveDepressions");
-
- const depressionsLeft = depressions.at(-1);
- if (depressionsLeft) {
- if (bestCellHeights && bestDrainableLakes) {
- WARN &&
- console.warn(`Cannot resolve all depressions. Depressions: ${depressions[0]}. Best result: ${bestDepressions}`);
- return [bestCellHeights, bestDrainableLakes];
- }
-
- WARN && console.warn(`Cannot resolve depressions. Depressions: ${depressionsLeft}`);
- return [initialCellHeights, {}];
- }
-
- INFO && console.info(`ⓘ resolved all ${depressions[0]} depressions in ${depressions.length} iterations`);
- return [currentCellHeights, currentDrainableLakes];
-
- // define lakes that potentially can be open (drained into another water body)
- function checkLakesDrainability() {
- const canBeDrained: Dict = {}; // all false by default
-
- const ELEVATION_LIMIT = getInputNumber("lakeElevationLimitOutput");
- const drainAllLakes = ELEVATION_LIMIT === MAX_HEIGHT - MIN_LAND_HEIGHT;
-
- for (const lake of lakes) {
- if (drainAllLakes) {
- canBeDrained[lake.i] = true;
- continue;
- }
-
- canBeDrained[lake.i] = false;
- const minShoreHeight = getMinHeight(lake.shoreline);
- const minHeightShoreCell =
- lake.shoreline.find(cellId => initialCellHeights[cellId] === minShoreHeight) || lake.shoreline[0];
-
- const queue = [minHeightShoreCell];
- const checked = [];
- checked[minHeightShoreCell] = true;
- const breakableHeight = lake.height + ELEVATION_LIMIT;
-
- loopCellsAroundLake: while (queue.length) {
- const cellId = queue.pop()!;
-
- for (const neibCellId of cells.c[cellId]) {
- if (checked[neibCellId]) continue;
- if (initialCellHeights[neibCellId] >= breakableHeight) continue;
-
- if (initialCellHeights[neibCellId] < MIN_LAND_HEIGHT) {
- const waterFeatureMet = features[cells.f[neibCellId]];
- const isOceanMet = waterFeatureMet && waterFeatureMet.type === "ocean";
- const isLakeMet = waterFeatureMet && waterFeatureMet.type === "lake";
-
- if (isOceanMet || (isLakeMet && lake.height > waterFeatureMet.height)) {
- canBeDrained[lake.i] = true;
- break loopCellsAroundLake;
- }
- }
-
- checked[neibCellId] = true;
- queue.push(neibCellId);
- }
- }
- }
-
- return canBeDrained;
- }
-};
+}
diff --git a/src/scripts/generation/pack/rivers/resolveDepressions.ts b/src/scripts/generation/pack/rivers/resolveDepressions.ts
new file mode 100644
index 00000000..301a425e
--- /dev/null
+++ b/src/scripts/generation/pack/rivers/resolveDepressions.ts
@@ -0,0 +1,148 @@
+import {MIN_LAND_HEIGHT, MAX_HEIGHT} from "config/generation";
+import {TIME, WARN, INFO} from "config/logging";
+import {getInputNumber} from "utils/nodeUtils";
+
+// depression filling algorithm (for a correct water flux modeling)
+export function resolveDepressions(
+ cells: Pick,
+ features: TPackFeatures,
+ initialCellHeights: Float32Array
+): [Float32Array, Dict] {
+ TIME && console.time("resolveDepressions");
+
+ const MAX_INTERATIONS = getInputNumber("resolveDepressionsStepsOutput");
+ const checkLakeMaxIteration = MAX_INTERATIONS * 0.85;
+ const elevateLakeMaxIteration = MAX_INTERATIONS * 0.75;
+
+ const LAND_ELEVATION_INCREMENT = 0.1;
+ const LAKE_ELEVATION_INCREMENT = 0.2;
+
+ const lakes = features.filter(feature => feature && feature.type === "lake") as IPackFeatureLake[];
+ lakes.sort((a, b) => a.height - b.height); // lowest lakes go first
+
+ const getHeight = (i: number) => currentLakeHeights[cells.f[i]] || currentCellHeights[i];
+ const getMinHeight = (cellsIds: number[]) => Math.min(...cellsIds.map(getHeight));
+ const getMinLandHeight = (cellsIds: number[]) => Math.min(...cellsIds.map(i => currentCellHeights[i]));
+
+ const landCells = cells.i.filter(i => initialCellHeights[i] >= MIN_LAND_HEIGHT && !cells.b[i]);
+ landCells.sort((a, b) => initialCellHeights[a] - initialCellHeights[b]); // lowest cells go first
+
+ const currentCellHeights = Float32Array.from(initialCellHeights);
+ const currentLakeHeights = Object.fromEntries(lakes.map(({i, height}) => [i, height]));
+ const currentDrainableLakes = checkLakesDrainability();
+ const depressions: number[] = [];
+
+ let bestDepressions = Infinity;
+ let bestCellHeights: typeof currentCellHeights | null = null;
+ let bestDrainableLakes: typeof currentDrainableLakes | null = null;
+
+ for (let iteration = 0; depressions.at(-1) !== 0 && iteration < MAX_INTERATIONS; iteration++) {
+ let depressionsLeft = 0;
+
+ // elevate potentially drainable lakes
+ if (iteration < checkLakeMaxIteration) {
+ for (const lake of lakes) {
+ if (currentDrainableLakes[lake.i] !== true) continue;
+
+ const minShoreHeight = getMinLandHeight(lake.shoreline);
+ if (minShoreHeight >= MAX_HEIGHT || currentLakeHeights[lake.i] > minShoreHeight) continue;
+
+ if (iteration > elevateLakeMaxIteration) {
+ // reset heights
+ for (const shoreCellId of lake.shoreline) {
+ currentCellHeights[shoreCellId] = initialCellHeights[shoreCellId];
+ }
+ currentLakeHeights[lake.i] = lake.height;
+
+ currentDrainableLakes[lake.i] = false;
+ continue;
+ }
+
+ currentLakeHeights[lake.i] = minShoreHeight + LAKE_ELEVATION_INCREMENT;
+ depressionsLeft++;
+ }
+ }
+
+ for (const cellId of landCells) {
+ const minHeight = getMinHeight(cells.c[cellId]);
+ if (minHeight >= MAX_HEIGHT || currentCellHeights[cellId] > minHeight) continue;
+
+ currentCellHeights[cellId] = minHeight + LAND_ELEVATION_INCREMENT;
+ depressionsLeft++;
+ }
+
+ depressions.push(depressionsLeft);
+ if (depressionsLeft < bestDepressions) {
+ bestDepressions = depressionsLeft;
+ bestCellHeights = Float32Array.from(currentCellHeights);
+ bestDrainableLakes = structuredClone(currentDrainableLakes);
+ }
+ }
+
+ TIME && console.timeEnd("resolveDepressions");
+
+ const depressionsLeft = depressions.at(-1);
+ if (depressionsLeft) {
+ if (bestCellHeights && bestDrainableLakes) {
+ WARN &&
+ console.warn(`Cannot resolve all depressions. Depressions: ${depressions[0]}. Best result: ${bestDepressions}`);
+ return [bestCellHeights, bestDrainableLakes];
+ }
+
+ WARN && console.warn(`Cannot resolve depressions. Depressions: ${depressionsLeft}`);
+ return [initialCellHeights, {}];
+ }
+
+ INFO && console.info(`ⓘ resolved all ${depressions[0]} depressions in ${depressions.length} iterations`);
+ return [currentCellHeights, currentDrainableLakes];
+
+ // define lakes that potentially can be open (drained into another water body)
+ function checkLakesDrainability() {
+ const canBeDrained: Dict = {}; // all false by default
+
+ const ELEVATION_LIMIT = getInputNumber("lakeElevationLimitOutput");
+ const drainAllLakes = ELEVATION_LIMIT === MAX_HEIGHT - MIN_LAND_HEIGHT;
+
+ for (const lake of lakes) {
+ if (drainAllLakes) {
+ canBeDrained[lake.i] = true;
+ continue;
+ }
+
+ canBeDrained[lake.i] = false;
+ const minShoreHeight = getMinHeight(lake.shoreline);
+ const minHeightShoreCell =
+ lake.shoreline.find(cellId => initialCellHeights[cellId] === minShoreHeight) || lake.shoreline[0];
+
+ const queue = [minHeightShoreCell];
+ const checked = [];
+ checked[minHeightShoreCell] = true;
+ const breakableHeight = lake.height + ELEVATION_LIMIT;
+
+ loopCellsAroundLake: while (queue.length) {
+ const cellId = queue.pop()!;
+
+ for (const neibCellId of cells.c[cellId]) {
+ if (checked[neibCellId]) continue;
+ if (initialCellHeights[neibCellId] >= breakableHeight) continue;
+
+ if (initialCellHeights[neibCellId] < MIN_LAND_HEIGHT) {
+ const waterFeatureMet = features[cells.f[neibCellId]];
+ const isOceanMet = waterFeatureMet && waterFeatureMet.type === "ocean";
+ const isLakeMet = waterFeatureMet && waterFeatureMet.type === "lake";
+
+ if (isOceanMet || (isLakeMet && lake.height > waterFeatureMet.height)) {
+ canBeDrained[lake.i] = true;
+ break loopCellsAroundLake;
+ }
+ }
+
+ checked[neibCellId] = true;
+ queue.push(neibCellId);
+ }
+ }
+ }
+
+ return canBeDrained;
+ }
+}
diff --git a/src/scripts/generation/pack/generateRoutes.ts b/src/scripts/generation/pack/routes/generateRoutes.ts
similarity index 88%
rename from src/scripts/generation/pack/generateRoutes.ts
rename to src/scripts/generation/pack/routes/generateRoutes.ts
index b7f3e76a..e852fbd6 100644
--- a/src/scripts/generation/pack/generateRoutes.ts
+++ b/src/scripts/generation/pack/routes/generateRoutes.ts
@@ -1,10 +1,10 @@
-import Delaunator from "delaunator";
import FlatQueue from "flatqueue";
import {TIME} from "config/logging";
import {ELEVATION, MIN_LAND_HEIGHT, ROUTES} from "config/generation";
import {dist2} from "utils/functionUtils";
import {isBurg} from "utils/typeUtils";
+import {calculateUrquhartEdges} from "./urquhart";
type TCellsData = Pick;
@@ -292,44 +292,3 @@ function getRouteSegments(pathCells: number[], connections: Map
return segments;
}
-
-// Urquhart graph is obtained by removing the longest edge from each triangle in the Delaunay triangulation
-// this gives us an aproximation of a desired road network, i.e. connections between burgs
-// code from https://observablehq.com/@mbostock/urquhart-graph
-function calculateUrquhartEdges(points: TPoints) {
- const score = (p0: number, p1: number) => dist2(points[p0], points[p1]);
-
- const {halfedges, triangles} = Delaunator.from(points);
- const n = triangles.length;
-
- const removed = new Uint8Array(n);
- const edges = [];
-
- for (let e = 0; e < n; e += 3) {
- const p0 = triangles[e],
- p1 = triangles[e + 1],
- p2 = triangles[e + 2];
-
- const p01 = score(p0, p1),
- p12 = score(p1, p2),
- p20 = score(p2, p0);
-
- removed[
- p20 > p01 && p20 > p12
- ? Math.max(e + 2, halfedges[e + 2])
- : p12 > p01 && p12 > p20
- ? Math.max(e + 1, halfedges[e + 1])
- : Math.max(e, halfedges[e])
- ] = 1;
- }
-
- for (let e = 0; e < n; ++e) {
- if (e > halfedges[e] && !removed[e]) {
- const t0 = triangles[e];
- const t1 = triangles[e % 3 === 2 ? e - 2 : e + 1];
- edges.push([t0, t1]);
- }
- }
-
- return edges;
-}
diff --git a/src/scripts/generation/pack/routes/urquhart.ts b/src/scripts/generation/pack/routes/urquhart.ts
new file mode 100644
index 00000000..67d6ecd3
--- /dev/null
+++ b/src/scripts/generation/pack/routes/urquhart.ts
@@ -0,0 +1,44 @@
+import Delaunator from "delaunator";
+
+import {dist2} from "utils/functionUtils";
+
+// Urquhart graph is obtained by removing the longest edge from each triangle in the Delaunay triangulation
+// this gives us an aproximation of a desired road network, i.e. connections between burgs
+// code from https://observablehq.com/@mbostock/urquhart-graph
+export function calculateUrquhartEdges(points: TPoints) {
+ const score = (p0: number, p1: number) => dist2(points[p0], points[p1]);
+
+ const {halfedges, triangles} = Delaunator.from(points);
+ const n = triangles.length;
+
+ const removed = new Uint8Array(n);
+ const edges = [];
+
+ for (let e = 0; e < n; e += 3) {
+ const p0 = triangles[e],
+ p1 = triangles[e + 1],
+ p2 = triangles[e + 2];
+
+ const p01 = score(p0, p1),
+ p12 = score(p1, p2),
+ p20 = score(p2, p0);
+
+ removed[
+ p20 > p01 && p20 > p12
+ ? Math.max(e + 2, halfedges[e + 2])
+ : p12 > p01 && p12 > p20
+ ? Math.max(e + 1, halfedges[e + 1])
+ : Math.max(e, halfedges[e])
+ ] = 1;
+ }
+
+ for (let e = 0; e < n; ++e) {
+ if (e > halfedges[e] && !removed[e]) {
+ const t0 = triangles[e];
+ const t1 = triangles[e % 3 === 2 ? e - 2 : e + 1];
+ edges.push([t0, t1]);
+ }
+ }
+
+ return edges;
+}
diff --git a/src/scripts/getPolesOfInaccessibility.ts b/src/scripts/getPolesOfInaccessibility.ts
new file mode 100644
index 00000000..e95500b0
--- /dev/null
+++ b/src/scripts/getPolesOfInaccessibility.ts
@@ -0,0 +1,66 @@
+import polylabel from "polylabel";
+
+import {TIME} from "config/logging";
+import {connectVertices} from "./connectVertices";
+import {rn} from "utils/numberUtils";
+
+interface IGetPolesProps {
+ vertices: IGraphVertices;
+ cellNeighbors: number[][];
+ cellVertices: number[][];
+ getType: (cellId: number) => number;
+}
+
+export function getPolesOfInaccessibility(props: IGetPolesProps) {
+ TIME && console.time("getPolesOfInaccessibility");
+ const multiPolygons = getMultiPolygons(props);
+ const sortByLength = (a: unknown[], b: unknown[]) => b.length - a.length;
+
+ const poles: Dict = Object.fromEntries(
+ Object.entries(multiPolygons).map(([id, multiPolygon]) => {
+ const [x, y] = polylabel(multiPolygon.sort(sortByLength), 20);
+ return [id, [rn(x), rn(y)]];
+ })
+ );
+
+ TIME && console.timeEnd("getPolesOfInaccessibility");
+ return poles;
+}
+
+function getMultiPolygons({vertices, getType, cellNeighbors, cellVertices}: IGetPolesProps) {
+ const multiPolygons: Dict = {};
+
+ const checkedCells = new Uint8Array(cellNeighbors.length);
+ const addToChecked = (cellId: number) => {
+ checkedCells[cellId] = 1;
+ };
+ const isChecked = (cellId: number) => checkedCells[cellId] === 1;
+
+ for (let cellId = 0; cellId < cellNeighbors.length; cellId++) {
+ if (isChecked(cellId) || getType(cellId) === 0) continue;
+ addToChecked(cellId);
+
+ const type = getType(cellId);
+ const ofSameType = (cellId: number) => getType(cellId) === type;
+ const ofDifferentType = (cellId: number) => getType(cellId) !== type;
+
+ const onborderCell = cellNeighbors[cellId].find(ofDifferentType);
+ if (onborderCell === undefined) continue;
+
+ const startingVertex = cellVertices[cellId].find(v => vertices.c[v].some(ofDifferentType));
+ if (startingVertex === undefined) throw new Error(`Starting vertex for cell ${cellId} is not found`);
+
+ const vertexChain = connectVertices({vertices, startingVertex, ofSameType, addToChecked, closeRing: true});
+ if (vertexChain.length < 3) continue;
+
+ addPolygon(type, vertexChain);
+ }
+
+ return multiPolygons;
+
+ function addPolygon(id: number, vertexChain: number[]) {
+ if (!multiPolygons[id]) multiPolygons[id] = [];
+ const polygon = vertexChain.map(vertex => vertices.p[vertex]);
+ multiPolygons[id].push(polygon);
+ }
+}
diff --git a/src/types/pack/burgs.d.ts b/src/types/pack/burgs.d.ts
index 01e39700..36755f59 100644
--- a/src/types/pack/burgs.d.ts
+++ b/src/types/pack/burgs.d.ts
@@ -3,6 +3,7 @@ interface IBurg {
name: string;
feature: number;
state: number;
+ culture: number;
cell: number;
x: number;
y: number;
@@ -11,8 +12,12 @@ interface IBurg {
coa: ICoa | "string";
capital: Logical; // 1 - capital, 0 - burg
port: number; // port feature id, 0 - not a port
- shanty?: number;
- MFCG?: string | number;
+ citadel: Logical;
+ plaza: Logical;
+ walls: Logical;
+ shanty: Logical;
+ temple: Logical;
+ MFCG?: string | number; // MFCG link of seed
removed?: boolean;
}
diff --git a/src/types/pack/pack.d.ts b/src/types/pack/pack.d.ts
index 39491bf8..824431c1 100644
--- a/src/types/pack/pack.d.ts
+++ b/src/types/pack/pack.d.ts
@@ -27,8 +27,8 @@ interface IPackCells {
state: Uint16Array;
culture: Uint16Array;
religion: Uint16Array;
- province: UintArray;
- burg: UintArray;
+ province: Uint16Array;
+ burg: Uint16Array;
haven: UintArray;
harbor: UintArray;
route: Uint8Array; // [0, 1, 2, 3], see ROUTES enum, defined by generateRoutes()
diff --git a/src/types/pack/provinces.d.ts b/src/types/pack/provinces.d.ts
index 502b5b80..39f7ec8e 100644
--- a/src/types/pack/provinces.d.ts
+++ b/src/types/pack/provinces.d.ts
@@ -1,8 +1,17 @@
interface IProvince {
i: number;
name: string;
+ burg: number;
+ formName: string;
fullName: string;
+ color: Hex | CssUrls;
+ state: number;
+ center: number;
+ pole: TPoint;
+ coa: ICoa | string;
removed?: boolean;
}
-type TProvinces = IProvince[];
+type TNoProvince = 0;
+
+type TProvinces = [TNoProvince, ...IProvince[]];
diff --git a/src/types/pack/states.d.ts b/src/types/pack/states.d.ts
index 8136e6ce..79dd27cd 100644
--- a/src/types/pack/states.d.ts
+++ b/src/types/pack/states.d.ts
@@ -7,11 +7,11 @@ interface IState {
type: TCultureType;
culture: number;
expansionism: number;
- form: string;
+ form: TStateForm;
formName: string;
fullName: string;
+ pole: TPoint;
coa: ICoa | string;
- // pole: TPoint ?
area: number;
cells: number;
burgs: number;
@@ -38,6 +38,8 @@ interface ICoa {
t1: string;
}
+type TStateForm = "Monarchy" | "Republic" | "Theocracy" | "Union" | "Anarchy";
+
type TRelation =
| "Ally"
| "Friendly"
diff --git a/src/utils/debugUtils.ts b/src/utils/debugUtils.ts
index f72c614f..c973cedf 100644
--- a/src/utils/debugUtils.ts
+++ b/src/utils/debugUtils.ts
@@ -31,6 +31,19 @@ export function drawLine([x1, y1]: TPoint, [x2, y2]: TPoint, {stroke = "#444", s
.attr("stroke-width", strokeWidth);
}
+export function drawPolyline(points: TPoints, {fill = "none", stroke = "#444", strokeWidth = 0.2} = {}) {
+ debug
+ .append("polyline")
+ .attr("points", points.join(" "))
+ .attr("fill", fill)
+ .attr("stroke", stroke)
+ .attr("stroke-width", strokeWidth);
+}
+
+export function drawPath(d: string, {fill = "none", stroke = "#444", strokeWidth = 0.2} = {}) {
+ debug.append("path").attr("d", d).attr("fill", fill).attr("stroke", stroke).attr("stroke-width", strokeWidth);
+}
+
export function drawArrow([x1, y1]: TPoint, [x2, y2]: TPoint, {width = 1, color = "#444"} = {}) {
const normal = getNormal([x1, y1], [x2, y2]);
const [xMid, yMid] = [(x1 + x2) / 2, (y1 + y2) / 2];
diff --git a/src/utils/typeUtils.ts b/src/utils/typeUtils.ts
index 8248144b..f355f139 100644
--- a/src/utils/typeUtils.ts
+++ b/src/utils/typeUtils.ts
@@ -14,3 +14,6 @@ export const isBurg = (burg: TNoBurg | IBurg): burg is IBurg => burg.i !== 0 &&
export const isReligion = (religion: TNoReligion | IReligion): religion is IReligion =>
religion.i !== 0 && !(religion as IReligion).removed;
+
+export const isProvince = (province: TNoProvince | IProvince): province is IProvince =>
+ province !== 0 && !(province as IProvince).removed;
diff --git a/yarn.lock b/yarn.lock
index e5e80b0e..2de78906 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -155,6 +155,11 @@
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-1.2.9.tgz#c7dc78992cd8ca5c850243a265fd257ea56df1fa"
integrity sha512-E/7RgPr2ylT5dWG0CswMi9NpFcjIEDqLcUSBgNHe/EMahfqYaTx4zhcggG3khqoEB/leY4Vl6nTSbwLUPjXceA==
+"@types/d3-array@^3.0.3":
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.0.3.tgz#87d990bf504d14ad6b16766979d04e943c046dac"
+ integrity sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ==
+
"@types/d3-axis@^1":
version "1.0.16"
resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-1.0.16.tgz#93d7a28795c2f8b0e2fd550fcc4d29b7f174e693"
@@ -738,6 +743,13 @@ d3-array@1, d3-array@^1.1.1, d3-array@^1.2.0:
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f"
integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==
+d3-array@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.0.tgz#15bf96cd9b7333e02eb8de8053d78962eafcff14"
+ integrity sha512-3yXFQo0oG3QCxbF06rMPFyGRMGJNS7NvsV1+2joOjbBE+9xvWQ8+GcMJAjRCzw06zQ3/arXeJgbPYcjUCuC+3g==
+ dependencies:
+ internmap "1 - 2"
+
d3-axis@1:
version "1.0.12"
resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-1.0.12.tgz#cdf20ba210cfbb43795af33756886fb3638daac9"
@@ -1469,6 +1481,11 @@ inherits@2, inherits@^2.0.3, inherits@~2.0.3:
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+"internmap@1 - 2":
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
+ integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
+
is-builtin-module@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.1.0.tgz#6fdb24313b1c03b75f8b9711c0feb8c30b903b00"