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">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 350">
|
||||||
<rect width="100%" height="100%" fill="#005bbb"></rect>
|
<rect width="100%" height="100%" fill="#005bbb"></rect>
|
||||||
<rect y="50%" width="100%" height="50%" fill="#ffd500"></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">
|
<text x="50%" text-anchor="middle" font-size="4em" y="78%" fill="#005bdd">
|
||||||
war.ukraine.ua/support-ukraine
|
war.ukraine.ua/support-ukraine
|
||||||
</text>
|
</text>
|
||||||
|
|
@ -2604,8 +2604,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div data-tip="Lake average depth in selected units">
|
<div data-tip="Lake average depth in selected units">
|
||||||
<div class="label">Avarage depth:</div>
|
<div class="label">Average depth:</div>
|
||||||
<input id="lakeAvarageDepth" disabled />
|
<input id="lakeAverageDepth" disabled />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div data-tip="Lake maximum depth in selected units">
|
<div data-tip="Lake maximum depth in selected units">
|
||||||
|
|
@ -3987,11 +3987,6 @@
|
||||||
></button>
|
></button>
|
||||||
<button id="convertColorsButton" data-tip="Set maximum number of colors" class="icon-signal"></button>
|
<button id="convertColorsButton" data-tip="Set maximum number of colors" class="icon-signal"></button>
|
||||||
<input id="convertColors" value="100" style="display: none" />
|
<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
|
<button
|
||||||
id="convertCancel"
|
id="convertCancel"
|
||||||
data-tip="Cancel the conversion. Previous heightmap will be restored"
|
data-tip="Cancel the conversion. Previous heightmap will be restored"
|
||||||
|
|
@ -4013,12 +4008,23 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div data-tip="Select a color to re-assign the height value" id="colorsAssigned" style="display: none">
|
<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>
|
||||||
|
|
||||||
<div data-tip="Select a color to assign a height value" id="colorsUnassigned" style="display: none">
|
<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>
|
</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>
|
||||||
|
|
||||||
<div id="biomesEditor" class="dialog stable" style="display: none">
|
<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 data-tip="Set map rotation speed. Set to 0 is you want to toggle off the rotation">
|
||||||
<div>Rotation:</div>
|
<div>Rotation:</div>
|
||||||
<input id="options3dMeshRotationRange" type="range" min="0" max="10" step=".1" />
|
<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>
|
||||||
|
|
||||||
<div data-tip="Set height scale">
|
<div data-tip="Set height scale">
|
||||||
<div>Height scale:</div>
|
<div>Height scale:</div>
|
||||||
<input id="options3dScaleRange" type="range" min="0" max="100" />
|
<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>
|
||||||
|
|
||||||
<div data-tip="Set scene lightness">
|
<div data-tip="Set scene lightness">
|
||||||
<div>Lightness:</div>
|
<div>Lightness:</div>
|
||||||
<input id="options3dLightnessRange" type="range" min="0" max="100" />
|
<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>
|
||||||
|
|
||||||
<div data-tip="Set sun position (x, y, z) to define shadows">
|
<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 data-tip="Set globe rotation speed. Set to 0 is you want to toggle off the rotation">
|
||||||
<div>Rotation:</div>
|
<div>Rotation:</div>
|
||||||
<input id="options3dGlobeRotationRange" type="range" min="0" max="10" step=".1" />
|
<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>
|
||||||
|
|
||||||
<div data-tip="Set globe texture resolution">
|
<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" />
|
<input id="pngResolutionOutput" data-stored="pngResolution" type="number" min="1" max="8" value="1" />
|
||||||
</div>
|
</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 style="margin: 1em 0 0.3em; font-weight: bold">Export to GeoJSON</div>
|
||||||
<div>
|
<div>
|
||||||
<button id="saveGeoJSON_Cells" data-tip="Download cells data in GeoJSON format">cells</button>
|
<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>
|
<a href="https://github.com/Azgaar/Fantasy-Map-Generator/wiki/GIS-data-export" target="_blank">wiki-page</a>
|
||||||
for guidance.
|
for guidance.
|
||||||
</p>
|
</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 style="margin: 1em 0 0.3em; font-weight: bold">Export To JSON</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -5622,12 +5629,10 @@
|
||||||
</div>
|
</div>
|
||||||
<p>Export in JSON format can be used as an API replacement.</p>
|
<p>Export in JSON format can be used as an API replacement.</p>
|
||||||
|
|
||||||
<div>
|
<p>
|
||||||
<p>
|
It's also possible to export map to <i>Foundry VTT</i>, see
|
||||||
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>
|
||||||
<a href="https://github.com/Ethck/azgaar-foundry" target="_blank">the module.</a>
|
</p>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="saveMapData" style="display: none" class="dialog">
|
<div id="saveMapData" style="display: none" class="dialog">
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
"@rollup/plugin-node-resolve": "^13.3.0",
|
"@rollup/plugin-node-resolve": "^13.3.0",
|
||||||
"@rollup/plugin-replace": "^4.0.0",
|
"@rollup/plugin-replace": "^4.0.0",
|
||||||
"@types/d3": "^5.9.0",
|
"@types/d3": "^5.9.0",
|
||||||
|
"@types/d3-array": "^3.0.3",
|
||||||
"@types/delaunator": "^5.0.0",
|
"@types/delaunator": "^5.0.0",
|
||||||
"@types/jquery": "^3.5.14",
|
"@types/jquery": "^3.5.14",
|
||||||
"@types/jqueryui": "^1.12.16",
|
"@types/jqueryui": "^1.12.16",
|
||||||
|
|
@ -35,6 +36,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"d3": "5.8.0",
|
"d3": "5.8.0",
|
||||||
|
"d3-array": "^3.2.0",
|
||||||
"delaunator": "^5.0.0",
|
"delaunator": "^5.0.0",
|
||||||
"flatqueue": "^2.0.3",
|
"flatqueue": "^2.0.3",
|
||||||
"lineclip": "^1.1.5",
|
"lineclip": "^1.1.5",
|
||||||
|
|
|
||||||
|
|
@ -1190,7 +1190,7 @@ export function open(options) {
|
||||||
.on("click", mapClicked);
|
.on("click", mapClicked);
|
||||||
|
|
||||||
const colors = pallete.map(p => `rgb(${p[0]}, ${p[1]}, ${p[2]})`);
|
const colors = pallete.map(p => `rgb(${p[0]}, ${p[1]}, ${p[2]})`);
|
||||||
d3.select("#colorsUnassigned")
|
d3.select("#colorsUnassignedContainer")
|
||||||
.selectAll("div")
|
.selectAll("div")
|
||||||
.data(colors)
|
.data(colors)
|
||||||
.enter()
|
.enter()
|
||||||
|
|
@ -1253,25 +1253,23 @@ export function open(options) {
|
||||||
this.setAttribute("data-height", height);
|
this.setAttribute("data-height", height);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (selectedColor.parentNode.id === "colorsUnassigned") {
|
if (selectedColor.parentNode.id === "colorsUnassignedContainer") {
|
||||||
colorsAssigned.appendChild(selectedColor);
|
colorsAssignedContainer.appendChild(selectedColor);
|
||||||
colorsAssigned.style.display = "block";
|
colorsAssigned.style.display = "block";
|
||||||
|
|
||||||
byId("colorsUnassignedNumber").innerHTML = colorsUnassigned.childElementCount - 2;
|
byId("colorsUnassignedNumber").innerHTML = colorsUnassignedContainer.childElementCount - 2;
|
||||||
byId("colorsAssignedNumber").innerHTML = colorsAssigned.childElementCount - 2;
|
byId("colorsAssignedNumber").innerHTML = colorsAssignedContainer.childElementCount - 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// auto assign color based on luminosity or hue
|
// auto assign color based on luminosity or hue
|
||||||
function autoAssing(type) {
|
function autoAssing(type) {
|
||||||
let unassigned = colorsUnassigned.querySelectorAll("div");
|
let unassigned = colorsUnassignedContainer.querySelectorAll("div");
|
||||||
if (!unassigned.length) {
|
if (!unassigned.length) {
|
||||||
heightsFromImage(+convertColors.value);
|
heightsFromImage(+convertColors.value);
|
||||||
unassigned = colorsUnassigned.querySelectorAll("div");
|
unassigned = colorsUnassignedContainer.querySelectorAll("div");
|
||||||
if (!unassigned.length) {
|
if (!unassigned.length)
|
||||||
tip("No unassigned colors. Please load an image and click the button again", false, "error");
|
return tip("No unassigned colors. Please load an image and click the button again", false, "error");
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getHeightByHue = function (color) {
|
const getHeightByHue = function (color) {
|
||||||
|
|
@ -1315,18 +1313,18 @@ export function open(options) {
|
||||||
} // if color is already added, remove it
|
} // if color is already added, remove it
|
||||||
el.style.backgroundColor = el.dataset.color = colorTo;
|
el.style.backgroundColor = el.dataset.color = colorTo;
|
||||||
el.dataset.height = height;
|
el.dataset.height = height;
|
||||||
colorsAssigned.appendChild(el);
|
colorsAssignedContainer.appendChild(el);
|
||||||
assinged[height] = true;
|
assinged[height] = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// sort assigned colors by height
|
// sort assigned colors by height
|
||||||
Array.from(colorsAssigned.children)
|
Array.from(colorsAssignedContainer.children)
|
||||||
.sort((a, b) => +a.dataset.height - +b.dataset.height)
|
.sort((a, b) => +a.dataset.height - +b.dataset.height)
|
||||||
.forEach(line => colorsAssigned.appendChild(line));
|
.forEach(line => colorsAssignedContainer.appendChild(line));
|
||||||
|
|
||||||
colorsAssigned.style.display = "block";
|
colorsAssigned.style.display = "block";
|
||||||
colorsUnassigned.style.display = "none";
|
colorsUnassigned.style.display = "none";
|
||||||
byId("colorsAssignedNumber").innerHTML = colorsAssigned.childElementCount - 2;
|
byId("colorsAssignedNumber").innerHTML = colorsAssignedContainer.childElementCount - 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setConvertColorsNumber() {
|
function setConvertColorsNumber() {
|
||||||
|
|
@ -1346,7 +1344,8 @@ export function open(options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyConversion() {
|
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
|
viewbox
|
||||||
.select("#heights")
|
.select("#heights")
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ export function open({el}) {
|
||||||
closeDialogs();
|
closeDialogs();
|
||||||
if (!layerIsOn("toggleLabels")) toggleLayer("toggleLabels");
|
if (!layerIsOn("toggleLabels")) toggleLayer("toggleLabels");
|
||||||
|
|
||||||
|
const lineGen = d3.line().curve(d3.curveBundle.beta(1));
|
||||||
|
|
||||||
const textPath = el.parentNode;
|
const textPath = el.parentNode;
|
||||||
const text = textPath.parentNode;
|
const text = textPath.parentNode;
|
||||||
elSelected = d3.select(text).call(d3.drag().on("start", dragLabel)).classed("draggable", true);
|
elSelected = d3.select(text).call(d3.drag().on("start", dragLabel)).classed("draggable", true);
|
||||||
|
|
@ -123,8 +125,6 @@ export function open({el}) {
|
||||||
redrawLabelPath();
|
redrawLabelPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
const lineGen = d3.line().curve(d3.curveBundle.beta(1));
|
|
||||||
|
|
||||||
function redrawLabelPath() {
|
function redrawLabelPath() {
|
||||||
const path = byId("textPath_" + elSelected.attr("id"));
|
const path = byId("textPath_" + elSelected.attr("id"));
|
||||||
const points = [];
|
const points = [];
|
||||||
|
|
@ -308,26 +308,12 @@ export function open({el}) {
|
||||||
function changeText() {
|
function changeText() {
|
||||||
const input = byId("labelText").value;
|
const input = byId("labelText").value;
|
||||||
const el = elSelected.select("textPath").node();
|
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 lines = input.split("|");
|
||||||
const top = (lines.length - 1) / -2; // y offset
|
const top = (lines.length - 1) / -2; // y offset
|
||||||
const inner = lines
|
const inner = lines.map((l, d) => `<tspan x="0" dy="${d ? 1 : top}em">${l}</tspan>`).join("");
|
||||||
.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("");
|
|
||||||
|
|
||||||
el.innerHTML = inner;
|
el.innerHTML = inner;
|
||||||
example.remove();
|
|
||||||
|
|
||||||
if (elSelected.attr("id").slice(0, 10) === "stateLabel")
|
if (elSelected.attr("id").slice(0, 10) === "stateLabel")
|
||||||
tip("Use States Editor to change an actual state name, not just a label", false, "warning");
|
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]);
|
const heights = lakeCells.map(i => cells.h[i]);
|
||||||
|
|
||||||
byId("lakeElevation").value = getHeight(l.height);
|
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("lakeMaxDepth").value = getHeight(d3.min(heights), true);
|
||||||
|
|
||||||
byId("lakeFlux").value = l.flux;
|
byId("lakeFlux").value = l.flux;
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,7 @@ a {
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#provincesBody,
|
||||||
#statesBody {
|
#statesBody {
|
||||||
stroke-width: 3;
|
stroke-width: 3;
|
||||||
}
|
}
|
||||||
|
|
@ -179,10 +180,6 @@ a {
|
||||||
stroke-linejoin: round;
|
stroke-linejoin: round;
|
||||||
}
|
}
|
||||||
|
|
||||||
#provincesBody {
|
|
||||||
stroke-width: 0.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
#statesBody,
|
#statesBody,
|
||||||
#provincesBody,
|
#provincesBody,
|
||||||
#relig,
|
#relig,
|
||||||
|
|
@ -243,7 +240,7 @@ i.icon-lock {
|
||||||
}
|
}
|
||||||
|
|
||||||
#labels {
|
#labels {
|
||||||
text-anchor: start;
|
text-anchor: middle;
|
||||||
dominant-baseline: central;
|
dominant-baseline: central;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
@ -1144,12 +1141,17 @@ div#regimentSelectorBody > div > div {
|
||||||
filter: sepia(1) hue-rotate(200deg);
|
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 {
|
.color-div {
|
||||||
width: 3em;
|
width: 3em;
|
||||||
height: 1em;
|
height: 1.5em;
|
||||||
display: inline-block;
|
border: 1px #999 solid;
|
||||||
margin: 0 0.16em;
|
|
||||||
border: 1px #c5c5c5 groove;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import * as d3 from "d3";
|
import * as d3 from "d3";
|
||||||
|
|
||||||
import {getProvincesVertices} from "./drawProvinces";
|
|
||||||
import {minmax, rn} from "utils/numberUtils";
|
import {minmax, rn} from "utils/numberUtils";
|
||||||
import {byId} from "utils/shorthands";
|
import {byId} from "utils/shorthands";
|
||||||
|
|
||||||
|
|
@ -42,7 +41,7 @@ export function drawEmblems() {
|
||||||
|
|
||||||
const sizeProvinces = getProvinceEmblemsSize();
|
const sizeProvinces = getProvinceEmblemsSize();
|
||||||
const provinceCOAs = validProvinces.map(province => {
|
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 [x, y] = province.pole || pack.cells.p[province.center];
|
||||||
const size = province.coaSize || 1;
|
const size = province.coaSize || 1;
|
||||||
const shift = (sizeProvinces * size) / 2;
|
const shift = (sizeProvinces * size) / 2;
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,11 @@
|
||||||
export function drawLabels() {
|
import * as d3 from "d3";
|
||||||
drawBurgLabels();
|
|
||||||
// TODO: draw other labels
|
|
||||||
|
|
||||||
window.Zoom.invoke();
|
export function drawBurgLabels(burgs: TBurgs) {
|
||||||
}
|
|
||||||
|
|
||||||
function drawBurgLabels() {
|
|
||||||
// remove old data
|
// remove old data
|
||||||
|
const burgLabels = d3.select("#burgLabels");
|
||||||
burgLabels.selectAll("text").remove();
|
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
|
// capitals
|
||||||
const capitals = validBurgs.filter(burg => burg.capital);
|
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
|
valid.forEach(s => (s.diplomacy = new Array(states.length).fill("x"))); // clear all relationships
|
||||||
if (valid.length < 2) return; // no states to renerate relations with
|
if (valid.length < 2) return; // no states to renerate relations with
|
||||||
const areaMean = d3.mean(valid.map(s => s.area)); // avarage state area
|
const areaMean = d3.mean(valid.map(s => s.area)); // average state area
|
||||||
|
|
||||||
// generic relations
|
// generic relations
|
||||||
for (let f = 1; f < states.length; f++) {
|
for (let f = 1; f < states.length; f++) {
|
||||||
|
|
|
||||||
|
|
@ -75,10 +75,8 @@ export function editNamesbase() {
|
||||||
|
|
||||||
function updateInputs() {
|
function updateInputs() {
|
||||||
const base = +document.getElementById("namesbaseSelect").value;
|
const base = +document.getElementById("namesbaseSelect").value;
|
||||||
if (!nameBases[base]) {
|
if (!nameBases[base]) return tip(`Namesbase ${base} is not defined`, false, "error");
|
||||||
tip(`Namesbase ${base} is not defined`, false, "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
document.getElementById("namesbaseTextarea").value = nameBases[base].b;
|
document.getElementById("namesbaseTextarea").value = nameBases[base].b;
|
||||||
document.getElementById("namesbaseName").value = nameBases[base].name;
|
document.getElementById("namesbaseName").value = nameBases[base].name;
|
||||||
document.getElementById("namesbaseMin").value = nameBases[base].min;
|
document.getElementById("namesbaseMin").value = nameBases[base].min;
|
||||||
|
|
@ -104,20 +102,23 @@ export function editNamesbase() {
|
||||||
|
|
||||||
function updateNamesData() {
|
function updateNamesData() {
|
||||||
const base = +document.getElementById("namesbaseSelect").value;
|
const base = +document.getElementById("namesbaseSelect").value;
|
||||||
const b = document.getElementById("namesbaseTextarea").value;
|
const rawInput = document.getElementById("namesbaseTextarea").value;
|
||||||
if (b.split(",").length < 3) {
|
if (rawInput.split(",").length < 3) return tip("The names data provided is too short of incorrect", false, "error");
|
||||||
tip("The names data provided is too short of incorrect", false, "error");
|
|
||||||
return;
|
const namesData = rawInput.replace(/[/|]/g, "");
|
||||||
}
|
nameBases[base].b = namesData;
|
||||||
nameBases[base].b = b;
|
|
||||||
Names.updateChain(base);
|
Names.updateChain(base);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateBaseName() {
|
function updateBaseName() {
|
||||||
const base = +document.getElementById("namesbaseSelect").value;
|
const base = +document.getElementById("namesbaseSelect").value;
|
||||||
const select = document.getElementById("namesbaseSelect");
|
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() {
|
function updateBaseMin() {
|
||||||
|
|
|
||||||
|
|
@ -485,10 +485,17 @@ function changeDialogsTheme(themeColor, transparency) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeZoomExtent(value) {
|
function changeZoomExtent(value) {
|
||||||
const min = Math.max(+byId("zoomExtentMin").value, 0.01);
|
const zoomExtentMin = byId("zoomExtentMin");
|
||||||
const max = Math.min(+byId("zoomExtentMax").value, 200);
|
const zoomExtentMax = byId("zoomExtentMax");
|
||||||
Zoom.scaleExtent([min, max]);
|
|
||||||
|
|
||||||
|
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);
|
const scale = minmax(+value, 0.01, 200);
|
||||||
Zoom.scaleTo(svg, scale);
|
Zoom.scaleTo(svg, scale);
|
||||||
}
|
}
|
||||||
|
|
@ -1056,6 +1063,7 @@ export function toggle3dOptions() {
|
||||||
const globe = byId("canvas3d").dataset.type === "viewGlobe";
|
const globe = byId("canvas3d").dataset.type === "viewGlobe";
|
||||||
options3dMesh.style.display = globe ? "none" : "block";
|
options3dMesh.style.display = globe ? "none" : "block";
|
||||||
options3dGlobe.style.display = globe ? "block" : "none";
|
options3dGlobe.style.display = globe ? "block" : "none";
|
||||||
|
options3dOBJSave.style.display = globe ? "none" : "inline-block";
|
||||||
options3dScaleRange.value = options3dScaleNumber.value = ThreeD.options.scale;
|
options3dScaleRange.value = options3dScaleNumber.value = ThreeD.options.scale;
|
||||||
options3dLightnessRange.value = options3dLightnessNumber.value = ThreeD.options.lightness * 100;
|
options3dLightnessRange.value = options3dLightnessNumber.value = ThreeD.options.lightness * 100;
|
||||||
options3dSunX.value = ThreeD.options.sun.x;
|
options3dSunX.value = ThreeD.options.sun.x;
|
||||||
|
|
|
||||||
|
|
@ -67,10 +67,10 @@ async function generate(options?: IGenerationOptions) {
|
||||||
// renderLayer("heightmap");
|
// renderLayer("heightmap");
|
||||||
// renderLayer("rivers");
|
// renderLayer("rivers");
|
||||||
// renderLayer("biomes");
|
// renderLayer("biomes");
|
||||||
renderLayer("burgs");
|
// renderLayer("burgs");
|
||||||
renderLayer("routes");
|
// renderLayer("routes");
|
||||||
renderLayer("states");
|
renderLayer("states");
|
||||||
// renderLayer("religions");
|
renderLayer("labels");
|
||||||
|
|
||||||
// pack.cells.route.forEach((route, index) => {
|
// pack.cells.route.forEach((route, index) => {
|
||||||
// if (route === 2) drawPoint(pack.cells.p[index], {color: "black"});
|
// if (route === 2) drawPoint(pack.cells.p[index], {color: "black"});
|
||||||
|
|
|
||||||
|
|
@ -97,12 +97,15 @@ export const adjectivalForms = [
|
||||||
"Theocracy",
|
"Theocracy",
|
||||||
"Oligarchy",
|
"Oligarchy",
|
||||||
"Union",
|
"Union",
|
||||||
|
"Federation",
|
||||||
"Confederation",
|
"Confederation",
|
||||||
"Trade Company",
|
"Trade Company",
|
||||||
"League",
|
"League",
|
||||||
"Tetrarchy",
|
"Tetrarchy",
|
||||||
"Triumvirate",
|
"Triumvirate",
|
||||||
"Diarchy",
|
"Diarchy",
|
||||||
|
"Khanate",
|
||||||
|
"Khaganate",
|
||||||
"Horde",
|
"Horde",
|
||||||
"Marches"
|
"Marches"
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ export function defineStateForm(
|
||||||
const generic = {Monarchy: 25, Republic: 2, Union: 1};
|
const generic = {Monarchy: 25, Republic: 2, Union: 1};
|
||||||
const naval = {Monarchy: 6, 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
|
const isAnarchy = P((1 - areaTier / 5) / 100); // [1% - 0.2%] chance
|
||||||
if (isAnarchy) return "Anarchy";
|
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 {ELEVATION, FOREST_BIOMES, MIN_LAND_HEIGHT, DISTANCE_FIELD} from "config/generation";
|
||||||
import type {TStateData} from "./createStateData";
|
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
|
// growth algorithm to assign cells to states
|
||||||
export function expandStates(
|
export function expandStates(
|
||||||
capitalCells: Map<number, boolean>,
|
capitalCells: Map<number, boolean>,
|
||||||
|
|
@ -30,39 +66,6 @@ export function expandStates(
|
||||||
queue.push({cellId, stateId}, 0);
|
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]));
|
const statesMap = new Map<number, TStateData>(statesData.map(stateData => [stateData.i, stateData]));
|
||||||
|
|
||||||
while (queue.length) {
|
while (queue.length) {
|
||||||
|
|
@ -100,7 +103,7 @@ export function expandStates(
|
||||||
return normalizeStates(stateIds, capitalCells, cells.c, cells.h);
|
return normalizeStates(stateIds, capitalCells, cells.c, cells.h);
|
||||||
|
|
||||||
function getCultureCost(cellId: number, stateCulture: number) {
|
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) {
|
function getPopulationCost(cellId: number) {
|
||||||
|
|
@ -108,19 +111,19 @@ export function expandStates(
|
||||||
if (isWater) return 0;
|
if (isWater) return 0;
|
||||||
|
|
||||||
const suitability = cells.s[cellId];
|
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) {
|
function getBiomeCost(cellId: number, capitalBiome: number, type: TCultureType) {
|
||||||
const biome = cells.biome[cellId];
|
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];
|
const defaultCost = biomesData.cost[biome];
|
||||||
if (type === "Hunting") return defaultCost * HUNTERS_NON_NATIVE_BIOME_FEE_MULTIPLIER;
|
if (type === "Hunting") return defaultCost * multipliers.HUNTERS_NON_NATIVE_BIOME;
|
||||||
if (type === "Nomadic" && FOREST_BIOMES.includes(biome)) return defaultCost * NOMADS_FOREST_BIOMES_FEE_MULTIPLIER;
|
if (type === "Nomadic" && FOREST_BIOMES.includes(biome)) return defaultCost * multipliers.NOMADS_FOREST_BIOMES;
|
||||||
return defaultCost * GENERIC_NON_NATIVE_BIOME_FEE_MULTIPLIER;
|
return defaultCost * multipliers.GENERIC_NON_NATIVE_BIOME;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHeightCost(cellId: number, type: TCultureType) {
|
function getHeightCost(cellId: number, type: TCultureType) {
|
||||||
|
|
@ -131,12 +134,12 @@ export function expandStates(
|
||||||
const feature = features[cells.f[cellId]];
|
const feature = features[cells.f[cellId]];
|
||||||
if (feature === 0) throw new Error(`No feature for cell ${cellId}`);
|
if (feature === 0) throw new Error(`No feature for cell ${cellId}`);
|
||||||
const isDeepWater = cells.t[cellId] > DISTANCE_FIELD.WATER_COAST;
|
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 === "Lake" && feature.type === "lake") return costs.LAKE_STATES_LAKE_CROSSING * multiplier;
|
||||||
if (type === "Naval") return NAVAL_WATER_CROSSING_FEE * multiplier;
|
if (type === "Naval") return costs.NAVAL_WATER_CROSSING * multiplier;
|
||||||
if (type === "Nomadic") return NOMADS_WATER_CROSSING_FEE * multiplier;
|
if (type === "Nomadic") return costs.NOMADS_WATER_CROSSING * multiplier;
|
||||||
return GENERIC_WATER_CROSSING_FEE * multiplier;
|
return costs.GENERIC_WATER_CROSSING * multiplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLowlands = height <= ELEVATION.FOOTHILLS;
|
const isLowlands = height <= ELEVATION.FOOTHILLS;
|
||||||
|
|
@ -144,22 +147,22 @@ export function expandStates(
|
||||||
const isMountains = height >= ELEVATION.MOUNTAINS;
|
const isMountains = height >= ELEVATION.MOUNTAINS;
|
||||||
|
|
||||||
if (type === "Highland") {
|
if (type === "Highland") {
|
||||||
if (isLowlands) return HIGHLAND_STATE_LOWLANDS_FEE;
|
if (isLowlands) return costs.HIGHLAND_STATE_LOWLANDS;
|
||||||
return HIGHLAND_STATE_HIGHTLAND_COST;
|
return costs.HIGHLAND_STATE_HIGHTLAND;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMountains) return GENERIC_MOUNTAINS_CROSSING_FEE;
|
if (isMountains) return costs.GENERIC_MOUNTAINS_CROSSING;
|
||||||
if (isHills) return GENERIC_HILLS_CROSSING_FEE;
|
if (isHills) return costs.GENERIC_HILLS_CROSSING;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRiverCost(cellId: number, type: TCultureType) {
|
function getRiverCost(cellId: number, type: TCultureType) {
|
||||||
const isRiver = cells.r[cellId] !== 0;
|
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;
|
if (!isRiver) return 0;
|
||||||
|
|
||||||
const flux = cells.fl[cellId];
|
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) {
|
function getTypeCost(cellId: number, type: TCultureType) {
|
||||||
|
|
@ -168,15 +171,15 @@ export function expandStates(
|
||||||
|
|
||||||
const isLandCoast = t === DISTANCE_FIELD.LAND_COAST;
|
const isLandCoast = t === DISTANCE_FIELD.LAND_COAST;
|
||||||
if (isLandCoast) {
|
if (isLandCoast) {
|
||||||
if (isMaritime) return MARITIME_LAND_COAST_FEE;
|
if (isMaritime) return costs.MARITIME_LAND_COAST;
|
||||||
if (type === "Nomadic") return NOMADS_LAND_COAST_FEE;
|
if (type === "Nomadic") return costs.NOMADS_LAND_COAST;
|
||||||
return GENERIC_LAND_COAST_FEE;
|
return costs.GENERIC_LAND_COAST;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLandlocked = t === DISTANCE_FIELD.LANDLOCKED;
|
const isLandlocked = t === DISTANCE_FIELD.LANDLOCKED;
|
||||||
if (isLandlocked) {
|
if (isLandlocked) {
|
||||||
if (type === "Naval") return NAVAL_LANDLOCKED_FEE;
|
if (type === "Naval") return costs.NAVAL_LANDLOCKED;
|
||||||
return GENERIC_LANDLOCKED_FEE;
|
return costs.GENERIC_LANDLOCKED;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -193,7 +196,7 @@ function normalizeStates(
|
||||||
|
|
||||||
const normalizedStateIds = Uint16Array.from(stateIds);
|
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;
|
if (heights[cellId] < MIN_LAND_HEIGHT) continue;
|
||||||
|
|
||||||
const neibs = neibCells[cellId].filter(neib => heights[neib] >= MIN_LAND_HEIGHT);
|
const neibs = neibCells[cellId].filter(neib => heights[neib] >= MIN_LAND_HEIGHT);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import {WARN} from "config/logging";
|
import {WARN} from "config/logging";
|
||||||
|
import {getPolesOfInaccessibility} from "scripts/getPolesOfInaccessibility";
|
||||||
import {pick} from "utils/functionUtils";
|
import {pick} from "utils/functionUtils";
|
||||||
import {getInputNumber} from "utils/nodeUtils";
|
import {getInputNumber} from "utils/nodeUtils";
|
||||||
import {collectStatistics} from "./collectStatistics";
|
import {collectStatistics} from "./collectStatistics";
|
||||||
|
|
@ -70,7 +71,13 @@ export function generateBurgsAndStates(
|
||||||
|
|
||||||
const statistics = collectStatistics({...cells, state: stateIds, burg: burgIds}, burgs);
|
const statistics = collectStatistics({...cells, state: stateIds, burg: burgIds}, burgs);
|
||||||
const diplomacy = generateRelations(statesData, statistics, pick(cells, "f"));
|
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};
|
return {burgIds, stateIds, burgs, states, conflicts};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,24 +32,27 @@ export function specifyBurgs(
|
||||||
|
|
||||||
const burgs = [...capitals, ...towns].map((burgData, index) => {
|
const burgs = [...capitals, ...towns].map((burgData, index) => {
|
||||||
const {cell, culture, capital} = burgData;
|
const {cell, culture, capital} = burgData;
|
||||||
|
const isCapital = Boolean(capital);
|
||||||
const state = stateIds[cell];
|
const state = stateIds[cell];
|
||||||
|
|
||||||
const port = definePort(cell, capital);
|
const port = definePort(cell, isCapital);
|
||||||
const population = definePopulation(cell, capital, port);
|
const population = definePopulation(cell, isCapital, port);
|
||||||
const [x, y] = defineLocation(cell, port);
|
const [x, y] = defineLocation(cell, port);
|
||||||
|
|
||||||
const type = defineType(cell, port, population);
|
const type = defineType(cell, port, population);
|
||||||
const stateData = stateDataMap.get(state)!;
|
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;
|
return burg;
|
||||||
});
|
});
|
||||||
|
|
||||||
TIME && console.timeEnd("specifyBurgs");
|
TIME && console.timeEnd("specifyBurgs");
|
||||||
return [NO_BURG, ...burgs];
|
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 (!cells.haven[cellId]) return 0; // must be a coastal cell
|
||||||
if (temp[cells.g[cellId]] <= 0) return 0; // temperature must be above zero °C
|
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
|
if (feature.cells < 2) return 0; // water body must have at least 2 cells
|
||||||
|
|
||||||
const isSafeHarbor = cells.harbor[cellId] === 1;
|
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;
|
return havenFeatureId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get population in points, where 1 point = 1000 people by default
|
// 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 basePopulation = cells.s[cellId] / 4;
|
||||||
const decimalPart = (cellId % 1000) / 1000;
|
const decimalPart = (cellId % 1000) / 1000;
|
||||||
|
|
||||||
const capitalMultiplier = capital ? 1.3 : 1;
|
const capitalMultiplier = isCapital ? 1.3 : 1;
|
||||||
const portMultiplier = port ? 1.3 : 1;
|
const portMultiplier = port ? 1.3 : 1;
|
||||||
const randomMultiplier = gauss(1, 1.5, 0.3, 10, 3);
|
const randomMultiplier = gauss(1, 1.5, 0.3, 10, 3);
|
||||||
|
|
||||||
|
|
@ -123,12 +126,12 @@ export function specifyBurgs(
|
||||||
function defineEmblem(
|
function defineEmblem(
|
||||||
cultureId: number,
|
cultureId: number,
|
||||||
port: number,
|
port: number,
|
||||||
capital: Logical,
|
isCapital: boolean,
|
||||||
type: TCultureType,
|
type: TCultureType,
|
||||||
cultures: TCultures,
|
cultures: TCultures,
|
||||||
stateData: TStateData
|
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;
|
const cultureShield = cultures[cultureId].shield;
|
||||||
|
|
||||||
if (!stateData) {
|
if (!stateData) {
|
||||||
|
|
@ -147,11 +150,29 @@ export function specifyBurgs(
|
||||||
|
|
||||||
function defineKinshipToStateEmblem() {
|
function defineKinshipToStateEmblem() {
|
||||||
const baseKinship = 0.25;
|
const baseKinship = 0.25;
|
||||||
const capitalModifier = capital ? 0.1 : 0;
|
const capitalModifier = isCapital ? 0.1 : 0;
|
||||||
const portModifier = port ? -0.1 : 0;
|
const portModifier = port ? -0.1 : 0;
|
||||||
const cultureModifier = cultureId === stateCultureId ? 0 : -0.25;
|
const cultureModifier = cultureId === stateCultureId ? 0 : -0.25;
|
||||||
|
|
||||||
return baseKinship + capitalModifier + portModifier + cultureModifier;
|
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[],
|
statesData: TStateData[],
|
||||||
statistics: TStateStatistics,
|
statistics: TStateStatistics,
|
||||||
diplomacy: TDiplomacy,
|
diplomacy: TDiplomacy,
|
||||||
|
poles: Dict<TPoint>,
|
||||||
cultures: TCultures,
|
cultures: TCultures,
|
||||||
burgs: TBurgs
|
burgs: TBurgs
|
||||||
): {states: TStates; conflicts: IConflict[]} {
|
): {states: TStates; conflicts: IConflict[]} {
|
||||||
|
|
@ -41,6 +42,8 @@ export function specifyStates(
|
||||||
const name = defineStateName(center, capitalName, nameBase, formName);
|
const name = defineStateName(center, capitalName, nameBase, formName);
|
||||||
const fullName = defineFullStateName(name, formName);
|
const fullName = defineFullStateName(name, formName);
|
||||||
|
|
||||||
|
const pole = poles[i];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
...stateData,
|
...stateData,
|
||||||
|
|
@ -52,7 +55,8 @@ export function specifyStates(
|
||||||
burgs: burgsNumber,
|
burgs: burgsNumber,
|
||||||
...stats,
|
...stats,
|
||||||
neighbors,
|
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 * as d3 from "d3";
|
||||||
import FlatQueue from "flatqueue";
|
|
||||||
|
|
||||||
import {cultureSets, DEFAULT_SORT_STRING, TCultureSetName} from "config/cultureSets";
|
import {cultureSets, DEFAULT_SORT_STRING, TCultureSetName} from "config/cultureSets";
|
||||||
import {
|
import {DISTANCE_FIELD, ELEVATION, HUNTING_BIOMES, NOMADIC_BIOMES} from "config/generation";
|
||||||
DISTANCE_FIELD,
|
|
||||||
ELEVATION,
|
|
||||||
FOREST_BIOMES,
|
|
||||||
HUNTING_BIOMES,
|
|
||||||
MIN_LAND_HEIGHT,
|
|
||||||
NOMADIC_BIOMES
|
|
||||||
} from "config/generation";
|
|
||||||
import {ERROR, TIME, WARN} from "config/logging";
|
import {ERROR, TIME, WARN} from "config/logging";
|
||||||
import {getColors} from "utils/colorUtils";
|
import {getColors} from "utils/colorUtils";
|
||||||
import {abbreviate} from "utils/languageUtils";
|
import {abbreviate} from "utils/languageUtils";
|
||||||
import {getInputNumber, getInputValue, getSelectedOption} from "utils/nodeUtils";
|
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 {biased, P, rand} from "utils/probabilityUtils";
|
||||||
import {byId} from "utils/shorthands";
|
import {byId} from "utils/shorthands";
|
||||||
import {defaultNameBases} from "config/namebases";
|
import {defaultNameBases} from "config/namebases";
|
||||||
import {isCulture} from "utils/typeUtils";
|
|
||||||
|
|
||||||
const {COA} = window;
|
const {COA} = window;
|
||||||
|
|
||||||
|
|
@ -33,19 +24,17 @@ const cultureTypeBaseExpansionism: {[key in TCultureType]: number} = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const {MOUNTAINS, HILLS} = ELEVATION;
|
const {MOUNTAINS, HILLS} = ELEVATION;
|
||||||
const {LAND_COAST, LANDLOCKED, WATER_COAST} = DISTANCE_FIELD;
|
const {LAND_COAST, LANDLOCKED} = DISTANCE_FIELD;
|
||||||
|
|
||||||
export const generateCultures = function (
|
type TCellsData = Pick<
|
||||||
features: TPackFeatures,
|
IPack["cells"],
|
||||||
cells: Pick<
|
"p" | "i" | "g" | "t" | "h" | "haven" | "harbor" | "f" | "r" | "fl" | "s" | "pop" | "biome"
|
||||||
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 {
|
||||||
temp: Int8Array
|
|
||||||
): TCultures {
|
|
||||||
TIME && console.time("generateCultures");
|
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 populatedCellIds = cells.i.filter(cellId => cells.pop[cellId] > 0);
|
||||||
const maxSuitability = d3.max(cells.s)!;
|
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`);
|
ERROR && console.error(`Name base ${base} is not available, applying a fallback one`);
|
||||||
return base % nameBases.length;
|
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;
|
enteringFlux?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getClimateData = function (
|
export function getClimateData(
|
||||||
lakes: IPackFeatureLake[],
|
lakes: IPackFeatureLake[],
|
||||||
heights: Float32Array,
|
heights: Float32Array,
|
||||||
drainableLakes: Dict<boolean>,
|
drainableLakes: Dict<boolean>,
|
||||||
|
|
@ -44,13 +44,9 @@ export const getClimateData = function (
|
||||||
});
|
});
|
||||||
|
|
||||||
return lakeData;
|
return lakeData;
|
||||||
};
|
}
|
||||||
|
|
||||||
export const mergeLakeData = function (
|
export function mergeLakeData(features: TPackFeatures, lakeData: ILakeClimateData[], rivers: Pick<IRiver, "i">[]) {
|
||||||
features: TPackFeatures,
|
|
||||||
lakeData: ILakeClimateData[],
|
|
||||||
rivers: Pick<IRiver, "i">[]
|
|
||||||
) {
|
|
||||||
const updatedFeatures = features.map(feature => {
|
const updatedFeatures = features.map(feature => {
|
||||||
if (!feature) return 0;
|
if (!feature) return 0;
|
||||||
if (feature.type !== "lake") return feature;
|
if (feature.type !== "lake") return feature;
|
||||||
|
|
@ -71,7 +67,7 @@ export const mergeLakeData = function (
|
||||||
});
|
});
|
||||||
|
|
||||||
return updatedFeatures as TPackFeatures;
|
return updatedFeatures as TPackFeatures;
|
||||||
};
|
}
|
||||||
|
|
||||||
function defineLakeGroup({
|
function defineLakeGroup({
|
||||||
firstCell,
|
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 {markupPackFeatures} from "scripts/generation/markup";
|
||||||
import {rankCells} from "scripts/generation/pack/rankCells";
|
import {rankCells} from "scripts/generation/pack/rankCells";
|
||||||
import {createTypedArray} from "utils/arrayUtils";
|
|
||||||
import {pick} from "utils/functionUtils";
|
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 {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 {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;
|
const {Biomes} = window;
|
||||||
|
|
||||||
export function createPack(grid: IGrid): IPack {
|
export function createPack(grid: IGrid): IPack {
|
||||||
|
|
@ -149,12 +143,17 @@ export function createPack(grid: IGrid): IPack {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// BurgsAndStates.generateProvinces();
|
const {provinceIds, provinces} = generateProvinces(states, burgs, cultures, mergedFeatures, vertices, {
|
||||||
// BurgsAndStates.defineBurgFeatures();
|
i: cells.i,
|
||||||
|
c: cells.c,
|
||||||
// renderLayer("states");
|
v: cells.v,
|
||||||
// renderLayer("borders");
|
h: heights,
|
||||||
// BurgsAndStates.drawStateLabels();
|
t: distanceField,
|
||||||
|
f: featureIds,
|
||||||
|
culture: cultureIds,
|
||||||
|
state: stateIds,
|
||||||
|
burg: burgIds
|
||||||
|
});
|
||||||
|
|
||||||
// Rivers.specify();
|
// Rivers.specify();
|
||||||
// const updatedFeatures = generateLakeNames();
|
// const updatedFeatures = generateLakeNames();
|
||||||
|
|
@ -190,7 +189,7 @@ export function createPack(grid: IGrid): IPack {
|
||||||
state: stateIds,
|
state: stateIds,
|
||||||
route: cellRoutes,
|
route: cellRoutes,
|
||||||
religion: religionIds,
|
religion: religionIds,
|
||||||
province: new Uint16Array(cells.i.length)
|
province: provinceIds
|
||||||
},
|
},
|
||||||
features: mergedFeatures,
|
features: mergedFeatures,
|
||||||
rivers: rawRivers, // "name" | "basin" | "type"
|
rivers: rawRivers, // "name" | "basin" | "type"
|
||||||
|
|
@ -199,77 +198,9 @@ export function createPack(grid: IGrid): IPack {
|
||||||
burgs,
|
burgs,
|
||||||
routes,
|
routes,
|
||||||
religions,
|
religions,
|
||||||
|
provinces,
|
||||||
events
|
events
|
||||||
};
|
};
|
||||||
|
|
||||||
return pack;
|
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 * as d3 from "d3";
|
||||||
|
|
||||||
import {INFO, TIME, WARN} from "config/logging";
|
import {TIME} from "config/logging";
|
||||||
import {rn} from "utils/numberUtils";
|
import {rn} from "utils/numberUtils";
|
||||||
import {aleaPRNG} from "scripts/aleaPRNG";
|
import {aleaPRNG} from "scripts/aleaPRNG";
|
||||||
import {DISTANCE_FIELD, MAX_HEIGHT, MIN_LAND_HEIGHT} from "config/generation";
|
import {DISTANCE_FIELD, MIN_LAND_HEIGHT} from "config/generation";
|
||||||
import {getInputNumber} from "utils/nodeUtils";
|
|
||||||
import {pick} from "utils/functionUtils";
|
import {pick} from "utils/functionUtils";
|
||||||
import {byId} from "utils/shorthands";
|
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 {Rivers} = window;
|
||||||
const {LAND_COAST} = DISTANCE_FIELD;
|
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
|
// 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) => {
|
return new Float32Array(h.length).map((_, index) => {
|
||||||
if (h[index] < MIN_LAND_HEIGHT || t[index] < LAND_COAST) return h[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;
|
const mean = d3.mean(c[index].map(c => t[c])) || 0;
|
||||||
return h[index] + t[index] / 100 + mean / 10000;
|
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 FlatQueue from "flatqueue";
|
||||||
|
|
||||||
import {TIME} from "config/logging";
|
import {TIME} from "config/logging";
|
||||||
import {ELEVATION, MIN_LAND_HEIGHT, ROUTES} from "config/generation";
|
import {ELEVATION, MIN_LAND_HEIGHT, ROUTES} from "config/generation";
|
||||||
import {dist2} from "utils/functionUtils";
|
import {dist2} from "utils/functionUtils";
|
||||||
import {isBurg} from "utils/typeUtils";
|
import {isBurg} from "utils/typeUtils";
|
||||||
|
import {calculateUrquhartEdges} from "./urquhart";
|
||||||
|
|
||||||
type TCellsData = Pick<IPack["cells"], "c" | "p" | "g" | "h" | "t" | "biome" | "burg">;
|
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;
|
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;
|
name: string;
|
||||||
feature: number;
|
feature: number;
|
||||||
state: number;
|
state: number;
|
||||||
|
culture: number;
|
||||||
cell: number;
|
cell: number;
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
|
|
@ -11,8 +12,12 @@ interface IBurg {
|
||||||
coa: ICoa | "string";
|
coa: ICoa | "string";
|
||||||
capital: Logical; // 1 - capital, 0 - burg
|
capital: Logical; // 1 - capital, 0 - burg
|
||||||
port: number; // port feature id, 0 - not a port
|
port: number; // port feature id, 0 - not a port
|
||||||
shanty?: number;
|
citadel: Logical;
|
||||||
MFCG?: string | number;
|
plaza: Logical;
|
||||||
|
walls: Logical;
|
||||||
|
shanty: Logical;
|
||||||
|
temple: Logical;
|
||||||
|
MFCG?: string | number; // MFCG link of seed
|
||||||
removed?: boolean;
|
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;
|
state: Uint16Array;
|
||||||
culture: Uint16Array;
|
culture: Uint16Array;
|
||||||
religion: Uint16Array;
|
religion: Uint16Array;
|
||||||
province: UintArray;
|
province: Uint16Array;
|
||||||
burg: UintArray;
|
burg: Uint16Array;
|
||||||
haven: UintArray;
|
haven: UintArray;
|
||||||
harbor: UintArray;
|
harbor: UintArray;
|
||||||
route: Uint8Array; // [0, 1, 2, 3], see ROUTES enum, defined by generateRoutes()
|
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 {
|
interface IProvince {
|
||||||
i: number;
|
i: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
burg: number;
|
||||||
|
formName: string;
|
||||||
fullName: string;
|
fullName: string;
|
||||||
|
color: Hex | CssUrls;
|
||||||
|
state: number;
|
||||||
|
center: number;
|
||||||
|
pole: TPoint;
|
||||||
|
coa: ICoa | string;
|
||||||
removed?: boolean;
|
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;
|
type: TCultureType;
|
||||||
culture: number;
|
culture: number;
|
||||||
expansionism: number;
|
expansionism: number;
|
||||||
form: string;
|
form: TStateForm;
|
||||||
formName: string;
|
formName: string;
|
||||||
fullName: string;
|
fullName: string;
|
||||||
|
pole: TPoint;
|
||||||
coa: ICoa | string;
|
coa: ICoa | string;
|
||||||
// pole: TPoint ?
|
|
||||||
area: number;
|
area: number;
|
||||||
cells: number;
|
cells: number;
|
||||||
burgs: number;
|
burgs: number;
|
||||||
|
|
@ -38,6 +38,8 @@ interface ICoa {
|
||||||
t1: string;
|
t1: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TStateForm = "Monarchy" | "Republic" | "Theocracy" | "Union" | "Anarchy";
|
||||||
|
|
||||||
type TRelation =
|
type TRelation =
|
||||||
| "Ally"
|
| "Ally"
|
||||||
| "Friendly"
|
| "Friendly"
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,19 @@ export function drawLine([x1, y1]: TPoint, [x2, y2]: TPoint, {stroke = "#444", s
|
||||||
.attr("stroke-width", strokeWidth);
|
.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"} = {}) {
|
export function drawArrow([x1, y1]: TPoint, [x2, y2]: TPoint, {width = 1, color = "#444"} = {}) {
|
||||||
const normal = getNormal([x1, y1], [x2, y2]);
|
const normal = getNormal([x1, y1], [x2, y2]);
|
||||||
const [xMid, yMid] = [(x1 + x2) / 2, (y1 + y2) / 2];
|
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 =>
|
export const isReligion = (religion: TNoReligion | IReligion): religion is IReligion =>
|
||||||
religion.i !== 0 && !(religion as IReligion).removed;
|
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"
|
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-1.2.9.tgz#c7dc78992cd8ca5c850243a265fd257ea56df1fa"
|
||||||
integrity sha512-E/7RgPr2ylT5dWG0CswMi9NpFcjIEDqLcUSBgNHe/EMahfqYaTx4zhcggG3khqoEB/leY4Vl6nTSbwLUPjXceA==
|
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":
|
"@types/d3-axis@^1":
|
||||||
version "1.0.16"
|
version "1.0.16"
|
||||||
resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-1.0.16.tgz#93d7a28795c2f8b0e2fd550fcc4d29b7f174e693"
|
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"
|
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f"
|
||||||
integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==
|
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:
|
d3-axis@1:
|
||||||
version "1.0.12"
|
version "1.0.12"
|
||||||
resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-1.0.12.tgz#cdf20ba210cfbb43795af33756886fb3638daac9"
|
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"
|
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
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:
|
is-builtin-module@^3.1.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.1.0.tgz#6fdb24313b1c03b75f8b9711c0feb8c30b903b00"
|
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