mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 17:51:24 +01:00
commit
2035b12115
45 changed files with 1525 additions and 700 deletions
47
index.html
47
index.html
|
|
@ -2206,7 +2206,7 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 350">
|
||||
<rect width="100%" height="100%" fill="#005bbb"></rect>
|
||||
<rect y="50%" width="100%" height="50%" fill="#ffd500"></rect>
|
||||
<text x="50%" text-anchor="middle" font-size="9em" y="35%" fill="#f5f5f5">Support Ukraine</text>
|
||||
<text x="50%" text-anchor="middle" font-size="8em" y="32%" fill="#f5f5f5">Support Ukraine</text>
|
||||
<text x="50%" text-anchor="middle" font-size="4em" y="78%" fill="#005bdd">
|
||||
war.ukraine.ua/support-ukraine
|
||||
</text>
|
||||
|
|
@ -2604,8 +2604,8 @@
|
|||
</div>
|
||||
|
||||
<div data-tip="Lake average depth in selected units">
|
||||
<div class="label">Avarage depth:</div>
|
||||
<input id="lakeAvarageDepth" disabled />
|
||||
<div class="label">Average depth:</div>
|
||||
<input id="lakeAverageDepth" disabled />
|
||||
</div>
|
||||
|
||||
<div data-tip="Lake maximum depth in selected units">
|
||||
|
|
@ -3987,11 +3987,6 @@
|
|||
></button>
|
||||
<button id="convertColorsButton" data-tip="Set maximum number of colors" class="icon-signal"></button>
|
||||
<input id="convertColors" value="100" style="display: none" />
|
||||
<button
|
||||
id="convertComplete"
|
||||
data-tip="Complete the conversion. All unassigned colors will be considered as ocean"
|
||||
class="icon-check"
|
||||
></button>
|
||||
<button
|
||||
id="convertCancel"
|
||||
data-tip="Cancel the conversion. Previous heightmap will be restored"
|
||||
|
|
@ -4013,12 +4008,23 @@
|
|||
</div>
|
||||
|
||||
<div data-tip="Select a color to re-assign the height value" id="colorsAssigned" style="display: none">
|
||||
<i>Assigned colors (<span id="colorsAssignedNumber"></span>):</i><br />
|
||||
<i>Assigned colors (<span id="colorsAssignedNumber"></span>):</i>
|
||||
<div id="colorsAssignedContainer" class="colorsContainer"></div>
|
||||
</div>
|
||||
|
||||
<div data-tip="Select a color to assign a height value" id="colorsUnassigned" style="display: none">
|
||||
<i>Unassigned colors (<span id="colorsUnassignedNumber"></span>):</i><br />
|
||||
<i>Unassigned colors (<span id="colorsUnassignedNumber"></span>):</i>
|
||||
<div id="colorsUnassignedContainer" class="colorsContainer"></div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
id="convertComplete"
|
||||
data-tip="Complete the conversion. All unassigned colors will be considered as ocean"
|
||||
style="margin: 0.4em 0"
|
||||
class="glow"
|
||||
>
|
||||
Complete the conversion
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="biomesEditor" class="dialog stable" style="display: none">
|
||||
|
|
@ -5466,19 +5472,19 @@
|
|||
<div data-tip="Set map rotation speed. Set to 0 is you want to toggle off the rotation">
|
||||
<div>Rotation:</div>
|
||||
<input id="options3dMeshRotationRange" type="range" min="0" max="10" step=".1" />
|
||||
<input id="options3dMeshRotationNumber" type="number" min="0" max="10" step=".1" style="width: 3em" />
|
||||
<input id="options3dMeshRotationNumber" type="number" min="0" max="10" step=".1" style="width: 4em" />
|
||||
</div>
|
||||
|
||||
<div data-tip="Set height scale">
|
||||
<div>Height scale:</div>
|
||||
<input id="options3dScaleRange" type="range" min="0" max="100" />
|
||||
<input id="options3dScaleNumber" type="number" min="0" max="1000" style="width: 3em" />
|
||||
<input id="options3dScaleNumber" type="number" min="0" max="1000" style="width: 4em" />
|
||||
</div>
|
||||
|
||||
<div data-tip="Set scene lightness">
|
||||
<div>Lightness:</div>
|
||||
<input id="options3dLightnessRange" type="range" min="0" max="100" />
|
||||
<input id="options3dLightnessNumber" type="number" min="0" max="500" style="width: 3em" />
|
||||
<input id="options3dLightnessNumber" type="number" min="0" max="500" style="width: 4em" />
|
||||
</div>
|
||||
|
||||
<div data-tip="Set sun position (x, y, z) to define shadows">
|
||||
|
|
@ -5518,7 +5524,7 @@
|
|||
<div data-tip="Set globe rotation speed. Set to 0 is you want to toggle off the rotation">
|
||||
<div>Rotation:</div>
|
||||
<input id="options3dGlobeRotationRange" type="range" min="0" max="10" step=".1" />
|
||||
<input id="options3dGlobeRotationNumber" type="number" min="0" max="10" step=".1" style="width: 3em" />
|
||||
<input id="options3dGlobeRotationNumber" type="number" min="0" max="10" step=".1" style="width: 4em" />
|
||||
</div>
|
||||
|
||||
<div data-tip="Set globe texture resolution">
|
||||
|
|
@ -5595,6 +5601,8 @@
|
|||
<input id="pngResolutionOutput" data-stored="pngResolution" type="number" min="1" max="8" value="1" />
|
||||
</div>
|
||||
|
||||
<p>Generator uses pop-up window to download files. Please ensure your browser does not block popups.</p>
|
||||
|
||||
<div style="margin: 1em 0 0.3em; font-weight: bold">Export to GeoJSON</div>
|
||||
<div>
|
||||
<button id="saveGeoJSON_Cells" data-tip="Download cells data in GeoJSON format">cells</button>
|
||||
|
|
@ -5607,7 +5615,6 @@
|
|||
<a href="https://github.com/Azgaar/Fantasy-Map-Generator/wiki/GIS-data-export" target="_blank">wiki-page</a>
|
||||
for guidance.
|
||||
</p>
|
||||
<p>Generator uses pop-up window to download files. Please ensure your browser does not block popups.</p>
|
||||
|
||||
<div style="margin: 1em 0 0.3em; font-weight: bold">Export To JSON</div>
|
||||
<div>
|
||||
|
|
@ -5622,12 +5629,10 @@
|
|||
</div>
|
||||
<p>Export in JSON format can be used as an API replacement.</p>
|
||||
|
||||
<div>
|
||||
<p>
|
||||
It's also possible to export map to <i>Foundry VTT</i>, see
|
||||
<a href="https://github.com/Ethck/azgaar-foundry" target="_blank">the module.</a>
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
It's also possible to export map to <i>Foundry VTT</i>, see
|
||||
<a href="https://github.com/Ethck/azgaar-foundry" target="_blank">the module.</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div id="saveMapData" style="display: none" class="dialog">
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 `<tspan x="${left}px" dy="${d ? 1 : top}em">${l}</tspan>`;
|
||||
})
|
||||
.join("");
|
||||
const inner = lines.map((l, d) => `<tspan x="0" dy="${d ? 1 : top}em">${l}</tspan>`).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");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
306
src/layers/renderers/drawLabels/drawStateLabels.ts
Normal file
306
src/layers/renderers/drawLabels/drawStateLabels.ts
Normal file
|
|
@ -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) => `<tspan x="0" dy="${index ? 1 : top}em">${line}</tspan>`);
|
||||
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 = `<tspan x="0">${text}</tspan>`;
|
||||
|
||||
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;
|
||||
}
|
||||
12
src/layers/renderers/drawLabels/index.ts
Normal file
12
src/layers/renderers/drawLabels/index.ts
Normal file
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
45
src/layers/renderers/drawProvinces.ts
Normal file
45
src/layers/renderers/drawProvinces.ts
Normal file
|
|
@ -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 `<text x="${x}" y="${y}" id="provinceLabel${i}">${name}</text>`;
|
||||
});
|
||||
};
|
||||
|
||||
const htmlPaths = paths.map(([index, {fill, waterGap}]) => {
|
||||
const color = getColor(index);
|
||||
|
||||
return /* html */ `
|
||||
<path d="${waterGap}" fill="none" stroke="${color}" id="province-gap${index}" />
|
||||
<path d="${fill}" fill="${color}" stroke="none" id="province${index}" />
|
||||
`;
|
||||
});
|
||||
|
||||
byId("provs")!.innerHTML = /* html*/ `
|
||||
<g id="provincesBody">
|
||||
${htmlPaths.join("")}
|
||||
</g>
|
||||
<g id="provinceLabels">
|
||||
${getLabels().join("")}
|
||||
</g>
|
||||
`;
|
||||
}
|
||||
|
|
@ -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++) {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"});
|
||||
|
|
|
|||
|
|
@ -97,12 +97,15 @@ export const adjectivalForms = [
|
|||
"Theocracy",
|
||||
"Oligarchy",
|
||||
"Union",
|
||||
"Federation",
|
||||
"Confederation",
|
||||
"Trade Company",
|
||||
"League",
|
||||
"Tetrarchy",
|
||||
"Triumvirate",
|
||||
"Diarchy",
|
||||
"Khanate",
|
||||
"Khaganate",
|
||||
"Horde",
|
||||
"Marches"
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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<number, boolean>,
|
||||
|
|
@ -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<number, TStateData>(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);
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export function specifyStates(
|
|||
statesData: TStateData[],
|
||||
statistics: TStateStatistics,
|
||||
diplomacy: TDiplomacy,
|
||||
poles: Dict<TPoint>,
|
||||
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
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
106
src/scripts/generation/pack/cultures/expandCultures.ts
Normal file
106
src/scripts/generation/pack/cultures/expandCultures.ts
Normal file
|
|
@ -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<IPack["cells"], "c" | "area" | "h" | "t" | "f" | "r" | "fl" | "biome" | "pop">
|
||||
) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IPack["cells"], "c" | "area" | "h" | "t" | "f" | "r" | "fl" | "biome" | "pop">
|
||||
) {
|
||||
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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ export interface ILakeClimateData extends IPackFeatureLake {
|
|||
enteringFlux?: number;
|
||||
}
|
||||
|
||||
export const getClimateData = function (
|
||||
export function getClimateData(
|
||||
lakes: IPackFeatureLake[],
|
||||
heights: Float32Array,
|
||||
drainableLakes: Dict<boolean>,
|
||||
|
|
@ -44,13 +44,9 @@ export const getClimateData = function (
|
|||
});
|
||||
|
||||
return lakeData;
|
||||
};
|
||||
}
|
||||
|
||||
export const mergeLakeData = function (
|
||||
features: TPackFeatures,
|
||||
lakeData: ILakeClimateData[],
|
||||
rivers: Pick<IRiver, "i">[]
|
||||
) {
|
||||
export function mergeLakeData(features: TPackFeatures, lakeData: ILakeClimateData[], rivers: Pick<IRiver, "i">[]) {
|
||||
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,
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
8
src/scripts/generation/pack/provinces/config.ts
Normal file
8
src/scripts/generation/pack/provinces/config.ts
Normal file
|
|
@ -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}
|
||||
};
|
||||
86
src/scripts/generation/pack/provinces/expandProvinces.ts
Normal file
86
src/scripts/generation/pack/provinces/expandProvinces.ts
Normal file
|
|
@ -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<IPack["cells"], "i" | "c" | "h" | "t" | "state" | "burg">
|
||||
) {
|
||||
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;
|
||||
}
|
||||
|
|
@ -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<number> = 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;
|
||||
}
|
||||
39
src/scripts/generation/pack/provinces/generateProvinces.ts
Normal file
39
src/scripts/generation/pack/provinces/generateProvinces.ts
Normal file
|
|
@ -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<IPack["cells"], "i" | "c" | "v" | "h" | "t" | "f" | "culture" | "state" | "burg">
|
||||
): {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};
|
||||
}
|
||||
200
src/scripts/generation/pack/provinces/generateWildProvinces.ts
Normal file
200
src/scripts/generation/pack/provinces/generateWildProvinces.ts
Normal file
|
|
@ -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<IPack["cells"], "i" | "c" | "h" | "t" | "f" | "culture" | "state" | "burg">;
|
||||
}) {
|
||||
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<number, string[]>(
|
||||
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<number>();
|
||||
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<boolean> = {[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;
|
||||
}
|
||||
}
|
||||
20
src/scripts/generation/pack/provinces/specifyProvinces.ts
Normal file
20
src/scripts/generation/pack/provinces/specifyProvinces.ts
Normal file
|
|
@ -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];
|
||||
}
|
||||
79
src/scripts/generation/pack/repackGrid.ts
Normal file
79
src/scripts/generation/pack/repackGrid.ts
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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<IPack["cells"], "h" | "c" | "t">) => {
|
||||
function applyDistanceField({h, c, t}: Pick<IPack["cells"], "h" | "c" | "t">) {
|
||||
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<IPack["cells"], "i" | "c" | "b" | "f">,
|
||||
features: TPackFeatures,
|
||||
initialCellHeights: Float32Array
|
||||
): [Float32Array, Dict<boolean>] {
|
||||
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<boolean> = {}; // 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
148
src/scripts/generation/pack/rivers/resolveDepressions.ts
Normal file
148
src/scripts/generation/pack/rivers/resolveDepressions.ts
Normal file
|
|
@ -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<IPack["cells"], "i" | "c" | "b" | "f">,
|
||||
features: TPackFeatures,
|
||||
initialCellHeights: Float32Array
|
||||
): [Float32Array, Dict<boolean>] {
|
||||
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<boolean> = {}; // 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IPack["cells"], "c" | "p" | "g" | "h" | "t" | "biome" | "burg">;
|
||||
|
||||
|
|
@ -292,44 +292,3 @@ function getRouteSegments(pathCells: number[], connections: Map<string, boolean>
|
|||
|
||||
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;
|
||||
}
|
||||
44
src/scripts/generation/pack/routes/urquhart.ts
Normal file
44
src/scripts/generation/pack/routes/urquhart.ts
Normal file
|
|
@ -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;
|
||||
}
|
||||
66
src/scripts/getPolesOfInaccessibility.ts
Normal file
66
src/scripts/getPolesOfInaccessibility.ts
Normal file
|
|
@ -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<TPoint> = 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<number[][][]> = {};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
9
src/types/pack/burgs.d.ts
vendored
9
src/types/pack/burgs.d.ts
vendored
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
4
src/types/pack/pack.d.ts
vendored
4
src/types/pack/pack.d.ts
vendored
|
|
@ -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()
|
||||
|
|
|
|||
11
src/types/pack/provinces.d.ts
vendored
11
src/types/pack/provinces.d.ts
vendored
|
|
@ -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[]];
|
||||
|
|
|
|||
6
src/types/pack/states.d.ts
vendored
6
src/types/pack/states.d.ts
vendored
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
17
yarn.lock
17
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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue