Refactor layers rendering (#1120)

* feat: render states - use global fn

* feat: render states - separate pole detection from layer render

* feat: render provinces

* chore: unify drawFillWithGap

* refactor: drawIce

* refactor: drawBorders

* refactor: drawHeightmap

* refactor: drawTemperature

* refactor: drawBiomes

* refactor: drawPrec

* refactor: drawPrecipitation

* refactor: drawPopulation

* refactor: drawCells

* refactor: geColor

* refactor: drawMarkers

* refactor: drawScaleBar

* refactor: drawScaleBar

* refactor: drawMilitary

* refactor: pump version to 1.104.00

* refactor: pump version to 1.104.00

* refactor: drawCoastline and createDefaultRuler

* refactor: drawCoastline

* refactor: Features module start

* refactor: features - define distance fields

* feat: drawFeatures

* feat: drawIce don't hide

* feat: detect coastline - fix issue with border feature

* feat: separate labels rendering from generation process

* feat: auto-update and restore layers

* refactor - change layers

* refactor - sort layers

* fix: regenerate burgs to re-render layers

* fix: getColor is not defined

* fix: burgs overview - don't auto-show labels on hover

* fix: redraw population on change

* refactor: improve tooltip logic for burg labels and icons

* chore: pump version to 1.104.0

* fefactor: edit coastline and lake

* fix: minot fixes

* fix: submap

---------

Co-authored-by: Azgaar <azgaar.fmg@yandex.com>
This commit is contained in:
Azgaar 2024-09-20 12:20:27 +02:00 committed by GitHub
parent ec993d1a9b
commit 05de284e02
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
52 changed files with 2473 additions and 2713 deletions

View file

@ -122,10 +122,6 @@ a {
fill: none;
}
#biomes {
stroke-width: 0.7;
}
#landmass {
mask: url(#land);
fill-rule: evenodd;
@ -190,20 +186,12 @@ t,
font-size: 0.8em;
}
#statesBody {
stroke-width: 3;
}
#statesHalo {
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
#provincesBody {
stroke-width: 0.2;
}
#statesBody,
#provincesBody,
#relig,
@ -733,7 +721,7 @@ input[type="color"]::-webkit-color-swatch-wrapper {
#toolsContent > .grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-columns: repeat(3, 1fr);
margin: 0.2em 0;
}
@ -780,7 +768,7 @@ fieldset {
text-overflow: ellipsis;
}
.tabcontent .buttonoff {
.tabcontent li.buttonoff {
background-color: var(--bg-disabled);
color: #444444aa;
}

View file

@ -138,7 +138,7 @@
}
</style>
<link rel="preload" href="index.css?v=1.99.11" as="style" onload="this.onload=null; this.rel='stylesheet'" />
<link rel="preload" href="index.css?v=1.104.0" as="style" onload="this.onload=null; this.rel='stylesheet'" />
<link rel="preload" href="icons.css" as="style" onload="this.onload=null; this.rel='stylesheet'" />
<link rel="preload" href="libs/jquery-ui.css" as="style" onload="this.onload=null; this.rel='stylesheet'" />
</head>
@ -344,6 +344,10 @@
</g>
<g id="deftemp">
<g id="featurePaths"></g>
<g id="textPaths"></g>
<g id="statePaths"></g>
<g id="defs-emblems"></g>
<mask id="land"></mask>
<mask id="water">
<rect x="0" y="0" width="100%" height="100%" fill="white" />
@ -351,9 +355,6 @@
<mask id="fog" style="stroke-width: 10; stroke: black; stroke-linejoin: round; stroke-opacity: 0.1">
<rect x="0" y="0" width="100%" height="100%" fill="white" stroke="none" />
</mask>
<g id="textPaths"></g>
<g id="statePaths"></g>
<g id="defs-emblems"></g>
</g>
<pattern id="oceanic" width="100" height="100" patternUnits="userSpaceOnUse">
@ -438,7 +439,7 @@
<select
data-tip="Select a map layers preset"
id="layersPreset"
onchange="changePreset(this.value)"
onchange="handleLayersPresetChange(this.value)"
style="width: 45%"
>
<option value="political" selected>Political map</option>
@ -478,7 +479,6 @@
id="toggleTexture"
data-tip="Texture overlay: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="X"
class="buttonoff"
onclick="toggleTexture(event)"
>
Te<u>x</u>ture
@ -487,7 +487,6 @@
id="toggleHeight"
data-tip="Heightmap: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="H"
class="buttonoff"
onclick="toggleHeight(event)"
>
<u>H</u>eightmap
@ -496,7 +495,6 @@
id="toggleBiomes"
data-tip="Biomes: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="B"
class="buttonoff"
onclick="toggleBiomes(event)"
>
<u>B</u>iomes
@ -505,7 +503,6 @@
id="toggleCells"
data-tip="Cells structure: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="E"
class="buttonoff"
onclick="toggleCells(event)"
>
C<u>e</u>lls
@ -514,7 +511,6 @@
id="toggleGrid"
data-tip="Grid: click to toggle, drag to raise or lower. Ctrl + click to edit layer style and select type"
data-shortcut="G"
class="buttonoff"
onclick="toggleGrid(event)"
>
<u>G</u>rid
@ -523,7 +519,6 @@
id="toggleCoordinates"
data-tip="Coordinate grid: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="O"
class="buttonoff"
onclick="toggleCoordinates(event)"
>
C<u>o</u>ordinates
@ -532,7 +527,6 @@
id="toggleCompass"
data-tip="Wind (Compass) Rose: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="W"
class="buttonoff"
onclick="toggleCompass(event)"
>
<u>W</u>ind Rose
@ -549,7 +543,6 @@
id="toggleRelief"
data-tip="Relief and biome icons: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="F"
class="buttonoff"
onclick="toggleRelief(event)"
>
Relie<u>f</u>
@ -558,7 +551,6 @@
id="toggleReligions"
data-tip="Religions: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="R"
class="buttonoff"
onclick="toggleReligions(event)"
>
<u>R</u>eligions
@ -567,7 +559,6 @@
id="toggleCultures"
data-tip="Cultures: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="C"
class="buttonoff"
onclick="toggleCultures(event)"
>
<u>C</u>ultures
@ -584,7 +575,6 @@
id="toggleProvinces"
data-tip="Provinces: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="P"
class="buttonoff"
onclick="toggleProvinces(event)"
>
<u>P</u>rovinces
@ -593,7 +583,6 @@
id="toggleZones"
data-tip="Zones: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="Z"
class="buttonoff"
onclick="toggleZones(event)"
>
<u>Z</u>ones
@ -610,17 +599,15 @@
id="toggleRoutes"
data-tip="Trade routes: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="U"
class="buttonoff"
onclick="toggleRoutes(event)"
>
Ro<u>u</u>tes
</li>
<li
id="toggleTemp"
id="toggleTemperature"
data-tip="Temperature map: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="T"
class="buttonoff"
onclick="toggleTemp(event)"
onclick="toggleTemperature(event)"
>
<u>T</u>emperature
</li>
@ -628,7 +615,6 @@
id="togglePopulation"
data-tip="Population map: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="N"
class="buttonoff"
onclick="togglePopulation(event)"
>
Populatio<u>n</u>
@ -637,17 +623,15 @@
id="toggleIce"
data-tip="Icebergs and glaciers: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="J"
class="buttonoff"
onclick="toggleIce(event)"
>
Ice
</li>
<li
id="togglePrec"
id="togglePrecipitation"
data-tip="Precipitation map: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="A"
class="buttonoff"
onclick="togglePrec(event)"
onclick="togglePrecipitation(event)"
>
Precipit<u>a</u>tion
</li>
@ -655,7 +639,6 @@
id="toggleEmblems"
data-tip="Emblems: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="Y"
class="buttonoff"
onclick="toggleEmblems(event)"
>
Emblems
@ -669,10 +652,10 @@
<u>L</u>abels
</li>
<li
id="toggleIcons"
id="toggleBurgIcons"
data-tip="Burg icons: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="I"
onclick="toggleIcons(event)"
onclick="toggleBurgIcons(event)"
>
<u>I</u>cons
</li>
@ -680,7 +663,6 @@
id="toggleMilitary"
data-tip="Military forces: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="M"
class="buttonoff"
onclick="toggleMilitary(event)"
>
<u>M</u>ilitary
@ -689,7 +671,6 @@
id="toggleMarkers"
data-tip="Markers: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="K"
class="buttonoff"
onclick="toggleMarkers(event)"
>
Mar<u>k</u>ers
@ -698,7 +679,6 @@
id="toggleRulers"
data-tip="Rulers: click to toggle, drag to move, click on label to delete. Ctrl + click to edit layer style"
data-shortcut="= (equal sign)"
class="buttonoff"
onclick="toggleRulers(event)"
>
Rulers
@ -2095,7 +2075,7 @@
id="regenerateStateLabels"
data-tip="Click to update state labels placement based on current borders"
>
Labels
State Labels
</button>
<button id="regenerateMarkers" data-tip="Click to regenerate unlocked markers">
Markers <i id="configRegenerateMarkers" class="icon-cog" data-tip="Click to set number multiplier"></i>
@ -2128,7 +2108,7 @@
<button id="regenerateRoutes" data-tip="Click to regenerate all unlocked routes">Routes</button>
<button
id="regenerateStates"
data-tip="Click to select new capitals and regenerate non-locked states. Emblems and military forces will be regenerated as well, burgs will remain as they are"
data-tip="Click to regenerate non-locked states. Emblems and military forces will be regenerated as well, burgs will remain as they are, but capitals will be different"
>
States
</button>
@ -8051,47 +8031,49 @@
<script src="utils/stringUtils.js?v=1.99.00"></script>
<script src="utils/languageUtils.js?v=1.99.00"></script>
<script src="utils/unitUtils.js?v=1.99.00"></script>
<script src="utils/pathUtils.js?v=1.100.00"></script>
<script src="utils/pathUtils.js?v=1.104.0"></script>
<script defer src="utils/debugUtils.js?v=1.99.00"></script>
<script src="modules/voronoi.js"></script>
<script src="config/heightmap-templates.js"></script>
<script src="config/precreated-heightmaps.js"></script>
<script src="modules/heightmap-generator.js?v=1.99.00"></script>
<script src="modules/ocean-layers.js?v=1.100.00"></script>
<script src="modules/features.js?v=1.104.0"></script>
<script src="modules/ocean-layers.js?v=1.104.0"></script>
<script src="modules/river-generator.js?v=1.99.05"></script>
<script src="modules/lakes.js?v=1.99.00"></script>
<script src="modules/biomes.js?v=1.99.00"></script>
<script src="modules/names-generator.js?v=1.87.14"></script>
<script src="modules/cultures-generator.js?v=1.99.05"></script>
<script src="modules/renderers/state-labels.js?v=1.96.04"></script>
<script src="modules/burgs-and-states.js?v=1.99.05"></script>
<script src="modules/burgs-and-states.js?v=1.104.0"></script>
<script src="modules/provinces-generator.js?v=1.104.0"></script>
<script src="modules/routes-generator.js?v=1.99.04"></script>
<script src="modules/religions-generator.js?v=1.99.05"></script>
<script src="modules/military-generator.js?v=1.99.00"></script>
<script src="modules/markers-generator.js?v=1.103.02"></script>
<script src="modules/zones-generator.js?v=1.103.7"></script>
<script src="modules/military-generator.js?v=1.104.0"></script>
<script src="modules/markers-generator.js?v=1.104.0"></script>
<script src="modules/zones-generator.js?v=1.104.0"></script>
<script src="modules/coa-generator.js?v=1.99.00"></script>
<script src="modules/submap.js?v=1.100.00"></script>
<script src="modules/submap.js?v=1.104.0"></script>
<script src="libs/alea.min.js"></script>
<script src="libs/polylabel.min.js"></script>
<script src="libs/lineclip.min.js"></script>
<script src="libs/alea.min.js"></script>
<script src="libs/simplify.js"></script>
<script src="modules/fonts.js?v=1.99.03"></script>
<script src="modules/ui/layers.js?v=1.101.00"></script>
<script src="modules/ui/measurers.js?v=1.99.00"></script>
<script src="modules/ui/style-presets.js?v=1.100.00"></script>
<script src="modules/ui/general.js?v=1.103.5"></script>
<script src="modules/ui/general.js?v=1.104.0"></script>
<script src="modules/ui/options.js?v=1.100.00"></script>
<script src="main.js?v=1.100.00"></script>
<script src="main.js?v=1.104.0"></script>
<script defer src="modules/relief-icons.js?v=1.99.05"></script>
<script defer src="modules/ui/style.js?v=1.101.00"></script>
<script defer src="modules/ui/style.js?v=1.104.0"></script>
<script defer src="modules/ui/editors.js?v=1.99.05"></script>
<script defer src="modules/ui/tools.js?v=1.100.00"></script>
<script defer src="modules/ui/world-configurator.js?v=1.99.00"></script>
<script defer src="modules/ui/heightmap-editor.js?v=1.100.00"></script>
<script defer src="modules/ui/provinces-editor.js?v=1.99.05"></script>
<script defer src="modules/ui/tools.js?v=1.104.0"></script>
<script defer src="modules/ui/world-configurator.js?v=1.104.0"></script>
<script defer src="modules/ui/heightmap-editor.js?v=1.104.0"></script>
<script defer src="modules/ui/provinces-editor.js?v=1.104.0"></script>
<script defer src="modules/ui/biomes-editor.js?v=1.99.05"></script>
<script defer src="modules/ui/namesbase-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/elevation-profile.js?v=1.99.00"></script>
@ -8107,7 +8089,7 @@
<script defer src="modules/ui/rivers-creator.js?v=1.99.00"></script>
<script defer src="modules/ui/relief-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/burg-editor.js?v=1.102.00"></script>
<script defer src="modules/ui/units-editor.js?v=1.99.05"></script>
<script defer src="modules/ui/units-editor.js?v=1.104.0"></script>
<script defer src="modules/ui/notes-editor.js?v=1.99.06"></script>
<script defer src="modules/ui/ai-generator.js?v=1.99.09"></script>
<script defer src="modules/ui/diplomacy-editor.js?v=1.99.00"></script>
@ -8116,7 +8098,7 @@
<script defer src="modules/ui/routes-overview.js?v=1.100.00"></script>
<script defer src="modules/ui/rivers-overview.js?v=1.99.00"></script>
<script defer src="modules/ui/military-overview.js?v=1.99.00"></script>
<script defer src="modules/ui/regiments-overview.js?v=1.99.00"></script>
<script defer src="modules/ui/regiments-overview.js?v=1.104.0"></script>
<script defer src="modules/ui/markers-overview.js?v=1.99.00"></script>
<script defer src="modules/ui/regiment-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/battle-screen.js?v=1.99.00"></script>
@ -8124,13 +8106,25 @@
<script defer src="modules/ui/markers-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/3d.js?v=1.99.00"></script>
<script defer src="modules/ui/submap.js?v=1.99.10"></script>
<script defer src="modules/ui/hotkeys.js?v=1.99.15"></script>
<script defer src="modules/ui/hotkeys.js?v=1.104.0"></script>
<script defer src="modules/coa-renderer.js?v=1.99.00"></script>
<script defer src="libs/rgbquant.min.js"></script>
<script defer src="libs/jquery.ui.touch-punch.min.js"></script>
<script defer src="modules/io/save.js?v=1.100.00"></script>
<script defer src="modules/io/load.js?v=1.103.4"></script>
<script defer src="modules/io/load.js?v=1.104.0"></script>
<script defer src="modules/io/cloud.js?v=1.99.00"></script>
<script defer src="modules/io/export.js?v=1.100.00"></script>
<script defer src="modules/renderers/draw-features.js?v=1.104.0"></script>
<script defer src="modules/renderers/draw-borders.js?v=1.104.0"></script>
<script defer src="modules/renderers/draw-heightmap.js?v=1.104.0"></script>
<script defer src="modules/renderers/draw-markers.js?v=1.104.0"></script>
<script defer src="modules/renderers/draw-scalebar.js?v=1.104.0"></script>
<script defer src="modules/renderers/draw-temperature.js?v=1.104.0"></script>
<script defer src="modules/renderers/draw-emblems.js?v=1.104.0"></script>
<script defer src="modules/renderers/draw-military.js?v=1.104.0"></script>
<script defer src="modules/renderers/draw-state-labels.js?v=1.104.0"></script>
<script defer src="modules/renderers/draw-burg-labels.js?v=1.104.0"></script>
<script defer src="modules/renderers/draw-burg-icons.js?v=1.104.0"></script>
</body>
</html>

103
libs/simplify.js Normal file
View file

@ -0,0 +1,103 @@
/*
(c) 2017, Vladimir Agafonkin
Simplify.js, a high-performance JS polyline simplification library
mourner.github.io/simplify-js
*/
{
// square distance between 2 points
function getSqDist([x1, y1], [x2, y2]) {
const dx = x1 - x2;
const dy = y1 - y2;
return dx * dx + dy * dy;
}
// square distance from a point to a segment
function getSqSegDist([x1, y1], [x, y], [x2, y2]) {
let dx = x2 - x;
let dy = y2 - y;
if (dx !== 0 || dy !== 0) {
const t = ((x1 - x) * dx + (y1 - y) * dy) / (dx * dx + dy * dy);
if (t > 1) {
x = x2;
y = y2;
} else if (t > 0) {
x += dx * t;
y += dy * t;
}
}
dx = x1 - x;
dy = y1 - y;
return dx * dx + dy * dy;
}
// rest of the code doesn't care about point format
// basic distance-based simplification
function simplifyRadialDist(points, sqTolerance) {
let prevPoint = points[0];
const newPoints = [prevPoint];
let point;
for (let i = 1, len = points.length; i < len; i++) {
point = points[i];
if (getSqDist(point, prevPoint) > sqTolerance) {
newPoints.push(point);
prevPoint = point;
}
}
if (point && prevPoint !== point) newPoints.push(point);
return newPoints;
}
function simplifyDPStep(points, first, last, sqTolerance, simplified) {
let maxSqDist = sqTolerance;
let index = first;
for (let i = first + 1; i < last; i++) {
const sqDist = getSqSegDist(points[i], points[first], points[last]);
if (sqDist > maxSqDist) {
index = i;
maxSqDist = sqDist;
}
}
if (maxSqDist > sqTolerance) {
if (index - first > 1) simplifyDPStep(points, first, index, sqTolerance, simplified);
simplified.push(points[index]);
if (last - index > 1) simplifyDPStep(points, index, last, sqTolerance, simplified);
}
}
// simplification using Ramer-Douglas-Peucker algorithm
function simplifyDouglasPeucker(points, sqTolerance) {
const last = points.length - 1;
const simplified = [points[0]];
simplifyDPStep(points, 0, last, sqTolerance, simplified);
simplified.push(points[last]);
return simplified;
}
// both algorithms combined for awesome performance
function simplify(points, tolerance, highestQuality = false) {
if (points.length <= 2) return points;
const sqTolerance = tolerance * tolerance;
points = highestQuality ? points : simplifyRadialDist(points, sqTolerance);
points = simplifyDouglasPeucker(points, sqTolerance);
return points;
}
window.simplify = simplify;
}

328
main.js
View file

@ -14,6 +14,7 @@ const ERROR = true;
const MOBILE = window.innerWidth < 600 || navigator.userAgentData?.mobile;
// typed arrays max values
const INT8_MAX = 127;
const UINT8_MAX = 255;
const UINT16_MAX = 65535;
const UINT32_MAX = 4294967295;
@ -72,7 +73,7 @@ let trails = routes.append("g").attr("id", "trails");
let searoutes = routes.append("g").attr("id", "searoutes");
let temperature = viewbox.append("g").attr("id", "temperature");
let coastline = viewbox.append("g").attr("id", "coastline");
let ice = viewbox.append("g").attr("id", "ice").style("display", "none");
let ice = viewbox.append("g").attr("id", "ice");
let prec = viewbox.append("g").attr("id", "prec").style("display", "none");
let population = viewbox.append("g").attr("id", "population");
let emblems = viewbox.append("g").attr("id", "emblems").style("display", "none");
@ -80,7 +81,7 @@ let labels = viewbox.append("g").attr("id", "labels");
let icons = viewbox.append("g").attr("id", "icons");
let burgIcons = icons.append("g").attr("id", "burgIcons");
let anchors = icons.append("g").attr("id", "anchors");
let armies = viewbox.append("g").attr("id", "armies").style("display", "none");
let armies = viewbox.append("g").attr("id", "armies");
let markers = viewbox.append("g").attr("id", "markers");
let fogging = viewbox
.append("g")
@ -313,9 +314,10 @@ async function checkLoadParameters() {
async function generateMapOnLoad() {
await applyStyleOnLoad(); // apply previously selected default or custom style
await generate(); // generate map
applyPreset(); // apply saved layers preset
applyLayersPreset(); // apply saved layers preset and reder layers
drawLayers();
fitMapToScreen();
focusOn(); // based on searchParams focus on point, cell or burg from MFCG
focusOn(); // focus on point, cell or burg from MFCG based on url searchParams
}
// focus on coordinates, cell or burg provided in searchParams
@ -432,7 +434,9 @@ function findBurgForMFCG(params) {
function handleZoom(isScaleChanged, isPositionChanged) {
viewbox.attr("transform", `translate(${viewX} ${viewY}) scale(${scale})`);
if (isPositionChanged) drawCoordinates();
if (isPositionChanged) {
if (layerIsOn("toggleCoordinates")) drawCoordinates();
}
if (isScaleChanged) {
invokeActiveZooming();
@ -544,21 +548,6 @@ function invokeActiveZooming() {
}
}
async function renderGroupCOAs(g) {
const [group, type] =
g.id === "burgEmblems"
? [pack.burgs, "burg"]
: g.id === "provinceEmblems"
? [pack.provinces, "province"]
: [pack.states, "state"];
for (let use of g.children) {
const i = +use.dataset.i;
const id = type + "COA" + i;
COArenderer.trigger(id, group[i].coa);
use.setAttribute("href", "#" + id);
}
}
// add drag to upload logic, pull request from @evyatron
void (function addDragToUpload() {
document.addEventListener("dragover", function (e) {
@ -624,8 +613,7 @@ async function generate(options) {
grid.cells.h = await HeightmapGenerator.generate(grid);
pack = {}; // reset pack
markFeatures();
markupGridOcean();
Features.markupGrid();
addLakesInDeepDepressions();
openNearSeaLakes();
@ -636,11 +624,10 @@ async function generate(options) {
generatePrecipitation();
reGraph();
drawCoastline();
Features.markupPack();
createDefaultRuler();
Rivers.generate();
drawRivers();
Lakes.defineGroup();
Biomes.define();
rankCells();
@ -650,15 +637,12 @@ async function generate(options) {
Routes.generate();
Religions.generate();
BurgsAndStates.defineStateForms();
BurgsAndStates.generateProvinces();
Provinces.generate();
Provinces.getPoles();
BurgsAndStates.defineBurgFeatures();
drawStates();
drawBorders();
drawStateLabels();
Rivers.specify();
Lakes.generateName();
Features.specify();
Military.generate();
Markers.generate();
@ -716,69 +700,6 @@ function setSeed(precreatedSeed) {
Math.random = aleaPRNG(seed);
}
// Mark features (ocean, lakes, islands) and calculate distance field
function markFeatures() {
TIME && console.time("markFeatures");
Math.random = aleaPRNG(seed); // get the same result on heightmap edit in Erase mode
const cells = grid.cells;
const heights = grid.cells.h;
cells.f = new Uint16Array(cells.i.length); // cell feature number
cells.t = new Int8Array(cells.i.length); // cell type: 1 = land coast; -1 = water near coast
grid.features = [0];
for (let i = 1, queue = [0]; queue[0] !== -1; i++) {
cells.f[queue[0]] = i; // feature number
const land = heights[queue[0]] >= 20;
let border = false; // true if feature touches map border
while (queue.length) {
const q = queue.pop();
if (cells.b[q]) border = true;
cells.c[q].forEach(c => {
const cLand = heights[c] >= 20;
if (land === cLand && !cells.f[c]) {
cells.f[c] = i;
queue.push(c);
} else if (land && !cLand) {
cells.t[q] = 1;
cells.t[c] = -1;
}
});
}
const type = land ? "island" : border ? "ocean" : "lake";
grid.features.push({i, land, border, type});
queue[0] = cells.f.findIndex(f => !f); // find unmarked cell
}
TIME && console.timeEnd("markFeatures");
}
function markupGridOcean() {
TIME && console.time("markupGridOcean");
markup(grid.cells, -2, -1, -10);
TIME && console.timeEnd("markupGridOcean");
}
// Calculate cell-distance to coast for every cell
function markup(cells, start, increment, limit) {
for (let t = start, count = Infinity; count > 0 && t > limit; t += increment) {
count = 0;
const prevT = t - increment;
for (let i = 0; i < cells.i.length; i++) {
if (cells.t[i] !== prevT) continue;
for (const c of cells.c[i]) {
if (cells.t[c]) continue;
cells.t[c] = t;
count++;
}
}
}
}
function addLakesInDeepDepressions() {
TIME && console.time("addLakesInDeepDepressions");
const {cells, features} = grid;
@ -1220,222 +1141,6 @@ function reGraph() {
TIME && console.timeEnd("reGraph");
}
// Detect and draw the coastline
function drawCoastline() {
TIME && console.time("drawCoastline");
reMarkFeatures();
const cells = pack.cells,
vertices = pack.vertices,
n = cells.i.length,
features = pack.features;
const used = new Uint8Array(features.length); // store connected features
const largestLand = d3.scan(
features.map(f => (f.land ? f.cells : 0)),
(a, b) => b - a
);
const landMask = defs.select("#land");
const waterMask = defs.select("#water");
lineGen.curve(d3.curveBasisClosed);
for (const i of cells.i) {
const startFromEdge = !i && cells.h[i] >= 20;
if (!startFromEdge && cells.t[i] !== -1 && cells.t[i] !== 1) continue; // non-edge cell
const f = cells.f[i];
if (used[f]) continue; // already connected
if (features[f].type === "ocean") continue; // ocean cell
const type = features[f].type === "lake" ? 1 : -1; // type value to search for
const start = findStart(i, type);
if (start === -1) continue; // cannot start here
let vchain = connectVertices(start, type);
if (features[f].type === "lake") relax(vchain, 1.2);
used[f] = 1;
let points = clipPoly(
vchain.map(v => vertices.p[v]),
1
);
const area = d3.polygonArea(points); // area with lakes/islands
if (area > 0 && features[f].type === "lake") {
points = points.reverse();
vchain = vchain.reverse();
}
features[f].area = Math.abs(area);
features[f].vertices = vchain;
const path = round(lineGen(points));
if (features[f].type === "lake") {
landMask
.append("path")
.attr("d", path)
.attr("fill", "black")
.attr("id", "land_" + f);
// waterMask.append("path").attr("d", path).attr("fill", "white").attr("id", "water_"+id); // uncomment to show over lakes
lakes
.select("#freshwater")
.append("path")
.attr("d", path)
.attr("id", "lake_" + f)
.attr("data-f", f); // draw the lake
} else {
landMask
.append("path")
.attr("d", path)
.attr("fill", "white")
.attr("id", "land_" + f);
waterMask
.append("path")
.attr("d", path)
.attr("fill", "black")
.attr("id", "water_" + f);
const g = features[f].group === "lake_island" ? "lake_island" : "sea_island";
coastline
.select("#" + g)
.append("path")
.attr("d", path)
.attr("id", "island_" + f)
.attr("data-f", f); // draw the coastline
}
// draw ruler to cover the biggest land piece
if (f === largestLand) {
const from = points[d3.scan(points, (a, b) => a[0] - b[0])];
const to = points[d3.scan(points, (a, b) => b[0] - a[0])];
rulers.create(Ruler, [from, to]);
}
}
// find cell vertex to start path detection
function findStart(i, t) {
if (t === -1 && cells.b[i]) return cells.v[i].find(v => vertices.c[v].some(c => c >= n)); // map border cell
const filtered = cells.c[i].filter(c => cells.t[c] === t);
const index = cells.c[i].indexOf(d3.min(filtered));
return index === -1 ? index : cells.v[i][index];
}
// connect vertices to chain
function connectVertices(start, t) {
const chain = []; // vertices chain to form a path
for (let i = 0, current = start; i === 0 || (current !== start && i < 50000); i++) {
const prev = chain[chain.length - 1]; // previous vertex in chain
chain.push(current); // add current vertex to sequence
const c = vertices.c[current]; // cells adjacent to vertex
const v = vertices.v[current]; // neighboring vertices
const c0 = c[0] >= n || cells.t[c[0]] === t;
const c1 = c[1] >= n || cells.t[c[1]] === t;
const c2 = c[2] >= n || cells.t[c[2]] === t;
if (v[0] !== prev && c0 !== c1) current = v[0];
else if (v[1] !== prev && c1 !== c2) current = v[1];
else if (v[2] !== prev && c0 !== c2) current = v[2];
if (current === chain[chain.length - 1]) {
ERROR && console.error("Next vertex is not found");
break;
}
}
return chain;
}
// move vertices that are too close to already added ones
function relax(vchain, r) {
const p = vertices.p,
tree = d3.quadtree();
for (let i = 0; i < vchain.length; i++) {
const v = vchain[i];
let [x, y] = [p[v][0], p[v][1]];
if (i && vchain[i + 1] && tree.find(x, y, r) !== undefined) {
const v1 = vchain[i - 1],
v2 = vchain[i + 1];
const [x1, y1] = [p[v1][0], p[v1][1]];
const [x2, y2] = [p[v2][0], p[v2][1]];
[x, y] = [(x1 + x2) / 2, (y1 + y2) / 2];
p[v] = [x, y];
}
tree.add([x, y]);
}
}
TIME && console.timeEnd("drawCoastline");
}
// Re-mark features (ocean, lakes, islands)
function reMarkFeatures() {
TIME && console.time("reMarkFeatures");
const cells = pack.cells;
const features = (pack.features = [0]);
cells.f = new Uint16Array(cells.i.length); // cell feature number
cells.t = new Int8Array(cells.i.length); // cell type: 1 = land along coast; -1 = water along coast;
cells.haven = cells.i.length < 65535 ? new Uint16Array(cells.i.length) : new Uint32Array(cells.i.length); // cell haven (opposite water cell);
cells.harbor = new Uint8Array(cells.i.length); // cell harbor (number of adjacent water cells);
if (!cells.i.length) return; // no cells -> there is nothing to do
for (let i = 1, queue = [0]; queue[0] !== -1; i++) {
const start = queue[0]; // first cell
cells.f[start] = i; // assign feature number
const land = cells.h[start] >= 20;
let border = false; // true if feature touches map border
let cellNumber = 1; // to count cells number in a feature
while (queue.length) {
const q = queue.pop();
if (cells.b[q]) border = true;
cells.c[q].forEach(function (e) {
const eLand = cells.h[e] >= 20;
if (land && !eLand) {
cells.t[q] = 1;
cells.t[e] = -1;
if (!cells.haven[q]) defineHaven(q);
} else if (land && eLand) {
if (!cells.t[e] && cells.t[q] === 1) cells.t[e] = 2;
else if (!cells.t[q] && cells.t[e] === 1) cells.t[q] = 2;
}
if (!cells.f[e] && land === eLand) {
queue.push(e);
cells.f[e] = i;
cellNumber++;
}
});
}
const type = land ? "island" : border ? "ocean" : "lake";
let group;
if (type === "ocean") group = defineOceanGroup(cellNumber);
else if (type === "island") group = defineIslandGroup(start, cellNumber);
features.push({i, land, border, type, cells: cellNumber, firstCell: start, group});
queue[0] = cells.f.findIndex(f => !f); // find unmarked cell
}
markup(pack.cells, 3, 1, 0); // markupPackLand
markup(pack.cells, -2, -1, -10); // markupPackWater
function defineHaven(i) {
const water = cells.c[i].filter(c => cells.h[c] < 20);
const dist2 = water.map(c => (cells.p[i][0] - cells.p[c][0]) ** 2 + (cells.p[i][1] - cells.p[c][1]) ** 2);
const closest = water[dist2.indexOf(Math.min.apply(Math, dist2))];
cells.haven[i] = closest;
cells.harbor[i] = water.length;
}
function defineOceanGroup(number) {
if (number > grid.cells.i.length / 25) return "ocean";
if (number > grid.cells.i.length / 100) return "sea";
return "gulf";
}
function defineIslandGroup(cell, number) {
if (cell && features[cells.f[cell - 1]].type === "lake") return "lake_island";
if (number > grid.cells.i.length / 10) return "continent";
if (number > grid.cells.i.length / 1000) return "island";
return "isle";
}
TIME && console.timeEnd("reMarkFeatures");
}
function isWetLand(moisture, temperature, height) {
if (moisture > 40 && temperature > -2 && height < 25) return true; //near coast
if (moisture > 24 && temperature > -2 && height > 24 && height < 60) return true; //off coast
@ -1522,7 +1227,7 @@ const regenerateMap = debounce(async function (options) {
resetZoom(1000);
undraw();
await generate(options);
restoreLayers();
drawLayers();
if (ThreeD.options.isOn) ThreeD.redraw();
if ($("#worldConfigurator").is(":visible")) editWorld();
@ -1541,6 +1246,5 @@ function undraw() {
.forEach(el => el.remove());
byId("coas").innerHTML = ""; // remove auto-generated emblems
notes = [];
rulers = new Rulers();
unfog();
}

View file

@ -13,6 +13,8 @@ window.BurgsAndStates = (() => {
placeTowns();
expandStates();
normalizeStates();
getPoles();
specifyBurgs();
collectStatistics();
@ -20,7 +22,6 @@ window.BurgsAndStates = (() => {
generateCampaigns();
generateDiplomacy();
drawBurgs();
function placeCapitals() {
TIME && console.time("placeCapitals");
@ -272,103 +273,6 @@ window.BurgsAndStates = (() => {
});
};
const drawBurgs = () => {
TIME && console.time("drawBurgs");
// remove old data
burgIcons.selectAll("circle").remove();
burgLabels.selectAll("text").remove();
icons.selectAll("use").remove();
// capitals
const capitals = pack.burgs.filter(b => b.capital && !b.removed);
const capitalIcons = burgIcons.select("#cities");
const capitalLabels = burgLabels.select("#cities");
const capitalSize = capitalIcons.attr("size") || 1;
const capitalAnchors = anchors.selectAll("#cities");
const caSize = capitalAnchors.attr("size") || 2;
capitalIcons
.selectAll("circle")
.data(capitals)
.enter()
.append("circle")
.attr("id", d => "burg" + d.i)
.attr("data-id", d => d.i)
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", capitalSize);
capitalLabels
.selectAll("text")
.data(capitals)
.enter()
.append("text")
.attr("id", d => "burgLabel" + d.i)
.attr("data-id", d => d.i)
.attr("x", d => d.x)
.attr("y", d => d.y)
.attr("dy", `${capitalSize * -1.5}px`)
.text(d => d.name);
capitalAnchors
.selectAll("use")
.data(capitals.filter(c => c.port))
.enter()
.append("use")
.attr("xlink:href", "#icon-anchor")
.attr("data-id", d => d.i)
.attr("x", d => rn(d.x - caSize * 0.47, 2))
.attr("y", d => rn(d.y - caSize * 0.47, 2))
.attr("width", caSize)
.attr("height", caSize);
// towns
const towns = pack.burgs.filter(b => b.i && !b.capital && !b.removed);
const townIcons = burgIcons.select("#towns");
const townLabels = burgLabels.select("#towns");
const townSize = townIcons.attr("size") || 0.5;
const townsAnchors = anchors.selectAll("#towns");
const taSize = townsAnchors.attr("size") || 1;
townIcons
.selectAll("circle")
.data(towns)
.enter()
.append("circle")
.attr("id", d => "burg" + d.i)
.attr("data-id", d => d.i)
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", townSize);
townLabels
.selectAll("text")
.data(towns)
.enter()
.append("text")
.attr("id", d => "burgLabel" + d.i)
.attr("data-id", d => d.i)
.attr("x", d => d.x)
.attr("y", d => d.y)
.attr("dy", `${townSize * -1.5}px`)
.text(d => d.name);
townsAnchors
.selectAll("use")
.data(towns.filter(c => c.port))
.enter()
.append("use")
.attr("xlink:href", "#icon-anchor")
.attr("data-id", d => d.i)
.attr("x", d => rn(d.x - taSize * 0.47, 2))
.attr("y", d => rn(d.y - taSize * 0.47, 2))
.attr("width", taSize)
.attr("height", taSize);
TIME && console.timeEnd("drawBurgs");
};
// expand cultures across the map (Dijkstra-like algorithm)
const expandStates = () => {
TIME && console.time("expandStates");
@ -468,8 +372,7 @@ window.BurgsAndStates = (() => {
const normalizeStates = () => {
TIME && console.time("normalizeStates");
const cells = pack.cells,
burgs = pack.burgs;
const {cells, burgs} = pack;
for (const i of cells.i) {
if (cells.h[i] < 20 || cells.burg[i]) continue; // do not overwrite burgs
@ -486,26 +389,30 @@ window.BurgsAndStates = (() => {
TIME && console.timeEnd("normalizeStates");
};
// Resets the cultures of all burgs and states to their
// cell or center cell's (respectively) culture.
// calculate pole of inaccessibility for each state
const getPoles = () => {
const getType = cellId => pack.cells.state[cellId];
const poles = getPolesOfInaccessibility(pack, getType);
pack.states.forEach(s => {
if (!s.i || s.removed) return;
s.pole = poles[s.i] || [0, 0];
});
};
// Resets the cultures of all burgs and states to their cell or center cell's (respectively) culture
const updateCultures = () => {
TIME && console.time("updateCulturesForBurgsAndStates");
// Assign the culture associated with the burgs cell.
// Assign the culture associated with the burgs cell
pack.burgs = pack.burgs.map((burg, index) => {
// Ignore metadata burg
if (index === 0) {
return burg;
}
if (index === 0) return burg;
return {...burg, culture: pack.cells.culture[burg.cell]};
});
// Assign the culture associated with the states' center cell.
// Assign the culture associated with the states' center cell
pack.states = pack.states.map((state, index) => {
// Ignore neutrals state
if (index === 0) {
return state;
}
if (index === 0) return state;
return {...state, culture: pack.cells.culture[state.center]};
});
@ -949,253 +856,12 @@ window.BurgsAndStates = (() => {
return adjName ? `${getAdjective(state.name)} ${state.formName}` : `${state.formName} of ${state.name}`;
};
const generateProvinces = (regenerate = false, regenerateInLockedStates = false) => {
TIME && console.time("generateProvinces");
const localSeed = regenerate ? generateSeed() : seed;
Math.random = aleaPRNG(localSeed);
const {cells, states, burgs} = pack;
const provinces = [0];
const provinceIds = new Uint16Array(cells.i.length);
const isProvinceLocked = province => province.lock || (!regenerateInLockedStates && states[province.state]?.lock);
const isProvinceCellLocked = cell => provinceIds[cell] && isProvinceLocked(provinces[provinceIds[cell]]);
if (regenerate) {
pack.provinces.forEach(province => {
if (!province.i || province.removed || !isProvinceLocked(province)) return;
const newId = provinces.length;
for (const i of cells.i) {
if (cells.province[i] === province.i) provinceIds[i] = newId;
}
province.i = newId;
provinces.push(province);
});
}
const provincesRatio = +byId("provincesRatio").value;
const max = provincesRatio == 100 ? 1000 : gauss(20, 5, 5, 100) * provincesRatio ** 0.5; // max growth
const forms = {
Monarchy: {County: 22, Earldom: 6, Shire: 2, Landgrave: 2, Margrave: 2, Barony: 2, Captaincy: 1, Seneschalty: 1},
Republic: {Province: 6, Department: 2, Governorate: 2, District: 1, Canton: 1, Prefecture: 1},
Theocracy: {Parish: 3, Deanery: 1},
Union: {Province: 1, State: 1, Canton: 1, Republic: 1, County: 1, Council: 1},
Anarchy: {Council: 1, Commune: 1, Community: 1, Tribe: 1},
Wild: {Territory: 10, Land: 5, Region: 2, Tribe: 1, Clan: 1, Dependency: 1, Area: 1}
};
// generate provinces for selected burgs
states.forEach(s => {
s.provinces = [];
if (!s.i || s.removed) return;
if (provinces.length) s.provinces = provinces.filter(p => p.state === s.i).map(p => p.i); // locked provinces ids
if (s.lock && !regenerateInLockedStates) return; // don't regenerate provinces of a locked state
const stateBurgs = burgs
.filter(b => b.state === s.i && !b.removed && !provinceIds[b.cell])
.sort((a, b) => b.population * gauss(1, 0.2, 0.5, 1.5, 3) - a.population)
.sort((a, b) => b.capital - a.capital);
if (stateBurgs.length < 2) return; // at least 2 provinces are required
const provincesNumber = Math.max(Math.ceil((stateBurgs.length * provincesRatio) / 100), 2);
const form = Object.assign({}, forms[s.form]);
for (let i = 0; i < provincesNumber; i++) {
const provinceId = provinces.length;
const center = stateBurgs[i].cell;
const burg = stateBurgs[i].i;
const c = stateBurgs[i].culture;
const nameByBurg = P(0.5);
const name = nameByBurg ? stateBurgs[i].name : Names.getState(Names.getCultureShort(c), c);
const formName = rw(form);
form[formName] += 10;
const fullName = name + " " + formName;
const color = getMixedColor(s.color);
const kinship = nameByBurg ? 0.8 : 0.4;
const type = getType(center, burg.port);
const coa = COA.generate(stateBurgs[i].coa, kinship, null, type);
coa.shield = COA.getShield(c, s.i);
s.provinces.push(provinceId);
provinces.push({i: provinceId, state: s.i, center, burg, name, formName, fullName, color, coa});
}
});
// expand generated provinces
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const cost = [];
provinces.forEach(p => {
if (!p.i || p.removed || isProvinceLocked(p)) return;
provinceIds[p.center] = p.i;
queue.queue({e: p.center, p: 0, province: p.i, state: p.state});
cost[p.center] = 1;
});
while (queue.length) {
const {e, p, province, state} = queue.dequeue();
cells.c[e].forEach(e => {
if (isProvinceCellLocked(e)) return; // do not overwrite cell of locked provinces
const land = cells.h[e] >= 20;
if (!land && !cells.t[e]) return; // cannot pass deep ocean
if (land && cells.state[e] !== state) return;
const evevation = cells.h[e] >= 70 ? 100 : cells.h[e] >= 50 ? 30 : cells.h[e] >= 20 ? 10 : 100;
const totalCost = p + evevation;
if (totalCost > max) return;
if (!cost[e] || totalCost < cost[e]) {
if (land) provinceIds[e] = province; // assign province to a cell
cost[e] = totalCost;
queue.queue({e, p: totalCost, province, state});
}
});
}
// justify provinces shapes a bit
for (const i of cells.i) {
if (cells.burg[i]) continue; // do not overwrite burgs
if (isProvinceCellLocked(i)) continue; // do not overwrite cell of locked provinces
const neibs = cells.c[i]
.filter(c => cells.state[c] === cells.state[i] && !isProvinceCellLocked(c))
.map(c => provinceIds[c]);
const adversaries = neibs.filter(c => c !== provinceIds[i]);
if (adversaries.length < 2) continue;
const buddies = neibs.filter(c => c === provinceIds[i]).length;
if (buddies.length > 2) continue;
const competitors = adversaries.map(p => adversaries.reduce((s, v) => (v === p ? s + 1 : s), 0));
const max = d3.max(competitors);
if (buddies >= max) continue;
provinceIds[i] = adversaries[competitors.indexOf(max)];
}
// add "wild" provinces if some cells don't have a province assigned
const noProvince = Array.from(cells.i).filter(i => cells.state[i] && !provinceIds[i]); // cells without province assigned
states.forEach(s => {
if (!s.i || s.removed) return;
if (s.lock && !regenerateInLockedStates) return;
if (!s.provinces.length) return;
const coreProvinceNames = s.provinces.map(p => provinces[p]?.name);
const colonyNamePool = [s.name, ...coreProvinceNames].filter(name => name && !/new/i.test(name));
const getColonyName = () => {
if (colonyNamePool.length < 1) return null;
const index = rand(colonyNamePool.length - 1);
const spliced = colonyNamePool.splice(index, 1);
return spliced[0] ? `New ${spliced[0]}` : null;
};
let stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !provinceIds[i]);
while (stateNoProvince.length) {
// add new province
const provinceId = provinces.length;
const burgCell = stateNoProvince.find(i => cells.burg[i]);
const center = burgCell ? burgCell : stateNoProvince[0];
const burg = burgCell ? cells.burg[burgCell] : 0;
provinceIds[center] = provinceId;
// expand province
const cost = [];
cost[center] = 1;
queue.queue({e: center, p: 0});
while (queue.length) {
const {e, p} = queue.dequeue();
cells.c[e].forEach(nextCellId => {
if (provinceIds[nextCellId]) return;
const land = cells.h[nextCellId] >= 20;
if (cells.state[nextCellId] && cells.state[nextCellId] !== s.i) return;
const ter = land ? (cells.state[nextCellId] === s.i ? 3 : 20) : cells.t[nextCellId] ? 10 : 30;
const totalCost = p + ter;
if (totalCost > max) return;
if (!cost[nextCellId] || totalCost < cost[nextCellId]) {
if (land && cells.state[nextCellId] === s.i) provinceIds[nextCellId] = provinceId; // assign province to a cell
cost[nextCellId] = totalCost;
queue.queue({e: nextCellId, p: totalCost});
}
});
}
// generate "wild" province name
const c = cells.culture[center];
const f = pack.features[cells.f[center]];
const color = getMixedColor(s.color);
const provCells = stateNoProvince.filter(i => provinceIds[i] === provinceId);
const singleIsle = provCells.length === f.cells && !provCells.find(i => cells.f[i] !== f.i);
const isleGroup = !singleIsle && !provCells.find(i => pack.features[cells.f[i]].group !== "isle");
const colony = !singleIsle && !isleGroup && P(0.5) && !isPassable(s.center, center);
const name = (() => {
const colonyName = colony && P(0.8) && getColonyName();
if (colonyName) return colonyName;
if (burgCell && P(0.5)) return burgs[burg].name;
return Names.getState(Names.getCultureShort(c), c);
})();
const formName = (() => {
if (singleIsle) return "Island";
if (isleGroup) return "Islands";
if (colony) return "Colony";
return rw(forms["Wild"]);
})();
const fullName = name + " " + formName;
const dominion = colony ? P(0.95) : singleIsle || isleGroup ? P(0.7) : P(0.3);
const kinship = dominion ? 0 : 0.4;
const type = getType(center, burgs[burg]?.port);
const coa = COA.generate(s.coa, kinship, dominion, type);
coa.shield = COA.getShield(c, s.i);
provinces.push({i: provinceId, state: s.i, center, burg, name, formName, fullName, color, coa});
s.provinces.push(provinceId);
// check if there is a land way within the same state between two cells
function isPassable(from, to) {
if (cells.f[from] !== cells.f[to]) return false; // on different islands
const queue = [from],
used = new Uint8Array(cells.i.length),
state = cells.state[from];
while (queue.length) {
const current = queue.pop();
if (current === to) return true; // way is found
cells.c[current].forEach(c => {
if (used[c] || cells.h[c] < 20 || cells.state[c] !== state) return;
queue.push(c);
used[c] = 1;
});
}
return false; // way is not found
}
// re-check
stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !provinceIds[i]);
}
});
cells.province = provinceIds;
pack.provinces = provinces;
TIME && console.timeEnd("generateProvinces");
};
return {
generate,
expandStates,
normalizeStates,
getPoles,
assignColors,
drawBurgs,
specifyBurgs,
defineBurgFeatures,
getType,
@ -1205,7 +871,6 @@ window.BurgsAndStates = (() => {
generateDiplomacy,
defineStateForms,
getFullName,
generateProvinces,
updateCultures
};
})();

View file

@ -51,9 +51,8 @@ export function resolveVersionConflicts(mapVersion) {
BurgsAndStates.generateCampaigns();
BurgsAndStates.generateDiplomacy();
BurgsAndStates.defineStateForms();
drawStates();
BurgsAndStates.generateProvinces();
drawBorders();
Provinces.generate();
Provinces.getPoles();
if (!layerIsOn("toggleBorders")) $("#borders").fadeOut();
if (!layerIsOn("toggleStates")) regions.attr("display", "none").selectAll("path").remove();
@ -202,7 +201,9 @@ export function resolveVersionConflicts(mapVersion) {
defs.select("#water").selectAll("path").remove();
coastline.selectAll("path").remove();
lakes.selectAll("path").remove();
drawCoastline();
Features.markupPack();
createDefaultRuler();
}
if (isOlderThan("1.11.0")) {
@ -940,4 +941,19 @@ export function resolveVersionConflicts(mapVersion) {
zones.style("display", null).selectAll("*").remove();
if (layerIsOn("toggleZones")) drawZones();
}
if (isOlderThan("1.104.0")) {
// v1.104.00 separated pole of inaccessibility detection from layer rendering
BurgsAndStates.getPoles();
Provinces.getPoles();
// v1.104.00 removed some layers from initial render
viewbox.select("#icons").style("display", null);
viewbox.select("#armies").style("display", null);
viewbox.select("#ice").style("display", null);
// v1.104.00 added featurePaths to defs
const featurePaths = defs.select("#featurePaths");
if (!featurePaths.size()) defs.append("g").attr("id", "featurePaths");
}
}

View file

@ -485,6 +485,7 @@ function applyPopulationChange(oldRural, oldUrban, newRural, newUrban, culture)
burgs.forEach(b => (b.population = population));
}
if (layerIsOn("togglePopulation")) drawPopulation();
refreshCulturesEditor();
}

View file

@ -462,6 +462,7 @@ function changePopulation() {
burgs.forEach(b => (b.population = population));
}
if (layerIsOn("togglePopulation")) drawPopulation();
refreshReligionsEditor();
}
}

View file

@ -543,6 +543,7 @@ function changePopulation(stateId) {
burgs.forEach(b => (b.population = population));
}
if (layerIsOn("togglePopulation")) drawPopulation();
refreshStatesEditor();
}
}
@ -642,11 +643,11 @@ function stateRemove(stateId) {
pack.states[stateId] = {i: stateId, removed: true};
debug.selectAll(".highlight").remove();
if (!layerIsOn("toggleStates")) toggleStates();
else drawStates();
if (!layerIsOn("toggleBorders")) toggleBorders();
else drawBorders();
if (layerIsOn("toggleStates")) drawStates();
if (layerIsOn("toggleBorders")) drawBorders();
if (layerIsOn("toggleProvinces")) drawProvinces();
refreshStatesEditor();
}
@ -839,13 +840,15 @@ function recalculateStates(must) {
if (!must && !statesAutoChange.checked) return;
BurgsAndStates.expandStates();
BurgsAndStates.generateProvinces();
if (!layerIsOn("toggleStates")) toggleStates();
else drawStates();
if (!layerIsOn("toggleBorders")) toggleBorders();
else drawBorders();
Provinces.generate();
Provinces.getPoles();
BurgsAndStates.getPoles();
if (layerIsOn("toggleStates")) drawStates();
if (layerIsOn("toggleBorders")) drawBorders();
if (layerIsOn("toggleProvinces")) drawProvinces();
if (adjustLabels.checked) drawStateLabels();
refreshStatesEditor();
}
@ -981,6 +984,7 @@ function applyStatesManualAssignent() {
if (affectedStates.length) {
refreshStatesEditor();
BurgsAndStates.getPoles();
layerIsOn("toggleStates") ? drawStates() : toggleStates();
if (adjustLabels.checked) drawStateLabels([...new Set(affectedStates)]);
adjustProvinces([...new Set(affectedProvinces)]);
@ -1415,6 +1419,7 @@ function openStateMergeDialog() {
unfog();
debug.selectAll(".highlight").remove();
BurgsAndStates.getPoles();
layerIsOn("toggleStates") ? drawStates() : toggleStates();
layerIsOn("toggleBorders") ? drawBorders() : toggleBorders();
layerIsOn("toggleProvinces") && drawProvinces();

255
modules/features.js Normal file
View file

@ -0,0 +1,255 @@
"use strict";
window.Features = (function () {
const DEEPER_LAND = 3;
const LANDLOCKED = 2;
const LAND_COAST = 1;
const UNMARKED = 0;
const WATER_COAST = -1;
const DEEP_WATER = -2;
// calculate distance to coast for every cell
function markup({distanceField, neighbors, start, increment, limit = INT8_MAX}) {
for (let distance = start, marked = Infinity; marked > 0 && distance !== limit; distance += increment) {
marked = 0;
const prevDistance = distance - increment;
for (let cellId = 0; cellId < neighbors.length; cellId++) {
if (distanceField[cellId] !== prevDistance) continue;
for (const neighborId of neighbors[cellId]) {
if (distanceField[neighborId] !== UNMARKED) continue;
distanceField[neighborId] = distance;
marked++;
}
}
}
}
// mark Grid features (ocean, lakes, islands) and calculate distance field
function markupGrid() {
TIME && console.time("markupGrid");
Math.random = aleaPRNG(seed); // get the same result on heightmap edit in Erase mode
const {h: heights, c: neighbors, b: borderCells, i} = grid.cells;
const cellsNumber = i.length;
const distanceField = new Int8Array(cellsNumber); // gird.cells.t
const featureIds = new Uint16Array(cellsNumber); // gird.cells.f
const features = [0];
const queue = [0];
for (let featureId = 1; queue[0] !== -1; featureId++) {
const firstCell = queue[0];
featureIds[firstCell] = featureId;
const land = heights[firstCell] >= 20;
let border = false; // set true if feature touches map edge
while (queue.length) {
const cellId = queue.pop();
if (!border && borderCells[cellId]) border = true;
for (const neighborId of neighbors[cellId]) {
const isNeibLand = heights[neighborId] >= 20;
if (land === isNeibLand && featureIds[neighborId] === UNMARKED) {
featureIds[neighborId] = featureId;
queue.push(neighborId);
} else if (land && !isNeibLand) {
distanceField[cellId] = LAND_COAST;
distanceField[neighborId] = WATER_COAST;
}
}
}
const type = land ? "island" : border ? "ocean" : "lake";
features.push({i: featureId, land, border, type});
queue[0] = featureIds.findIndex(f => f === UNMARKED); // find unmarked cell
}
// markup deep ocean cells
markup({distanceField, neighbors, start: DEEP_WATER, increment: -1, limit: -10});
grid.cells.t = distanceField;
grid.cells.f = featureIds;
grid.features = features;
TIME && console.timeEnd("markupGrid");
}
// mark Pack features (ocean, lakes, islands), calculate distance field and add properties
function markupPack() {
TIME && console.time("markupPack");
const {cells, vertices} = pack;
const {c: neighbors, b: borderCells, i} = cells;
const packCellsNumber = i.length;
if (!packCellsNumber) return; // no cells -> there is nothing to do
const distanceField = new Int8Array(packCellsNumber); // pack.cells.t
const featureIds = new Uint16Array(packCellsNumber); // pack.cells.f
const haven = createTypedArray({maxValue: packCellsNumber, length: packCellsNumber}); // haven: opposite water cell
const harbor = new Uint8Array(packCellsNumber); // harbor: number of adjacent water cells
const features = [0];
const queue = [0];
for (let featureId = 1; queue[0] !== -1; featureId++) {
const firstCell = queue[0];
featureIds[firstCell] = featureId;
const land = isLand(firstCell);
let border = Boolean(borderCells[firstCell]); // true if feature touches map border
let totalCells = 1; // count cells in a feature
while (queue.length) {
const cellId = queue.pop();
if (borderCells[cellId]) border = true;
if (!border && borderCells[cellId]) border = true;
for (const neighborId of neighbors[cellId]) {
const isNeibLand = isLand(neighborId);
if (land && !isNeibLand) {
distanceField[cellId] = LAND_COAST;
distanceField[neighborId] = WATER_COAST;
if (!haven[cellId]) defineHaven(cellId);
} else if (land && isNeibLand) {
if (distanceField[neighborId] === UNMARKED && distanceField[cellId] === LAND_COAST)
distanceField[neighborId] = LANDLOCKED;
else if (distanceField[cellId] === UNMARKED && distanceField[neighborId] === LAND_COAST)
distanceField[cellId] = LANDLOCKED;
}
if (!featureIds[neighborId] && land === isNeibLand) {
queue.push(neighborId);
featureIds[neighborId] = featureId;
totalCells++;
}
}
}
features.push(addFeature({firstCell, land, border, featureId, totalCells}));
queue[0] = featureIds.findIndex(f => f === UNMARKED); // find unmarked cell
}
markup({distanceField, neighbors, start: DEEPER_LAND, increment: 1}); // markup pack land
markup({distanceField, neighbors, start: DEEP_WATER, increment: -1, limit: -10}); // markup pack water
pack.cells.t = distanceField;
pack.cells.f = featureIds;
pack.cells.haven = haven;
pack.cells.harbor = harbor;
pack.features = features;
TIME && console.timeEnd("markupPack");
function defineHaven(cellId) {
const waterCells = neighbors[cellId].filter(isWater);
const distances = waterCells.map(neibCellId => dist2(cells.p[cellId], cells.p[neibCellId]));
const closest = distances.indexOf(Math.min.apply(Math, distances));
haven[cellId] = waterCells[closest];
harbor[cellId] = waterCells.length;
}
function addFeature({firstCell, land, border, featureId, totalCells}) {
const type = land ? "island" : border ? "ocean" : "lake";
const featureVertices = type === "ocean" ? [] : getFeatureVertices(firstCell);
const points = clipPoly(featureVertices.map(vertex => vertices.p[vertex]));
const area = d3.polygonArea(points); // feature perimiter area
const absArea = Math.abs(rn(area));
const feature = {
i: featureId,
type,
land,
border,
firstCell,
cells: totalCells,
vertices: featureVertices,
area: absArea
};
if (type === "lake") {
if (area > 0) feature.vertices = feature.vertices.reverse();
feature.shoreline = unique(feature.vertices.map(vertex => vertices.c[vertex].filter(isLand)).flat());
feature.height = Lakes.getHeight(feature);
}
return feature;
function getFeatureVertices(firstCell) {
const getType = cellId => featureIds[cellId];
const type = getType(firstCell);
const ofSameType = cellId => getType(cellId) === type;
const ofDifferentType = cellId => getType(cellId) !== type;
const isOnBorder = borderCells[firstCell] || neighbors[firstCell].some(ofDifferentType);
if (!isOnBorder) throw new Error(`Markup: firstCell ${firstCell} is not on the feature or map border`);
const startingVertex = cells.v[firstCell].find(v => vertices.c[v].some(ofDifferentType));
if (startingVertex === undefined) throw new Error(`Markup: startingVertex for cell ${firstCell} is not found`);
return connectVertices({vertices, startingVertex, ofSameType, closeRing: false});
}
}
}
// add properties to pack features
function specify() {
const gridCellsNumber = grid.cells.i.length;
const OCEAN_MIN_SIZE = gridCellsNumber / 25;
const SEA_MIN_SIZE = gridCellsNumber / 1000;
const CONTINENT_MIN_SIZE = gridCellsNumber / 10;
const ISLAND_MIN_SIZE = gridCellsNumber / 1000;
for (const feature of pack.features) {
if (!feature || feature.type === "ocean") continue;
feature.group = defineGroup(feature);
if (feature.type === "lake") {
feature.height = Lakes.getHeight(feature);
feature.name = Lakes.getName(feature);
}
}
function defineGroup(feature) {
if (feature.type === "island") return defineIslandGroup(feature);
if (feature.type === "ocean") return defineOceanGroup();
if (feature.type === "lake") return defineLakeGroup(feature);
throw new Error(`Markup: unknown feature type ${feature.type}`);
}
function defineOceanGroup(feature) {
if (feature.cells > OCEAN_MIN_SIZE) return "ocean";
if (feature.cells > SEA_MIN_SIZE) return "sea";
return "gulf";
}
function defineIslandGroup(feature) {
const prevFeature = pack.features[pack.cells.f[feature.firstCell - 1]];
if (prevFeature && prevFeature.type === "lake") return "lake_island";
if (feature.cells > CONTINENT_MIN_SIZE) return "continent";
if (feature.cells > ISLAND_MIN_SIZE) return "island";
return "isle";
}
function defineLakeGroup(feature) {
if (feature.temp < -3) return "frozen";
if (feature.height > 60 && feature.cells < 10 && feature.firstCell % 10 === 0) return "lava";
if (!feature.inlets && !feature.outlet) {
if (feature.evaporation > feature.flux * 4) return "dry";
if (feature.cells < 3 && feature.firstCell % 10 === 0) return "sinkhole";
}
if (!feature.outlet && feature.evaporation > feature.flux) return "salt";
return "freshwater";
}
}
return {markupGrid, markupPack, specify};
})();

View file

@ -175,6 +175,7 @@ async function getMapURL(type, options) {
noWater = false,
noScaleBar = false,
noIce = false,
noVignette = false,
fullMap = false
} = options || {};
@ -199,6 +200,7 @@ async function getMapURL(type, options) {
clone.select("#oceanPattern").attr("opacity", 0);
}
if (noIce) clone.select("#ice")?.remove();
if (noVignette) clone.select("#vignette")?.remove();
if (fullMap) {
// reset transform to show the whole map
clone.attr("width", graphWidth).attr("height", graphHeight);

View file

@ -368,7 +368,7 @@ async function parseLoadedData(data, mapVersion) {
{
reGraph();
reMarkFeatures();
Features.markupPack();
pack.features = JSON.parse(data[12]);
pack.cultures = JSON.parse(data[13]);
pack.states = JSON.parse(data[14]);
@ -419,7 +419,7 @@ async function parseLoadedData(data, mapVersion) {
// turn on active layers
if (hasChild(texture, "image")) turnOn("toggleTexture");
if (hasChildren(terrs)) turnOn("toggleHeight");
if (hasChildren(terrs.select("#landHeights"))) turnOn("toggleHeight");
if (hasChildren(biomes)) turnOn("toggleBiomes");
if (hasChildren(cells)) turnOn("toggleCells");
if (hasChildren(gridOverlay)) turnOn("toggleGrid");
@ -434,13 +434,13 @@ async function parseLoadedData(data, mapVersion) {
if (hasChildren(zones) && isVisible(zones)) turnOn("toggleZones");
if (isVisible(borders) && hasChild(borders, "path")) turnOn("toggleBorders");
if (isVisible(routes) && hasChild(routes, "path")) turnOn("toggleRoutes");
if (hasChildren(temperature)) turnOn("toggleTemp");
if (hasChildren(temperature)) turnOn("toggleTemperature");
if (hasChild(population, "line")) turnOn("togglePopulation");
if (hasChildren(ice)) turnOn("toggleIce");
if (hasChild(prec, "circle")) turnOn("togglePrec");
if (hasChild(prec, "circle")) turnOn("togglePrecipitation");
if (isVisible(emblems) && hasChild(emblems, "use")) turnOn("toggleEmblems");
if (isVisible(labels)) turnOn("toggleLabels");
if (isVisible(icons)) turnOn("toggleIcons");
if (isVisible(icons)) turnOn("toggleBurgIcons");
if (hasChildren(armies) && isVisible(armies)) turnOn("toggleMilitary");
if (hasChildren(markers)) turnOn("toggleMarkers");
if (isVisible(ruler)) turnOn("toggleRulers");
@ -459,7 +459,7 @@ async function parseLoadedData(data, mapVersion) {
{
// dynamically import and run auto-update script
const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.100.00");
const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.104.0");
resolveVersionConflicts(mapVersion);
}

View file

@ -1,98 +1,87 @@
"use strict";
window.Lakes = (function () {
const setClimateData = function (h) {
const cells = pack.cells;
const lakeOutCells = new Uint16Array(cells.i.length);
const LAKE_ELEVATION_DELTA = 0.1;
pack.features.forEach(f => {
if (f.type !== "lake") return;
// check if lake can be potentially open (not in deep depression)
const detectCloseLakes = h => {
const {cells} = pack;
const ELEVATION_LIMIT = +byId("lakeElevationLimitOutput").value;
// default flux: sum of precipitation around lake
f.flux = f.shoreline.reduce((acc, c) => acc + grid.cells.prec[cells.g[c]], 0);
pack.features.forEach(feature => {
if (feature.type !== "lake") return;
delete feature.closed;
// temperature and evaporation to detect closed lakes
f.temp =
f.cells < 6
? grid.cells.temp[cells.g[f.firstCell]]
: rn(d3.mean(f.shoreline.map(c => grid.cells.temp[cells.g[c]])), 1);
const height = (f.height - 18) ** heightExponentInput.value; // height in meters
const evaporation = ((700 * (f.temp + 0.006 * height)) / 50 + 75) / (80 - f.temp); // based on Penman formula, [1-11]
f.evaporation = rn(evaporation * f.cells);
// no outlet for lakes in depressed areas
if (f.closed) return;
// lake outlet cell
f.outCell = f.shoreline[d3.scan(f.shoreline, (a, b) => h[a] - h[b])];
lakeOutCells[f.outCell] = f.i;
});
return lakeOutCells;
};
// get array of land cells aroound lake
const getShoreline = function (lake) {
const uniqueCells = new Set();
if (!lake.vertices) lake.vertices = [];
lake.vertices.forEach(v => pack.vertices.c[v].forEach(c => pack.cells.h[c] >= 20 && uniqueCells.add(c)));
lake.shoreline = [...uniqueCells];
};
const prepareLakeData = h => {
const cells = pack.cells;
const ELEVATION_LIMIT = +document.getElementById("lakeElevationLimitOutput").value;
pack.features.forEach(f => {
if (f.type !== "lake") return;
delete f.flux;
delete f.inlets;
delete f.outlet;
delete f.height;
delete f.closed;
!f.shoreline && Lakes.getShoreline(f);
// lake surface height is as lowest land cells around
const min = f.shoreline.sort((a, b) => h[a] - h[b])[0];
f.height = h[min] - 0.1;
// check if lake can be open (not in deep depression)
if (ELEVATION_LIMIT === 80) {
f.closed = false;
const MAX_ELEVATION = feature.height + ELEVATION_LIMIT;
if (MAX_ELEVATION > 99) {
feature.closed = false;
return;
}
let deep = true;
const threshold = f.height + ELEVATION_LIMIT;
const queue = [min];
let isDeep = true;
const lowestShorelineCell = feature.shoreline.sort((a, b) => h[a] - h[b])[0];
const queue = [lowestShorelineCell];
const checked = [];
checked[min] = true;
checked[lowestShorelineCell] = true;
// check if elevated lake can potentially pour to another water body
while (deep && queue.length) {
const q = queue.pop();
while (queue.length && isDeep) {
const cellId = queue.pop();
for (const n of cells.c[q]) {
if (checked[n]) continue;
if (h[n] >= threshold) continue;
for (const neibCellId of cells.c[cellId]) {
if (checked[neibCellId]) continue;
if (h[neibCellId] >= MAX_ELEVATION) continue;
if (h[n] < 20) {
const nFeature = pack.features[cells.f[n]];
if (nFeature.type === "ocean" || f.height > nFeature.height) {
deep = false;
break;
}
if (h[neibCellId] < 20) {
const nFeature = pack.features[cells.f[neibCellId]];
if (nFeature.type === "ocean" || feature.height > nFeature.height) isDeep = false;
}
checked[n] = true;
queue.push(n);
checked[neibCellId] = true;
queue.push(neibCellId);
}
}
f.closed = deep;
feature.closed = isDeep;
});
};
const defineClimateData = function (heights) {
const {cells, features} = pack;
const lakeOutCells = new Uint16Array(cells.i.length);
features.forEach(feature => {
if (feature.type !== "lake") return;
feature.flux = getFlux(feature);
feature.temp = getLakeTemp(feature);
feature.evaporation = getLakeEvaporation(feature);
if (feature.closed) return; // no outlet for lakes in depressed areas
feature.outCell = getLowestShoreCell(feature);
lakeOutCells[feature.outCell] = feature.i;
});
return lakeOutCells;
function getFlux(lake) {
return lake.shoreline.reduce((acc, c) => acc + grid.cells.prec[cells.g[c]], 0);
}
function getLakeTemp(lake) {
if (lake.cells < 6) return grid.cells.temp[cells.g[lake.firstCell]];
return rn(d3.mean(lake.shoreline.map(c => grid.cells.temp[cells.g[c]])), 1);
}
function getLakeEvaporation(lake) {
const height = (lake.height - 18) ** heightExponentInput.value; // height in meters
const evaporation = ((700 * (lake.temp + 0.006 * height)) / 50 + 75) / (80 - lake.temp); // based on Penman formula, [1-11]
return rn(evaporation * lake.cells);
}
function getLowestShoreCell(lake) {
return lake.shoreline.sort((a, b) => heights[a] - heights[b])[0];
}
};
const cleanupLakeData = function () {
for (const feature of pack.features) {
if (feature.type !== "lake") continue;
@ -111,23 +100,10 @@ window.Lakes = (function () {
}
};
const defineGroup = function () {
for (const feature of pack.features) {
if (feature.type !== "lake") continue;
const lakeEl = lakes.select(`[data-f="${feature.i}"]`).node();
if (!lakeEl) continue;
feature.group = getGroup(feature);
document.getElementById(feature.group).appendChild(lakeEl);
}
};
const generateName = function () {
Math.random = aleaPRNG(seed);
for (const feature of pack.features) {
if (feature.type !== "lake") continue;
feature.name = getName(feature);
}
const getHeight = function (feature) {
const heights = pack.cells.h;
const minShoreHeight = d3.min(feature.shoreline.map(cellId => heights[cellId])) || 20;
return rn(minShoreHeight - LAKE_ELEVATION_DELTA, 2);
};
const getName = function (feature) {
@ -136,19 +112,5 @@ window.Lakes = (function () {
return Names.getCulture(culture);
};
function getGroup(feature) {
if (feature.temp < -3) return "frozen";
if (feature.height > 60 && feature.cells < 10 && feature.firstCell % 10 === 0) return "lava";
if (!feature.inlets && !feature.outlet) {
if (feature.evaporation > feature.flux * 4) return "dry";
if (feature.cells < 3 && feature.firstCell % 10 === 0) return "sinkhole";
}
if (!feature.outlet && feature.evaporation > feature.flux) return "salt";
return "freshwater";
}
return {setClimateData, cleanupLakeData, prepareLakeData, defineGroup, generateName, getName, getShoreline};
return {defineClimateData, cleanupLakeData, detectCloseLakes, getHeight, getName};
})();

View file

@ -2,7 +2,7 @@
window.Military = (function () {
const generate = function () {
TIME && console.time("generateMilitaryForces");
TIME && console.time("generateMilitary");
const {cells, states} = pack;
const {p} = cells;
const valid = states.filter(s => s.i && !s.removed); // valid states
@ -252,8 +252,6 @@ window.Military = (function () {
delete s.temp; // do not store temp data
});
redraw();
function createRegiments(nodes, s) {
if (!nodes.length) return [];
@ -312,19 +310,9 @@ window.Military = (function () {
return regiments;
}
TIME && console.timeEnd("generateMilitaryForces");
TIME && console.timeEnd("generateMilitary");
};
function redraw() {
const validStates = pack.states.filter(s => s.i && !s.removed);
armies.selectAll("g > g").each(function () {
const index = notes.findIndex(n => n.id === this.id);
if (index != -1) notes.splice(index, 1);
});
armies.selectAll("g").remove();
validStates.forEach(s => drawRegiments(s.military, s.i));
}
const getDefaultOptions = function () {
return [
{icon: "⚔️", name: "infantry", rural: 0.25, urban: 0.2, crew: 1, power: 1, type: "melee", separate: 0},
@ -335,122 +323,6 @@ window.Military = (function () {
];
};
const drawRegiments = function (regiments, s) {
const size = +armies.attr("box-size");
const w = d => (d.n ? size * 4 : size * 6);
const h = size * 2;
const x = d => rn(d.x - w(d) / 2, 2);
const y = d => rn(d.y - size, 2);
const baseColor = pack.states[s].color[0] === "#" ? pack.states[s].color : "#999";
const darkerColor = d3.color(baseColor).darker().hex();
const army = armies
.append("g")
.attr("id", "army" + s)
.attr("fill", baseColor)
.attr("color", darkerColor);
const g = army
.selectAll("g")
.data(regiments)
.enter()
.append("g")
.attr("id", d => "regiment" + s + "-" + d.i)
.attr("data-name", d => d.name)
.attr("data-state", s)
.attr("data-id", d => d.i)
.attr("transform", d => (d.angle ? `rotate(${d.angle})` : null))
.attr("transform-origin", d => `${d.x}px ${d.y}px`);
g.append("rect")
.attr("x", d => x(d))
.attr("y", d => y(d))
.attr("width", d => w(d))
.attr("height", h);
g.append("text")
.attr("x", d => d.x)
.attr("y", d => d.y)
.text(d => getTotal(d));
g.append("rect")
.attr("fill", "currentColor")
.attr("x", d => x(d) - h)
.attr("y", d => y(d))
.attr("width", h)
.attr("height", h);
g.append("text")
.attr("class", "regimentIcon")
.attr("x", d => x(d) - size)
.attr("y", d => d.y)
.text(d => d.icon);
};
const drawRegiment = function (reg, stateId) {
const size = +armies.attr("box-size");
const w = reg.n ? size * 4 : size * 6;
const h = size * 2;
const x1 = rn(reg.x - w / 2, 2);
const y1 = rn(reg.y - size, 2);
let army = armies.select("g#army" + stateId);
if (!army.size()) {
const baseColor = pack.states[stateId].color[0] === "#" ? pack.states[stateId].color : "#999";
const darkerColor = d3.color(baseColor).darker().hex();
army = armies
.append("g")
.attr("id", "army" + stateId)
.attr("fill", baseColor)
.attr("color", darkerColor);
}
const g = army
.append("g")
.attr("id", "regiment" + stateId + "-" + reg.i)
.attr("data-name", reg.name)
.attr("data-state", stateId)
.attr("data-id", reg.i)
.attr("transform", `rotate(${reg.angle || 0})`)
.attr("transform-origin", `${reg.x}px ${reg.y}px`);
g.append("rect").attr("x", x1).attr("y", y1).attr("width", w).attr("height", h);
g.append("text").attr("x", reg.x).attr("y", reg.y).text(getTotal(reg));
g.append("rect")
.attr("fill", "currentColor")
.attr("x", x1 - h)
.attr("y", y1)
.attr("width", h)
.attr("height", h);
g.append("text")
.attr("class", "regimentIcon")
.attr("x", x1 - size)
.attr("y", reg.y)
.text(reg.icon);
};
// move one regiment to another
const moveRegiment = function (reg, x, y) {
const el = armies.select("g#army" + reg.state).select("g#regiment" + reg.state + "-" + reg.i);
if (!el.size()) return;
const duration = Math.hypot(reg.x - x, reg.y - y) * 8;
reg.x = x;
reg.y = y;
const size = +armies.attr("box-size");
const w = reg.n ? size * 4 : size * 6;
const h = size * 2;
const x1 = x => rn(x - w / 2, 2);
const y1 = y => rn(y - size, 2);
const move = d3.transition().duration(duration).ease(d3.easeSinInOut);
el.select("rect").transition(move).attr("x", x1(x)).attr("y", y1(y));
el.select("text").transition(move).attr("x", x).attr("y", y);
el.selectAll("rect:nth-of-type(2)")
.transition(move)
.attr("x", x1(x) - h)
.attr("y", y1(y));
el.select(".regimentIcon")
.transition(move)
.attr("x", x1(x) - size)
.attr("y", y);
};
// utilize si function to make regiment total text fit regiment box
const getTotal = reg => (reg.a > (reg.n ? 999 : 99999) ? si(reg.a) : reg.a);
@ -513,13 +385,9 @@ window.Military = (function () {
return {
generate,
redraw,
getDefaultOptions,
getName,
generateNote,
drawRegiments,
drawRegiment,
moveRegiment,
getTotal,
getEmblem
};

View file

@ -0,0 +1,257 @@
"use strict";
window.Provinces = (function () {
const forms = {
Monarchy: {County: 22, Earldom: 6, Shire: 2, Landgrave: 2, Margrave: 2, Barony: 2, Captaincy: 1, Seneschalty: 1},
Republic: {Province: 6, Department: 2, Governorate: 2, District: 1, Canton: 1, Prefecture: 1},
Theocracy: {Parish: 3, Deanery: 1},
Union: {Province: 1, State: 1, Canton: 1, Republic: 1, County: 1, Council: 1},
Anarchy: {Council: 1, Commune: 1, Community: 1, Tribe: 1},
Wild: {Territory: 10, Land: 5, Region: 2, Tribe: 1, Clan: 1, Dependency: 1, Area: 1}
};
const generate = (regenerate = false, regenerateLockedStates = false) => {
TIME && console.time("generateProvinces");
const localSeed = regenerate ? generateSeed() : seed;
Math.random = aleaPRNG(localSeed);
const {cells, states, burgs} = pack;
const provinces = [0]; // 0 index is reserved for "no province"
const provinceIds = new Uint16Array(cells.i.length);
const isProvinceLocked = province => province.lock || (!regenerateLockedStates && states[province.state]?.lock);
const isProvinceCellLocked = cell => provinceIds[cell] && isProvinceLocked(provinces[provinceIds[cell]]);
if (regenerate) {
pack.provinces.forEach(province => {
if (!province.i || province.removed || !isProvinceLocked(province)) return;
const newId = provinces.length;
for (const i of cells.i) {
if (cells.province[i] === province.i) provinceIds[i] = newId;
}
province.i = newId;
provinces.push(province);
});
}
const provincesRatio = +byId("provincesRatio").value;
const max = provincesRatio == 100 ? 1000 : gauss(20, 5, 5, 100) * provincesRatio ** 0.5; // max growth
// generate provinces for selected burgs
states.forEach(s => {
s.provinces = [];
if (!s.i || s.removed) return;
if (provinces.length) s.provinces = provinces.filter(p => p.state === s.i).map(p => p.i); // locked provinces ids
if (s.lock && !regenerateLockedStates) return; // don't regenerate provinces of a locked state
const stateBurgs = burgs
.filter(b => b.state === s.i && !b.removed && !provinceIds[b.cell])
.sort((a, b) => b.population * gauss(1, 0.2, 0.5, 1.5, 3) - a.population)
.sort((a, b) => b.capital - a.capital);
if (stateBurgs.length < 2) return; // at least 2 provinces are required
const provincesNumber = Math.max(Math.ceil((stateBurgs.length * provincesRatio) / 100), 2);
const form = Object.assign({}, forms[s.form]);
for (let i = 0; i < provincesNumber; i++) {
const provinceId = provinces.length;
const center = stateBurgs[i].cell;
const burg = stateBurgs[i].i;
const c = stateBurgs[i].culture;
const nameByBurg = P(0.5);
const name = nameByBurg ? stateBurgs[i].name : Names.getState(Names.getCultureShort(c), c);
const formName = rw(form);
form[formName] += 10;
const fullName = name + " " + formName;
const color = getMixedColor(s.color);
const kinship = nameByBurg ? 0.8 : 0.4;
const type = BurgsAndStates.getType(center, burg.port);
const coa = COA.generate(stateBurgs[i].coa, kinship, null, type);
coa.shield = COA.getShield(c, s.i);
s.provinces.push(provinceId);
provinces.push({i: provinceId, state: s.i, center, burg, name, formName, fullName, color, coa});
}
});
// expand generated provinces
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const cost = [];
provinces.forEach(p => {
if (!p.i || p.removed || isProvinceLocked(p)) return;
provinceIds[p.center] = p.i;
queue.queue({e: p.center, p: 0, province: p.i, state: p.state});
cost[p.center] = 1;
});
while (queue.length) {
const {e, p, province, state} = queue.dequeue();
cells.c[e].forEach(e => {
if (isProvinceCellLocked(e)) return; // do not overwrite cell of locked provinces
const land = cells.h[e] >= 20;
if (!land && !cells.t[e]) return; // cannot pass deep ocean
if (land && cells.state[e] !== state) return;
const evevation = cells.h[e] >= 70 ? 100 : cells.h[e] >= 50 ? 30 : cells.h[e] >= 20 ? 10 : 100;
const totalCost = p + evevation;
if (totalCost > max) return;
if (!cost[e] || totalCost < cost[e]) {
if (land) provinceIds[e] = province; // assign province to a cell
cost[e] = totalCost;
queue.queue({e, p: totalCost, province, state});
}
});
}
// justify provinces shapes a bit
for (const i of cells.i) {
if (cells.burg[i]) continue; // do not overwrite burgs
if (isProvinceCellLocked(i)) continue; // do not overwrite cell of locked provinces
const neibs = cells.c[i]
.filter(c => cells.state[c] === cells.state[i] && !isProvinceCellLocked(c))
.map(c => provinceIds[c]);
const adversaries = neibs.filter(c => c !== provinceIds[i]);
if (adversaries.length < 2) continue;
const buddies = neibs.filter(c => c === provinceIds[i]).length;
if (buddies.length > 2) continue;
const competitors = adversaries.map(p => adversaries.reduce((s, v) => (v === p ? s + 1 : s), 0));
const max = d3.max(competitors);
if (buddies >= max) continue;
provinceIds[i] = adversaries[competitors.indexOf(max)];
}
// add "wild" provinces if some cells don't have a province assigned
const noProvince = Array.from(cells.i).filter(i => cells.state[i] && !provinceIds[i]); // cells without province assigned
states.forEach(s => {
if (!s.i || s.removed) return;
if (s.lock && !regenerateLockedStates) return;
if (!s.provinces.length) return;
const coreProvinceNames = s.provinces.map(p => provinces[p]?.name);
const colonyNamePool = [s.name, ...coreProvinceNames].filter(name => name && !/new/i.test(name));
const getColonyName = () => {
if (colonyNamePool.length < 1) return null;
const index = rand(colonyNamePool.length - 1);
const spliced = colonyNamePool.splice(index, 1);
return spliced[0] ? `New ${spliced[0]}` : null;
};
let stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !provinceIds[i]);
while (stateNoProvince.length) {
// add new province
const provinceId = provinces.length;
const burgCell = stateNoProvince.find(i => cells.burg[i]);
const center = burgCell ? burgCell : stateNoProvince[0];
const burg = burgCell ? cells.burg[burgCell] : 0;
provinceIds[center] = provinceId;
// expand province
const cost = [];
cost[center] = 1;
queue.queue({e: center, p: 0});
while (queue.length) {
const {e, p} = queue.dequeue();
cells.c[e].forEach(nextCellId => {
if (provinceIds[nextCellId]) return;
const land = cells.h[nextCellId] >= 20;
if (cells.state[nextCellId] && cells.state[nextCellId] !== s.i) return;
const ter = land ? (cells.state[nextCellId] === s.i ? 3 : 20) : cells.t[nextCellId] ? 10 : 30;
const totalCost = p + ter;
if (totalCost > max) return;
if (!cost[nextCellId] || totalCost < cost[nextCellId]) {
if (land && cells.state[nextCellId] === s.i) provinceIds[nextCellId] = provinceId; // assign province to a cell
cost[nextCellId] = totalCost;
queue.queue({e: nextCellId, p: totalCost});
}
});
}
// generate "wild" province name
const c = cells.culture[center];
const f = pack.features[cells.f[center]];
const color = getMixedColor(s.color);
const provCells = stateNoProvince.filter(i => provinceIds[i] === provinceId);
const singleIsle = provCells.length === f.cells && !provCells.find(i => cells.f[i] !== f.i);
const isleGroup = !singleIsle && !provCells.find(i => pack.features[cells.f[i]].group !== "isle");
const colony = !singleIsle && !isleGroup && P(0.5) && !isPassable(s.center, center);
const name = (() => {
const colonyName = colony && P(0.8) && getColonyName();
if (colonyName) return colonyName;
if (burgCell && P(0.5)) return burgs[burg].name;
return Names.getState(Names.getCultureShort(c), c);
})();
const formName = (() => {
if (singleIsle) return "Island";
if (isleGroup) return "Islands";
if (colony) return "Colony";
return rw(forms["Wild"]);
})();
const fullName = name + " " + formName;
const dominion = colony ? P(0.95) : singleIsle || isleGroup ? P(0.7) : P(0.3);
const kinship = dominion ? 0 : 0.4;
const type = BurgsAndStates.getType(center, burgs[burg]?.port);
const coa = COA.generate(s.coa, kinship, dominion, type);
coa.shield = COA.getShield(c, s.i);
provinces.push({i: provinceId, state: s.i, center, burg, name, formName, fullName, color, coa});
s.provinces.push(provinceId);
// check if there is a land way within the same state between two cells
function isPassable(from, to) {
if (cells.f[from] !== cells.f[to]) return false; // on different islands
const queue = [from],
used = new Uint8Array(cells.i.length),
state = cells.state[from];
while (queue.length) {
const current = queue.pop();
if (current === to) return true; // way is found
cells.c[current].forEach(c => {
if (used[c] || cells.h[c] < 20 || cells.state[c] !== state) return;
queue.push(c);
used[c] = 1;
});
}
return false; // way is not found
}
// re-check
stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !provinceIds[i]);
}
});
cells.province = provinceIds;
pack.provinces = provinces;
TIME && console.timeEnd("generateProvinces");
};
// calculate pole of inaccessibility for each province
const getPoles = () => {
const getType = cellId => pack.cells.province[cellId];
const poles = getPolesOfInaccessibility(pack, getType);
pack.provinces.forEach(province => {
if (!province.i || province.removed) return;
province.pole = poles[province.i] || [0, 0];
});
};
return {generate, getPoles};
})();

View file

@ -0,0 +1,120 @@
"use strict";
function drawBorders() {
TIME && console.time("drawBorders");
const {cells, vertices} = pack;
const statePath = [];
const provincePath = [];
const checked = {};
const isLand = cellId => cells.h[cellId] >= 20;
for (let cellId = 0; cellId < cells.i.length; cellId++) {
if (!cells.state[cellId]) continue;
const provinceId = cells.province[cellId];
const stateId = cells.state[cellId];
// bordering cell of another province
if (provinceId) {
const provToCell = cells.c[cellId].find(neibId => {
const neibProvinceId = cells.province[neibId];
return (
neibProvinceId &&
provinceId > neibProvinceId &&
!checked[`prov-${provinceId}-${neibProvinceId}-${cellId}`] &&
cells.state[neibId] === stateId
);
});
if (provToCell !== undefined) {
const addToChecked = cellId => (checked[`prov-${provinceId}-${cells.province[provToCell]}-${cellId}`] = true);
const border = getBorder({type: "province", fromCell: cellId, toCell: provToCell, addToChecked});
if (border) {
provincePath.push(border);
cellId--; // check the same cell again
continue;
}
}
}
// if cell is on state border
const stateToCell = cells.c[cellId].find(neibId => {
const neibStateId = cells.state[neibId];
return isLand(neibId) && stateId > neibStateId && !checked[`state-${stateId}-${neibStateId}-${cellId}`];
});
if (stateToCell !== undefined) {
const addToChecked = cellId => (checked[`state-${stateId}-${cells.state[stateToCell]}-${cellId}`] = true);
const border = getBorder({type: "state", fromCell: cellId, toCell: stateToCell, addToChecked});
if (border) {
statePath.push(border);
cellId--; // check the same cell again
continue;
}
}
}
svg.select("#borders").selectAll("path").remove();
svg.select("#stateBorders").append("path").attr("d", statePath.join(" "));
svg.select("#provinceBorders").append("path").attr("d", provincePath.join(" "));
function getBorder({type, fromCell, toCell, addToChecked}) {
const getType = cellId => cells[type][cellId];
const isTypeFrom = cellId => cellId < cells.i.length && getType(cellId) === getType(fromCell);
const isTypeTo = cellId => cellId < cells.i.length && getType(cellId) === getType(toCell);
addToChecked(fromCell);
const startingVertex = cells.v[fromCell].find(v => vertices.c[v].some(i => isLand(i) && isTypeTo(i)));
if (startingVertex === undefined) return null;
const checkVertex = vertex =>
vertices.c[vertex].some(isTypeFrom) && vertices.c[vertex].some(c => isLand(c) && isTypeTo(c));
const chain = getVerticesLine({vertices, startingVertex, checkCell: isTypeFrom, checkVertex, addToChecked});
if (chain.length > 1) return "M" + chain.map(cellId => vertices.p[cellId]).join(" ");
return null;
}
// connect vertices to chain to form a border
function getVerticesLine({vertices, startingVertex, checkCell, checkVertex, addToChecked}) {
let chain = []; // vertices chain to form a path
let next = startingVertex;
const MAX_ITERATIONS = vertices.c.length;
for (let run = 0; run < 2; run++) {
// first run: from any vertex to a border edge
// second run: from found border edge to another edge
chain = [];
for (let i = 0; i < MAX_ITERATIONS; i++) {
const previous = chain.at(-1);
const current = next;
chain.push(current);
const neibCells = vertices.c[current];
neibCells.map(addToChecked);
const [c1, c2, c3] = neibCells.map(checkCell);
const [v1, v2, v3] = vertices.v[current].map(checkVertex);
const [vertex1, vertex2, vertex3] = vertices.v[current];
if (v1 && vertex1 !== previous && c1 !== c2) next = vertex1;
else if (v2 && vertex2 !== previous && c2 !== c3) next = vertex2;
else if (v3 && vertex3 !== previous && c1 !== c3) next = vertex3;
if (next === current || next === startingVertex) {
if (next === startingVertex) chain.push(startingVertex);
startingVertex = next;
break;
}
}
}
return chain;
}
TIME && console.timeEnd("drawBorders");
}

View file

@ -0,0 +1,69 @@
"use strict";
function drawBurgIcons() {
TIME && console.time("drawBurgIcons");
icons.selectAll("circle, use").remove(); // cleanup
// capitals
const capitals = pack.burgs.filter(b => b.capital && !b.removed);
const capitalIcons = burgIcons.select("#cities");
const capitalSize = capitalIcons.attr("size") || 1;
const capitalAnchors = anchors.selectAll("#cities");
const capitalAnchorsSize = capitalAnchors.attr("size") || 2;
capitalIcons
.selectAll("circle")
.data(capitals)
.enter()
.append("circle")
.attr("id", d => "burg" + d.i)
.attr("data-id", d => d.i)
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", capitalSize);
capitalAnchors
.selectAll("use")
.data(capitals.filter(c => c.port))
.enter()
.append("use")
.attr("xlink:href", "#icon-anchor")
.attr("data-id", d => d.i)
.attr("x", d => rn(d.x - capitalAnchorsSize * 0.47, 2))
.attr("y", d => rn(d.y - capitalAnchorsSize * 0.47, 2))
.attr("width", capitalAnchorsSize)
.attr("height", capitalAnchorsSize);
// towns
const towns = pack.burgs.filter(b => b.i && !b.capital && !b.removed);
const townIcons = burgIcons.select("#towns");
const townSize = townIcons.attr("size") || 0.5;
const townsAnchors = anchors.selectAll("#towns");
const townsAnchorsSize = townsAnchors.attr("size") || 1;
townIcons
.selectAll("circle")
.data(towns)
.enter()
.append("circle")
.attr("id", d => "burg" + d.i)
.attr("data-id", d => d.i)
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", townSize);
townsAnchors
.selectAll("use")
.data(towns.filter(c => c.port))
.enter()
.append("use")
.attr("xlink:href", "#icon-anchor")
.attr("data-id", d => d.i)
.attr("x", d => rn(d.x - townsAnchorsSize * 0.47, 2))
.attr("y", d => rn(d.y - townsAnchorsSize * 0.47, 2))
.attr("width", townsAnchorsSize)
.attr("height", townsAnchorsSize);
TIME && console.timeEnd("drawBurgIcons");
}

View file

@ -0,0 +1,39 @@
"use strict";
function drawBurgLabels() {
TIME && console.time("drawBurgLabels");
burgLabels.selectAll("text").remove(); // cleanup
const capitals = pack.burgs.filter(b => b.capital && !b.removed);
const capitalSize = burgIcons.select("#cities").attr("size") || 1;
burgLabels
.select("#cities")
.selectAll("text")
.data(capitals)
.enter()
.append("text")
.attr("id", d => "burgLabel" + d.i)
.attr("data-id", d => d.i)
.attr("x", d => d.x)
.attr("y", d => d.y)
.attr("dy", `${capitalSize * -1.5}px`)
.text(d => d.name);
const towns = pack.burgs.filter(b => b.i && !b.capital && !b.removed);
const townSize = burgIcons.select("#towns").attr("size") || 0.5;
burgLabels
.select("#towns")
.selectAll("text")
.data(towns)
.enter()
.append("text")
.attr("id", d => "burgLabel" + d.i)
.attr("data-id", d => d.i)
.attr("x", d => d.x)
.attr("y", d => d.y)
.attr("dy", `${townSize * -2}px`)
.text(d => d.name);
TIME && console.timeEnd("drawBurgLabels");
}

View file

@ -0,0 +1,129 @@
"use strict";
function drawEmblems() {
TIME && console.time("drawEmblems");
const {states, provinces, burgs} = pack;
const validStates = states.filter(s => s.i && !s.removed && s.coa && s.coa.size !== 0);
const validProvinces = provinces.filter(p => p.i && !p.removed && p.coa && p.coa.size !== 0);
const validBurgs = burgs.filter(b => b.i && !b.removed && b.coa && b.coa.size !== 0);
const getStateEmblemsSize = () => {
const startSize = minmax((graphHeight + graphWidth) / 40, 10, 100);
const statesMod = 1 + validStates.length / 100 - (15 - validStates.length) / 200; // states number modifier
const sizeMod = +emblems.select("#stateEmblems").attr("data-size") || 1;
return rn((startSize / statesMod) * sizeMod); // target size ~50px on 1536x754 map with 15 states
};
const getProvinceEmblemsSize = () => {
const startSize = minmax((graphHeight + graphWidth) / 100, 5, 70);
const provincesMod = 1 + validProvinces.length / 1000 - (115 - validProvinces.length) / 1000; // states number modifier
const sizeMod = +emblems.select("#provinceEmblems").attr("data-size") || 1;
return rn((startSize / provincesMod) * sizeMod); // target size ~20px on 1536x754 map with 115 provinces
};
const getBurgEmblemSize = () => {
const startSize = minmax((graphHeight + graphWidth) / 185, 2, 50);
const burgsMod = 1 + validBurgs.length / 1000 - (450 - validBurgs.length) / 1000; // states number modifier
const sizeMod = +emblems.select("#burgEmblems").attr("data-size") || 1;
return rn((startSize / burgsMod) * sizeMod); // target size ~8.5px on 1536x754 map with 450 burgs
};
const sizeBurgs = getBurgEmblemSize();
const burgCOAs = validBurgs.map(burg => {
const {x, y} = burg;
const size = burg.coa.size || 1;
const shift = (sizeBurgs * size) / 2;
return {type: "burg", i: burg.i, x: burg.coa.x || x, y: burg.coa.y || y, size, shift};
});
const sizeProvinces = getProvinceEmblemsSize();
const provinceCOAs = validProvinces.map(province => {
const [x, y] = province.pole || pack.cells.p[province.center];
const size = province.coa.size || 1;
const shift = (sizeProvinces * size) / 2;
return {type: "province", i: province.i, x: province.coa.x || x, y: province.coa.y || y, size, shift};
});
const sizeStates = getStateEmblemsSize();
const stateCOAs = validStates.map(state => {
const [x, y] = state.pole || pack.cells.p[state.center];
const size = state.coa.size || 1;
const shift = (sizeStates * size) / 2;
return {type: "state", i: state.i, x: state.coa.x || x, y: state.coa.y || y, size, shift};
});
const nodes = burgCOAs.concat(provinceCOAs).concat(stateCOAs);
const simulation = d3
.forceSimulation(nodes)
.alphaMin(0.6)
.alphaDecay(0.2)
.velocityDecay(0.6)
.force(
"collision",
d3.forceCollide().radius(d => d.shift)
)
.stop();
d3.timeout(function () {
const n = Math.ceil(Math.log(simulation.alphaMin()) / Math.log(1 - simulation.alphaDecay()));
for (let i = 0; i < n; ++i) {
simulation.tick();
}
const burgNodes = nodes.filter(node => node.type === "burg");
const burgString = burgNodes
.map(
d =>
`<use data-i="${d.i}" x="${rn(d.x - d.shift)}" y="${rn(d.y - d.shift)}" width="${d.size}em" height="${
d.size
}em"/>`
)
.join("");
emblems.select("#burgEmblems").attr("font-size", sizeBurgs).html(burgString);
const provinceNodes = nodes.filter(node => node.type === "province");
const provinceString = provinceNodes
.map(
d =>
`<use data-i="${d.i}" x="${rn(d.x - d.shift)}" y="${rn(d.y - d.shift)}" width="${d.size}em" height="${
d.size
}em"/>`
)
.join("");
emblems.select("#provinceEmblems").attr("font-size", sizeProvinces).html(provinceString);
const stateNodes = nodes.filter(node => node.type === "state");
const stateString = stateNodes
.map(
d =>
`<use data-i="${d.i}" x="${rn(d.x - d.shift)}" y="${rn(d.y - d.shift)}" width="${d.size}em" height="${
d.size
}em"/>`
)
.join("");
emblems.select("#stateEmblems").attr("font-size", sizeStates).html(stateString);
invokeActiveZooming();
});
TIME && console.timeEnd("drawEmblems");
}
const getDataAndType = id => {
if (id === "burgEmblems") return [pack.burgs, "burg"];
if (id === "provinceEmblems") return [pack.provinces, "province"];
if (id === "stateEmblems") return [pack.states, "state"];
throw new Error(`Unknown emblem type: ${id}`);
};
async function renderGroupCOAs(g) {
const [data, type] = getDataAndType(g.id);
for (let use of g.children) {
const i = +use.dataset.i;
const id = type + "COA" + i;
COArenderer.trigger(id, data[i].coa);
use.setAttribute("href", "#" + id);
}
}

View file

@ -0,0 +1,61 @@
"use strict";
function drawFeatures() {
TIME && console.time("drawFeatures");
const featurePaths = defs.select("#featurePaths");
const landMask = defs.select("#land");
const waterMask = defs.select("#water");
for (const feature of pack.features) {
if (!feature || feature.type === "ocean") continue;
featurePaths
.append("path")
.attr("d", getFeaturePath(feature))
.attr("id", "feature_" + feature.i)
.attr("data-f", feature.i);
if (feature.type === "lake") {
landMask
.append("use")
.attr("href", "#feature_" + feature.i)
.attr("data-f", feature.i)
.attr("fill", "black");
lakes
.select(`#${feature.group}`)
.append("use")
.attr("href", "#feature_" + feature.i)
.attr("data-f", feature.i);
} else {
landMask
.append("use")
.attr("href", "#feature_" + feature.i)
.attr("data-f", feature.i)
.attr("fill", "white");
waterMask
.append("use")
.attr("href", "#feature_" + feature.i)
.attr("data-f", feature.i)
.attr("fill", "black");
const coastlineGroup = feature.group === "lake_island" ? "#lake_island" : "#sea_island";
coastline
.select(coastlineGroup)
.append("use")
.attr("href", "#feature_" + feature.i)
.attr("data-f", feature.i);
}
}
TIME && console.timeEnd("drawFeatures");
}
function getFeaturePath(feature) {
const points = feature.vertices.map(vertex => pack.vertices.p[vertex]);
const simplifiedPoints = simplify(points, 0.3);
const clippedPoints = clipPoly(simplifiedPoints, 1);
const lineGen = d3.line().curve(d3.curveBasisClosed);
const path = round(lineGen(clippedPoints));
return path;
}

View file

@ -0,0 +1,144 @@
"use strict";
function drawHeightmap() {
TIME && console.time("drawHeightmap");
const ocean = terrs.select("#oceanHeights");
const land = terrs.select("#landHeights");
ocean.selectAll("*").remove();
land.selectAll("*").remove();
const paths = new Array(101);
const {cells, vertices} = grid;
const used = new Uint8Array(cells.i.length);
const heights = Array.from(cells.i).sort((a, b) => cells.h[a] - cells.h[b]);
// ocean cells
const renderOceanCells = Boolean(+ocean.attr("data-render"));
if (renderOceanCells) {
const skip = +ocean.attr("skip") + 1 || 1;
const relax = +ocean.attr("relax") || 0;
lineGen.curve(d3[ocean.attr("curve") || "curveBasisClosed"]);
let currentLayer = 0;
for (const i of heights) {
const h = cells.h[i];
if (h > currentLayer) currentLayer += skip;
if (h < currentLayer) continue;
if (currentLayer >= 20) break;
if (used[i]) continue; // already marked
const onborder = cells.c[i].some(n => cells.h[n] < h);
if (!onborder) continue;
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.h[i] < h));
const chain = connectVertices(cells, vertices, vertex, h, used);
if (chain.length < 3) continue;
const points = simplifyLine(chain, relax).map(v => vertices.p[v]);
if (!paths[h]) paths[h] = "";
paths[h] += round(lineGen(points));
}
}
// land cells
{
const skip = +land.attr("skip") + 1 || 1;
const relax = +land.attr("relax") || 0;
lineGen.curve(d3[land.attr("curve") || "curveBasisClosed"]);
let currentLayer = 20;
for (const i of heights) {
const h = cells.h[i];
if (h > currentLayer) currentLayer += skip;
if (h < currentLayer) continue;
if (currentLayer > 100) break; // no layers possible with height > 100
if (used[i]) continue; // already marked
const onborder = cells.c[i].some(n => cells.h[n] < h);
if (!onborder) continue;
const startVertex = cells.v[i].find(v => vertices.c[v].some(i => cells.h[i] < h));
const chain = connectVertices(cells, vertices, startVertex, h, used);
if (chain.length < 3) continue;
const points = simplifyLine(chain, relax).map(v => vertices.p[v]);
if (!paths[h]) paths[h] = "";
paths[h] += round(lineGen(points));
}
}
// render paths
for (const height of d3.range(0, 101)) {
const group = height < 20 ? ocean : land;
const scheme = getColorScheme(group.attr("scheme"));
if (height === 0 && renderOceanCells) {
// draw base ocean layer
group
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", graphWidth)
.attr("height", graphHeight)
.attr("fill", scheme(1));
}
if (height === 20) {
// draw base land layer
group
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", graphWidth)
.attr("height", graphHeight)
.attr("fill", scheme(0.8));
}
if (paths[height] && paths[height].length >= 10) {
const terracing = group.attr("terracing") / 10 || 0;
const color = getColor(height, scheme);
if (terracing) {
group
.append("path")
.attr("d", paths[height])
.attr("transform", "translate(.7,1.4)")
.attr("fill", d3.color(color).darker(terracing))
.attr("data-height", height);
}
group.append("path").attr("d", paths[height]).attr("fill", color).attr("data-height", height);
}
}
// connect vertices to chain: specific case for heightmap
function connectVertices(cells, vertices, start, h, used) {
const MAX_ITERATIONS = vertices.c.length;
const n = cells.i.length;
const chain = []; // vertices chain to form a path
for (let i = 0, current = start; i === 0 || (current !== start && i < MAX_ITERATIONS); i++) {
const prev = chain[chain.length - 1]; // previous vertex in chain
chain.push(current); // add current vertex to sequence
const c = vertices.c[current]; // cells adjacent to vertex
c.filter(c => cells.h[c] === h).forEach(c => (used[c] = 1));
const c0 = c[0] >= n || cells.h[c[0]] < h;
const c1 = c[1] >= n || cells.h[c[1]] < h;
const c2 = c[2] >= n || cells.h[c[2]] < h;
const v = vertices.v[current]; // neighboring vertices
if (v[0] !== prev && c0 !== c1) current = v[0];
else if (v[1] !== prev && c1 !== c2) current = v[1];
else if (v[2] !== prev && c0 !== c2) current = v[2];
if (current === chain[chain.length - 1]) {
ERROR && console.error("Next vertex is not found");
break;
}
}
return chain;
}
function simplifyLine(chain, simplification) {
if (!simplification) return chain;
const n = simplification + 1; // filter each nth element
return chain.filter((d, i) => i % n === 0);
}
TIME && console.timeEnd("drawHeightmap");
}

View file

@ -0,0 +1,50 @@
"use strict";
function drawMarkers() {
TIME && console.time("drawMarkers");
const rescale = +markers.attr("rescale");
const pinned = +markers.attr("pinned");
const markersData = pinned ? pack.markers.filter(({pinned}) => pinned) : pack.markers;
const html = markersData.map(marker => drawMarker(marker, rescale));
markers.html(html.join(""));
TIME && console.timeEnd("drawMarkers");
}
// prettier-ignore
const pinShapes = {
bubble: (fill, stroke) => `<path d="M6,19 l9,10 L24,19" fill="${stroke}" stroke="none" /><circle cx="15" cy="15" r="10" fill="${fill}" stroke="${stroke}"/>`,
pin: (fill, stroke) => `<path d="m 15,3 c -5.5,0 -9.7,4.09 -9.7,9.3 0,6.8 9.7,17 9.7,17 0,0 9.7,-10.2 9.7,-17 C 24.7,7.09 20.5,3 15,3 Z" fill="${fill}" stroke="${stroke}"/>`,
square: (fill, stroke) => `<path d="m 20,25 -5,4 -5,-4 z" fill="${stroke}"/><path d="M 5,5 H 25 V 25 H 5 Z" fill="${fill}" stroke="${stroke}"/>`,
squarish: (fill, stroke) => `<path d="m 5,5 h 20 v 20 h -6 l -4,4 -4,-4 H 5 Z" fill="${fill}" stroke="${stroke}" />`,
diamond: (fill, stroke) => `<path d="M 2,15 15,1 28,15 15,29 Z" fill="${fill}" stroke="${stroke}" />`,
hex: (fill, stroke) => `<path d="M 15,29 4.61,21 V 9 L 15,3 25.4,9 v 12 z" fill="${fill}" stroke="${stroke}" />`,
hexy: (fill, stroke) => `<path d="M 15,29 6,21 5,8 15,4 25,8 24,21 Z" fill="${fill}" stroke="${stroke}" />`,
shieldy: (fill, stroke) => `<path d="M 15,29 6,21 5,7 c 0,0 5,-3 10,-3 5,0 10,3 10,3 l -1,14 z" fill="${fill}" stroke="${stroke}" />`,
shield: (fill, stroke) => `<path d="M 4.6,5.2 H 25 v 6.7 A 20.3,20.4 0 0 1 15,29 20.3,20.4 0 0 1 4.6,11.9 Z" fill="${fill}" stroke="${stroke}" />`,
pentagon: (fill, stroke) => `<path d="M 4,16 9,4 h 12 l 5,12 -11,13 z" fill="${fill}" stroke="${stroke}" />`,
heptagon: (fill, stroke) => `<path d="M 15,29 6,22 4,12 10,4 h 10 l 6,8 -2,10 z" fill="${fill}" stroke="${stroke}" />`,
circle: (fill, stroke) => `<circle cx="15" cy="15" r="11" fill="${fill}" stroke="${stroke}" />`,
no: () => ""
};
const getPin = (shape = "bubble", fill = "#fff", stroke = "#000") => {
const shapeFunction = pinShapes[shape] || pinShapes.bubble;
return shapeFunction(fill, stroke);
};
function drawMarker(marker, rescale = 1) {
const {i, icon, x, y, dx = 50, dy = 50, px = 12, size = 30, pin, fill, stroke} = marker;
const id = `marker${i}`;
const zoomSize = rescale ? Math.max(rn(size / 5 + 24 / scale, 2), 1) : size;
const viewX = rn(x - zoomSize / 2, 1);
const viewY = rn(y - zoomSize, 1);
return /* html */ `
<svg id="${id}" viewbox="0 0 30 30" width="${zoomSize}" height="${zoomSize}" x="${viewX}" y="${viewY}">
<g>${getPin(pin, fill, stroke)}</g>
<text x="${dx}%" y="${dy}%" font-size="${px}px" >${icon}</text>
</svg>`;
}

View file

@ -0,0 +1,126 @@
"use strict";
function drawMilitary() {
TIME && console.time("drawMilitary");
armies.selectAll("g").remove();
pack.states.filter(s => s.i && !s.removed).forEach(s => drawRegiments(s.military, s.i));
TIME && console.timeEnd("drawMilitary");
}
const drawRegiments = function (regiments, s) {
const size = +armies.attr("box-size");
const w = d => (d.n ? size * 4 : size * 6);
const h = size * 2;
const x = d => rn(d.x - w(d) / 2, 2);
const y = d => rn(d.y - size, 2);
const baseColor = pack.states[s].color[0] === "#" ? pack.states[s].color : "#999";
const darkerColor = d3.color(baseColor).darker().hex();
const army = armies
.append("g")
.attr("id", "army" + s)
.attr("fill", baseColor)
.attr("color", darkerColor);
const g = army
.selectAll("g")
.data(regiments)
.enter()
.append("g")
.attr("id", d => "regiment" + s + "-" + d.i)
.attr("data-name", d => d.name)
.attr("data-state", s)
.attr("data-id", d => d.i)
.attr("transform", d => (d.angle ? `rotate(${d.angle})` : null))
.attr("transform-origin", d => `${d.x}px ${d.y}px`);
g.append("rect")
.attr("x", d => x(d))
.attr("y", d => y(d))
.attr("width", d => w(d))
.attr("height", h);
g.append("text")
.attr("x", d => d.x)
.attr("y", d => d.y)
.text(d => Military.getTotal(d));
g.append("rect")
.attr("fill", "currentColor")
.attr("x", d => x(d) - h)
.attr("y", d => y(d))
.attr("width", h)
.attr("height", h);
g.append("text")
.attr("class", "regimentIcon")
.attr("x", d => x(d) - size)
.attr("y", d => d.y)
.text(d => d.icon);
};
const drawRegiment = function (reg, stateId) {
const size = +armies.attr("box-size");
const w = reg.n ? size * 4 : size * 6;
const h = size * 2;
const x1 = rn(reg.x - w / 2, 2);
const y1 = rn(reg.y - size, 2);
let army = armies.select("g#army" + stateId);
if (!army.size()) {
const baseColor = pack.states[stateId].color[0] === "#" ? pack.states[stateId].color : "#999";
const darkerColor = d3.color(baseColor).darker().hex();
army = armies
.append("g")
.attr("id", "army" + stateId)
.attr("fill", baseColor)
.attr("color", darkerColor);
}
const g = army
.append("g")
.attr("id", "regiment" + stateId + "-" + reg.i)
.attr("data-name", reg.name)
.attr("data-state", stateId)
.attr("data-id", reg.i)
.attr("transform", `rotate(${reg.angle || 0})`)
.attr("transform-origin", `${reg.x}px ${reg.y}px`);
g.append("rect").attr("x", x1).attr("y", y1).attr("width", w).attr("height", h);
g.append("text").attr("x", reg.x).attr("y", reg.y).text(getTotal(reg));
g.append("rect")
.attr("fill", "currentColor")
.attr("x", x1 - h)
.attr("y", y1)
.attr("width", h)
.attr("height", h);
g.append("text")
.attr("class", "regimentIcon")
.attr("x", x1 - size)
.attr("y", reg.y)
.text(reg.icon);
};
// move one regiment to another
const moveRegiment = function (reg, x, y) {
const el = armies.select("g#army" + reg.state).select("g#regiment" + reg.state + "-" + reg.i);
if (!el.size()) return;
const duration = Math.hypot(reg.x - x, reg.y - y) * 8;
reg.x = x;
reg.y = y;
const size = +armies.attr("box-size");
const w = reg.n ? size * 4 : size * 6;
const h = size * 2;
const x1 = x => rn(x - w / 2, 2);
const y1 = y => rn(y - size, 2);
const move = d3.transition().duration(duration).ease(d3.easeSinInOut);
el.select("rect").transition(move).attr("x", x1(x)).attr("y", y1(y));
el.select("text").transition(move).attr("x", x).attr("y", y);
el.selectAll("rect:nth-of-type(2)")
.transition(move)
.attr("x", x1(x) - h)
.attr("y", y1(y));
el.select(".regimentIcon")
.transition(move)
.attr("x", x1(x) - size)
.attr("y", y);
};

View file

@ -0,0 +1,101 @@
"use strict";
function drawScaleBar(scaleBar, scaleLevel) {
if (!scaleBar.size() || scaleBar.style("display") === "none") return;
const unit = distanceUnitInput.value;
const size = +scaleBar.attr("data-bar-size");
const length = getLength(scaleLevel, size);
scaleBar.select("#scaleBarContent").remove(); // redraw content every time
const content = scaleBar.append("g").attr("id", "scaleBarContent");
const lines = content.append("g");
lines
.append("line")
.attr("x1", 0.5)
.attr("y1", 0)
.attr("x2", length + size - 0.5)
.attr("y2", 0)
.attr("stroke-width", size)
.attr("stroke", "white");
lines
.append("line")
.attr("x1", 0)
.attr("y1", size)
.attr("x2", length + size)
.attr("y2", size)
.attr("stroke-width", size)
.attr("stroke", "#3d3d3d");
lines
.append("line")
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", length + size)
.attr("y2", 0)
.attr("stroke-width", rn(size * 3, 2))
.attr("stroke-dasharray", size + " " + rn(length / 5 - size, 2))
.attr("stroke", "#3d3d3d");
const texts = content.append("g").attr("text-anchor", "middle").attr("font-family", "var(--serif)");
texts
.selectAll("text")
.data(d3.range(0, 6))
.enter()
.append("text")
.attr("x", d => rn((d * length) / 5, 2))
.attr("y", 0)
.attr("dy", "-.6em")
.text(d => rn((((d * length) / 5) * distanceScale) / scaleLevel) + (d < 5 ? "" : " " + unit));
const label = scaleBar.attr("data-label");
if (label) {
texts
.append("text")
.attr("x", (length + 1) / 2)
.attr("dy", ".6em")
.attr("dominant-baseline", "text-before-edge")
.text(label);
}
const scaleBarBack = scaleBar.select("#scaleBarBack");
if (scaleBarBack.size()) {
const bbox = content.node().getBBox();
const paddingTop = +scaleBarBack.attr("data-top") || 0;
const paddingLeft = +scaleBarBack.attr("data-left") || 0;
const paddingRight = +scaleBarBack.attr("data-right") || 0;
const paddingBottom = +scaleBarBack.attr("data-bottom") || 0;
scaleBar
.select("#scaleBarBack")
.attr("x", -paddingLeft)
.attr("y", -paddingTop)
.attr("width", bbox.width + paddingRight)
.attr("height", bbox.height + paddingBottom);
}
}
function getLength(scaleLevel) {
const init = 100;
const size = +scaleBar.attr("data-bar-size");
let val = (init * size * distanceScale) / scaleLevel; // bar length in distance unit
if (val > 900) val = rn(val, -3); // round to 1000
else if (val > 90) val = rn(val, -2); // round to 100
else if (val > 9) val = rn(val, -1); // round to 10
else val = rn(val); // round to 1
const length = (val * scaleLevel) / distanceScale; // actual length in pixels on this scale
return length;
}
function fitScaleBar(scaleBar, fullWidth, fullHeight) {
if (!scaleBar.select("rect").size() || scaleBar.style("display") === "none") return;
const posX = +scaleBar.attr("data-x") || 99;
const posY = +scaleBar.attr("data-y") || 99;
const bbox = scaleBar.select("rect").node().getBBox();
const x = rn((fullWidth * posX) / 100 - bbox.width + 10);
const y = rn((fullHeight * posY) / 100 - bbox.height + 20);
scaleBar.attr("transform", `translate(${x},${y})`);
}

View file

@ -2,7 +2,7 @@
// list - an optional array of stateIds to regenerate
function drawStateLabels(list) {
console.time("drawStateLabels");
TIME && console.time("drawStateLabels");
// temporary make the labels visible
const layerDisplay = labels.style("display");
@ -289,5 +289,5 @@ function drawStateLabels(list) {
return false;
}
console.timeEnd("drawStateLabels");
TIME && console.timeEnd("drawStateLabels");
}

View file

@ -0,0 +1,104 @@
"use strict";
function drawTemperature() {
TIME && console.time("drawTemperature");
temperature.selectAll("*").remove();
lineGen.curve(d3.curveBasisClosed);
const scheme = d3.scaleSequential(d3.interpolateSpectral);
const tMax = +byId("temperatureEquatorOutput").max;
const tMin = +byId("temperatureEquatorOutput").min;
const delta = tMax - tMin;
const {cells, vertices} = grid;
const n = cells.i.length;
const checkedCells = new Uint8Array(n);
const addToChecked = cellId => (checkedCells[cellId] = 1);
const min = d3.min(cells.temp);
const max = d3.max(cells.temp);
const step = Math.max(Math.round(Math.abs(min - max) / 5), 1);
const isolines = d3.range(min + step, max, step);
const chains = [];
const labels = []; // store label coordinates
for (const cellId of cells.i) {
const t = cells.temp[cellId];
if (checkedCells[cellId] || !isolines.includes(t)) continue;
const startingVertex = findStart(cellId, t);
if (!startingVertex) continue;
checkedCells[cellId] = 1;
const ofSameType = cellId => cells.temp[cellId] >= t;
const chain = connectVertices({vertices, startingVertex, ofSameType, addToChecked});
const relaxed = chain.filter((v, i) => i % 4 === 0 || vertices.c[v].some(c => c >= n));
if (relaxed.length < 6) continue;
const points = relaxed.map(v => vertices.p[v]);
chains.push([t, points]);
addLabel(points, t);
}
// min temp isoline covers all graph
temperature
.append("path")
.attr("d", `M0,0 h${graphWidth} v${graphHeight} h${-graphWidth} Z`)
.attr("fill", scheme(1 - (min - tMin) / delta))
.attr("stroke", "none");
for (const t of isolines) {
const path = chains
.filter(c => c[0] === t)
.map(c => round(lineGen(c[1])))
.join("");
if (!path) continue;
const fill = scheme(1 - (t - tMin) / delta),
stroke = d3.color(fill).darker(0.2);
temperature.append("path").attr("d", path).attr("fill", fill).attr("stroke", stroke);
}
const tempLabels = temperature.append("g").attr("id", "tempLabels").attr("fill-opacity", 1);
tempLabels
.selectAll("text")
.data(labels)
.enter()
.append("text")
.attr("x", d => d[0])
.attr("y", d => d[1])
.text(d => convertTemperature(d[2]));
// find cell with temp < isotherm and find vertex to start path detection
function findStart(i, t) {
if (cells.b[i]) return cells.v[i].find(v => vertices.c[v].some(c => c >= n)); // map border cell
return cells.v[i][cells.c[i].findIndex(c => cells.temp[c] < t || !cells.temp[c])];
}
function addLabel(points, t) {
const xCenter = svgWidth / 2;
// add label on isoline top center
const tc =
points[d3.scan(points, (a, b) => a[1] - b[1] + (Math.abs(a[0] - xCenter) - Math.abs(b[0] - xCenter)) / 2)];
pushLabel(tc[0], tc[1], t);
// add label on isoline bottom center
if (points.length > 20) {
const bc =
points[d3.scan(points, (a, b) => b[1] - a[1] + (Math.abs(a[0] - xCenter) - Math.abs(b[0] - xCenter)) / 2)];
const dist2 = (tc[1] - bc[1]) ** 2 + (tc[0] - bc[0]) ** 2; // square distance between this and top point
if (dist2 > 100) pushLabel(bc[0], bc[1], t);
}
}
function pushLabel(x, y, t) {
if (x < 20 || x > svgWidth - 20) return;
if (y < 20 || y > svgHeight - 20) return;
labels.push([x, y, t]);
}
TIME && console.timeEnd("drawTemperature");
}

View file

@ -8,6 +8,7 @@ window.Rivers = (function () {
const riversData = {}; // rivers data
const riverParents = {};
const addCellToRiver = function (cell, river) {
if (!riversData[river]) riversData[river] = [cell];
else riversData[river].push(cell);
@ -19,7 +20,7 @@ window.Rivers = (function () {
let riverNext = 1; // first river id is 1
const h = alterHeights();
Lakes.prepareLakeData(h);
Lakes.detectCloseLakes(h);
resolveDepressions(h);
drainWater();
defineRivers();
@ -39,9 +40,8 @@ window.Rivers = (function () {
const cellsNumberModifier = (pointsInput.dataset.cells / 10000) ** 0.25;
const prec = grid.cells.prec;
const area = pack.cells.area;
const land = cells.i.filter(i => h[i] >= 20).sort((a, b) => h[b] - h[a]);
const lakeOutCells = Lakes.setClimateData(h);
const lakeOutCells = Lakes.defineClimateData(h);
land.forEach(function (i) {
cells.fl[i] += prec[cells.g[i]] / cellsNumberModifier; // add flux from precipitation

View file

@ -1,31 +1,26 @@
"use strict";
/*
Cell resampler module used by submapper and resampler (transform)
main function: resample(options);
*/
window.Submap = (function () {
const isWater = (pack, id) => pack.cells.h[id] < 20;
const inMap = (x, y) => x > 0 && x < graphWidth && y > 0 && y < graphHeight;
function resample(parentMap, options) {
/*
/*
generate new map based on an existing one (resampling parentMap)
parentMap: {seed, grid, pack} from original map
options = {
projection: f(Number,Number)->[Number, Number]
function to calculate new coordinates
inverse: g(Number,Number)->[Number, Number]
inverse of f
depressRivers: Bool carve out riverbeds?
smoothHeightMap: Bool run smooth filter on heights
addLakesInDepressions: call FMG original funtion on heightmap
projection: f(Number,Number)->[Number, Number]
function to calculate new coordinates
inverse: g(Number,Number)->[Number, Number]
inverse of f
depressRivers: Bool carve out riverbeds?
smoothHeightMap: Bool run smooth filter on heights
addLakesInDepressions: call FMG original funtion on heightmap
lockMarkers: Bool Auto lock all copied markers
lockBurgs: Bool Auto lock all copied burgs
lockMarkers: Bool Auto lock all copied markers
lockBurgs: Bool Auto lock all copied burgs
}
*/
function resample(parentMap, options) {
const projection = options.projection;
const inverse = options.inverse;
const stage = s => INFO && console.info("SUBMAP:", s);
@ -38,7 +33,6 @@ window.Submap = (function () {
INFO && console.group("SubMap with seed: " + seed);
DEBUG && console.info("Using Options:", options);
// create new grid
applyGraphSize();
grid = generateGrid();
@ -53,7 +47,7 @@ window.Submap = (function () {
}
};
stage("Resampling heightmap, temperature and precipitation.");
stage("Resampling heightmap, temperature and precipitation");
// resample heightmap from old WorldState
const n = grid.points.length;
grid.cells.h = new Uint8Array(n); // heightmap
@ -87,7 +81,7 @@ window.Submap = (function () {
}
if (options.depressRivers) {
stage("Generating riverbeds.");
stage("Generating riverbeds");
const rbeds = new Uint16Array(grid.cells.i.length);
// and erode riverbeds
@ -96,7 +90,7 @@ window.Submap = (function () {
if (oldpc < 0) return; // ignore out-of-map marker (-1)
const oldc = parentMap.pack.cells.g[oldpc];
const targetCells = forwardGridMap[oldc];
if (!targetCells) throw "TargetCell shouldn't be empty.";
if (!targetCells) throw "TargetCell shouldn't be empty";
targetCells.forEach(c => {
if (grid.cells.h[c] < 20) return;
rbeds[c] = 1;
@ -110,33 +104,27 @@ window.Submap = (function () {
});
}
stage("Detect features, ocean and generating lakes.");
markFeatures();
markupGridOcean();
stage("Detect features, ocean and generating lakes");
Features.markupGrid();
// Warning: addLakesInDeepDepressions can be very slow!
if (options.addLakesInDepressions) {
addLakesInDeepDepressions();
openNearSeaLakes();
}
addLakesInDeepDepressions();
openNearSeaLakes();
OceanLayers();
calculateMapCoordinates();
// calculateTemperatures();
// generatePrecipitation();
stage("Cell cleanup.");
calculateTemperatures();
generatePrecipitation();
stage("Cell cleanup");
reGraph();
// remove misclassified cells
stage("Define coastline.");
drawCoastline();
stage("Define coastline");
Features.markupPack();
createDefaultRuler();
/****************************************************/
/* Packed Graph */
/****************************************************/
// Packed Graph
const oldCells = parentMap.pack.cells;
// const reverseMap = new Map(); // cellmap from new -> oldcell
const forwardMap = parentMap.pack.cells.p.map(_ => []); // old -> [newcelllist]
const pn = pack.cells.i.length;
@ -147,7 +135,7 @@ window.Submap = (function () {
cells.religion = new Uint16Array(pn);
cells.province = new Uint16Array(pn);
stage("Resampling culture, state and religion map.");
stage("Resampling culture, state and religion map");
for (const [id, gridCellId] of cells.g.entries()) {
const oldGridId = reverseGridMap[gridCellId];
if (oldGridId === undefined) {
@ -206,14 +194,12 @@ window.Submap = (function () {
forwardMap[oldid].push(id);
}
stage("Regenerating river network.");
stage("Regenerating river network");
Rivers.generate();
drawRivers();
Lakes.defineGroup();
// biome calculation based on (resampled) grid.cells.temp and prec
// it's safe to recalculate.
stage("Regenerating Biome.");
stage("Regenerating Biome");
Biomes.define();
// recalculate suitability and population
// TODO: normalize according to the base-map
@ -234,11 +220,11 @@ window.Submap = (function () {
c.center = newCenters.length ? newCenters[0] : pack.cells.culture.findIndex(x => x === i);
});
stage("Porting and locking burgs.");
stage("Porting and locking burgs");
copyBurgs(parentMap, projection, options);
// transfer states, mark states without land as removed.
stage("Porting states.");
stage("Porting states");
const validStates = new Set(pack.cells.state);
pack.states = parentMap.pack.states;
// keep valid states and neighbors only
@ -252,9 +238,10 @@ window.Submap = (function () {
? pack.burgs[s.capital].cell // capital is the best bet
: pack.cells.state.findIndex(x => x === i); // otherwise use the first valid cell
});
BurgsAndStates.getPoles();
// transfer provinces, mark provinces without land as removed.
stage("Porting provinces.");
stage("Porting provinces");
const validProvinces = new Set(pack.cells.province);
pack.provinces = parentMap.pack.provinces;
// mark uneccesary provinces
@ -267,20 +254,15 @@ window.Submap = (function () {
const newCenters = forwardMap[p.center];
p.center = newCenters.length ? newCenters[0] : pack.cells.province.findIndex(x => x === i);
});
Provinces.getPoles();
BurgsAndStates.drawBurgs();
stage("Regenerating routes network.");
stage("Regenerating routes network");
regenerateRoutes();
drawStates();
drawBorders();
drawStateLabels();
Rivers.specify();
Lakes.generateName();
Features.specify();
stage("Porting military.");
stage("Porting military");
for (const s of pack.states) {
if (!s.military) continue;
for (const m of s.military) {
@ -291,9 +273,8 @@ window.Submap = (function () {
}
s.military = s.military.filter(m => m.cell).map((m, i) => ({...m, i}));
}
Military.redraw();
stage("Copying markers.");
stage("Copying markers");
for (const m of pack.markers) {
const [x, y] = projection(m.x, m.y);
if (!inMap(x, y)) {
@ -307,14 +288,12 @@ window.Submap = (function () {
}
if (layerIsOn("toggleMarkers")) drawMarkers();
stage("Redraw emblems.");
drawEmblems();
stage("Regenerating Zones.");
stage("Regenerating Zones");
Zones.generate();
Names.getMapName();
stage("Restoring Notes.");
stage("Restoring Notes");
notes = parentMap.notes;
stage("Submap done.");
stage("Submap done");
WARN && console.warn(`TOTAL: ${rn((performance.now() - timeStart) / 1000, 2)}s`);
showStatistics();

View file

@ -373,7 +373,7 @@ window.ThreeD = (function () {
}
// icons
if (layerIsOn("toggleIcons")) {
if (layerIsOn("toggleBurgIcons")) {
const geometry = isCity ? city_icon_geometry : town_icon_geometry;
const material = isCity ? city_icon_material : town_icon_material;
const iconMesh = new THREE.Mesh(geometry, material);
@ -444,6 +444,7 @@ window.ThreeD = (function () {
const url = await getMapURL("mesh", {
noLabels: options.labels3d,
noWater: options.extendedWater,
noViewbox: true,
fullMap: true
});
const canvas = document.createElement("canvas");
@ -623,7 +624,7 @@ window.ThreeD = (function () {
material.map = texture;
if (addMesh) addGlobe3dMesh();
};
img2.src = await getMapURL("mesh", {noScaleBar: true, fullMap: true});
img2.src = await getMapURL("mesh", {noScaleBar: true, fullMap: true, noVignette: true});
}
function addGlobe3dMesh() {

View file

@ -277,7 +277,7 @@ class Battle {
const shift = side === "attackers" ? attackers.length * -8 : (defenders.length - 1) * 8;
regiment.px = regiment.x;
regiment.py = regiment.y;
Military.moveRegiment(regiment, defenders[0].x, defenders[0].y + shift);
moveRegiment(regiment, defenders[0].x, defenders[0].y + shift);
});
}
@ -909,7 +909,7 @@ class Battle {
cancelResults() {
// move regiments back to initial positions
this.attackers.regiments.concat(this.defenders.regiments).forEach(r => Military.moveRegiment(r, r.px, r.py));
this.attackers.regiments.concat(this.defenders.regiments).forEach(r => moveRegiment(r, r.px, r.py));
$("#battleScreen").dialog("close");
this.cleanData();
}

View file

@ -2,7 +2,7 @@
function editBurg(id) {
if (customization) return;
closeDialogs(".stable");
if (!layerIsOn("toggleIcons")) toggleIcons();
if (!layerIsOn("toggleBurgIcons")) toggleBurgIcons();
if (!layerIsOn("toggleLabels")) toggleLabels();
const burg = id || d3.event.target.dataset.id;

View file

@ -2,7 +2,7 @@
function overviewBurgs(settings = {stateId: null, cultureId: null}) {
if (customization) return;
closeDialogs("#burgsOverview, .stable");
if (!layerIsOn("toggleIcons")) toggleIcons();
if (!layerIsOn("toggleBurgIcons")) toggleBurgIcons();
if (!layerIsOn("toggleLabels")) toggleLabels();
const body = byId("burgsBody");
@ -154,9 +154,9 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) {
}
function burgHighlightOn(event) {
if (!layerIsOn("toggleLabels")) toggleLabels();
const burg = +event.target.dataset.id;
burgLabels.select("[data-id='" + burg + "']").classed("drag", true);
const label = burgLabels.select("[data-id='" + burg + "']");
if (label.size()) label.classed("drag", true);
}
function burgHighlightOff() {

View file

@ -1,5 +1,6 @@
"use strict";
function editCoastline(node = d3.event.target) {
function editCoastline() {
if (customization) return;
closeDialogs(".stable");
if (layerIsOn("toggleCells")) toggleCells();
@ -12,6 +13,7 @@ function editCoastline(node = d3.event.target) {
});
debug.append("g").attr("id", "vertices");
const node = d3.event.target;
elSelected = d3.select(node);
selectCoastlineGroup(node);
drawCoastlineVertices();
@ -21,93 +23,98 @@ function editCoastline(node = d3.event.target) {
modules.editCoastline = true;
// add listeners
document.getElementById("coastlineGroupsShow").addEventListener("click", showGroupSection);
document.getElementById("coastlineGroup").addEventListener("change", changeCoastlineGroup);
document.getElementById("coastlineGroupAdd").addEventListener("click", toggleNewGroupInput);
document.getElementById("coastlineGroupName").addEventListener("change", createNewGroup);
document.getElementById("coastlineGroupRemove").addEventListener("click", removeCoastlineGroup);
document.getElementById("coastlineGroupsHide").addEventListener("click", hideGroupSection);
document.getElementById("coastlineEditStyle").addEventListener("click", editGroupStyle);
byId("coastlineGroupsShow").on("click", showGroupSection);
byId("coastlineGroup").on("change", changeCoastlineGroup);
byId("coastlineGroupAdd").on("click", toggleNewGroupInput);
byId("coastlineGroupName").on("change", createNewGroup);
byId("coastlineGroupRemove").on("click", removeCoastlineGroup);
byId("coastlineGroupsHide").on("click", hideGroupSection);
byId("coastlineEditStyle").on("click", editGroupStyle);
function drawCoastlineVertices() {
const f = +elSelected.attr("data-f"); // feature id
const v = pack.features[f].vertices; // coastline outer vertices
const featureId = +elSelected.attr("data-f");
const {vertices, area} = pack.features[featureId];
const l = pack.cells.i.length;
const c = [...new Set(v.map(v => pack.vertices.c[v]).flat())].filter(c => c < l);
const cellsNumber = pack.cells.i.length;
const neibCells = unique(vertices.map(v => pack.vertices.c[v]).flat()).filter(cellId => cellId < cellsNumber);
debug
.select("#vertices")
.selectAll("polygon")
.data(c)
.data(neibCells)
.enter()
.append("polygon")
.attr("points", d => getPackPolygon(d))
.attr("points", getPackPolygon)
.attr("data-c", d => d);
debug
.select("#vertices")
.selectAll("circle")
.data(v)
.data(vertices)
.enter()
.append("circle")
.attr("cx", d => pack.vertices.p[d][0])
.attr("cy", d => pack.vertices.p[d][1])
.attr("r", 0.4)
.attr("data-v", d => d)
.call(d3.drag().on("drag", dragVertex))
.on("mousemove", () => tip("Drag to move the vertex, please use for fine-tuning only. Edit heightmap to change actual cell heights"));
.call(d3.drag().on("drag", handleVertexDrag).on("end", handleVertexDragEnd))
.on("mousemove", () =>
tip("Drag to move the vertex. Please use for fine-tuning only. Edit heightmap to change actual cell heights!")
);
const area = pack.features[f].area;
coastlineArea.innerHTML = si(getArea(area)) + " " + getAreaUnit();
}
function dragVertex() {
const x = rn(d3.event.x, 2),
y = rn(d3.event.y, 2);
function handleVertexDrag() {
const {vertices, features} = pack;
const x = rn(d3.event.x, 2);
const y = rn(d3.event.y, 2);
this.setAttribute("cx", x);
this.setAttribute("cy", y);
const v = +this.dataset.v;
pack.vertices.p[v] = [x, y];
debug
.select("#vertices")
.selectAll("polygon")
.attr("points", d => getPackPolygon(d));
redrawCoastline();
const vertexId = d3.select(this).datum();
vertices.p[vertexId] = [x, y];
const featureId = +elSelected.attr("data-f");
const feature = features[featureId];
// change coastline path
defs.select("#featurePaths > path#feature_" + featureId).attr("d", getFeaturePath(feature));
// update area
const points = feature.vertices.map(vertex => vertices.p[vertex]);
feature.area = Math.abs(d3.polygonArea(points));
coastlineArea.innerHTML = si(getArea(feature.area)) + " " + getAreaUnit();
// update cell
debug.select("#vertices").selectAll("polygon").attr("points", getPackPolygon);
}
function redrawCoastline() {
lineGen.curve(d3.curveBasisClosed);
const f = +elSelected.attr("data-f");
const vertices = pack.features[f].vertices;
const points = clipPoly(
vertices.map(v => pack.vertices.p[v]),
1
);
const d = round(lineGen(points));
elSelected.attr("d", d);
defs.select("mask#land > path#land_" + f).attr("d", d); // update land mask
defs.select("mask#water > path#water_" + f).attr("d", d); // update water mask
const area = Math.abs(d3.polygonArea(points));
coastlineArea.innerHTML = si(getArea(area)) + " " + getAreaUnit();
function handleVertexDragEnd() {
if (layerIsOn("toggleStates")) drawStates();
if (layerIsOn("toggleProvinces")) drawProvinces();
if (layerIsOn("toggleBorders")) drawBorders();
if (layerIsOn("toggleBiomes")) drawBiomes();
if (layerIsOn("toggleReligions")) drawReligions();
if (layerIsOn("toggleCultures")) drawCultures();
}
function showGroupSection() {
document.querySelectorAll("#coastlineEditor > button").forEach(el => (el.style.display = "none"));
document.getElementById("coastlineGroupsSelection").style.display = "inline-block";
byId("coastlineGroupsSelection").style.display = "inline-block";
}
function hideGroupSection() {
document.querySelectorAll("#coastlineEditor > button").forEach(el => (el.style.display = "inline-block"));
document.getElementById("coastlineGroupsSelection").style.display = "none";
document.getElementById("coastlineGroupName").style.display = "none";
document.getElementById("coastlineGroupName").value = "";
document.getElementById("coastlineGroup").style.display = "inline-block";
byId("coastlineGroupsSelection").style.display = "none";
byId("coastlineGroupName").style.display = "none";
byId("coastlineGroupName").value = "";
byId("coastlineGroup").style.display = "inline-block";
}
function selectCoastlineGroup(node) {
const group = node.parentNode.id;
const select = document.getElementById("coastlineGroup");
const select = byId("coastlineGroup");
select.options.length = 0; // remove all options
coastline.selectAll("g").each(function () {
@ -116,7 +123,7 @@ function editCoastline(node = d3.event.target) {
}
function changeCoastlineGroup() {
document.getElementById(this.value).appendChild(elSelected.node());
byId(this.value).appendChild(elSelected.node());
}
function toggleNewGroupInput() {
@ -131,54 +138,44 @@ function editCoastline(node = d3.event.target) {
}
function createNewGroup() {
if (!this.value) {
tip("Please provide a valid group name");
return;
}
if (!this.value) return tip("Please provide a valid group name");
const group = this.value
.toLowerCase()
.replace(/ /g, "_")
.replace(/[^\w\s]/gi, "");
if (document.getElementById(group)) {
tip("Element with this id already exists. Please provide a unique name", false, "error");
return;
}
if (byId(group)) return tip("Element with this id already exists. Please provide a unique name", false, "error");
if (Number.isFinite(+group.charAt(0))) {
tip("Group name should start with a letter", false, "error");
return;
}
if (Number.isFinite(+group.charAt(0))) return tip("Group name should start with a letter", false, "error");
// just rename if only 1 element left
const oldGroup = elSelected.node().parentNode;
const basic = ["sea_island", "lake_island"].includes(oldGroup.id);
if (!basic && oldGroup.childElementCount === 1) {
document.getElementById("coastlineGroup").selectedOptions[0].remove();
document.getElementById("coastlineGroup").options.add(new Option(group, group, false, true));
byId("coastlineGroup").selectedOptions[0].remove();
byId("coastlineGroup").options.add(new Option(group, group, false, true));
oldGroup.id = group;
toggleNewGroupInput();
document.getElementById("coastlineGroupName").value = "";
byId("coastlineGroupName").value = "";
return;
}
// create a new group
const newGroup = elSelected.node().parentNode.cloneNode(false);
document.getElementById("coastline").appendChild(newGroup);
byId("coastline").appendChild(newGroup);
newGroup.id = group;
document.getElementById("coastlineGroup").options.add(new Option(group, group, false, true));
document.getElementById(group).appendChild(elSelected.node());
byId("coastlineGroup").options.add(new Option(group, group, false, true));
byId(group).appendChild(elSelected.node());
toggleNewGroupInput();
document.getElementById("coastlineGroupName").value = "";
byId("coastlineGroupName").value = "";
}
function removeCoastlineGroup() {
const group = elSelected.node().parentNode.id;
if (["sea_island", "lake_island"].includes(group)) {
tip("This is one of the default groups, it cannot be removed", false, "error");
return;
}
if (["sea_island", "lake_island"].includes(group))
return tip("This is one of the default groups, it cannot be removed", false, "error");
const count = elSelected.node().parentNode.childElementCount;
alertMessage.innerHTML = /* html */ `Are you sure you want to remove the group? All coastline elements of the group (${count}) will be moved under
@ -190,14 +187,14 @@ function editCoastline(node = d3.event.target) {
buttons: {
Remove: function () {
$(this).dialog("close");
const sea = document.getElementById("sea_island");
const groupEl = document.getElementById(group);
const sea = byId("sea_island");
const groupEl = byId(group);
while (groupEl.childNodes.length) {
sea.appendChild(groupEl.childNodes[0]);
}
groupEl.remove();
document.getElementById("coastlineGroup").selectedOptions[0].remove();
document.getElementById("coastlineGroup").value = "sea_island";
byId("coastlineGroup").selectedOptions[0].remove();
byId("coastlineGroup").value = "sea_island";
},
Cancel: function () {
$(this).dialog("close");

View file

@ -10,31 +10,27 @@ function restoreDefaultEvents() {
legend.call(d3.drag().on("start", dragLegendBox));
}
// on viewbox click event - run function based on target
// handle viewbox click
function clicked() {
const el = d3.event.target;
if (!el || !el.parentElement || !el.parentElement.parentElement) return;
const parent = el.parentElement;
const grand = parent.parentElement;
const great = grand.parentElement;
const p = d3.mouse(this);
const i = findCell(p[0], p[1]);
const parent = el?.parentElement;
const grand = parent?.parentElement;
const great = grand?.parentElement;
const ancestor = great?.parentElement;
if (!ancestor) return;
if (grand.id === "emblems") editEmblem();
else if (parent.id === "rivers") editRiver(el.id);
else if (grand.id === "routes") editRoute(el.id);
else if (el.tagName === "tspan" && grand.parentNode.parentNode.id === "labels") editLabel();
else if (ancestor.id === "labels" && el.tagName === "tspan") editLabel();
else if (grand.id === "burgLabels") editBurg();
else if (grand.id === "burgIcons") editBurg();
else if (parent.id === "ice") editIce();
else if (parent.id === "terrain") editReliefIcon();
else if (grand.id === "markers" || great.id === "markers") editMarker();
else if (grand.id === "coastline") editCoastline();
else if (grand.id === "lakes") editLake();
else if (great.id === "armies") editRegiment();
else if (pack.cells.t[i] === 1) {
const node = byId("island_" + pack.cells.f[i]);
editCoastline(node);
} else if (grand.id === "lakes") editLake();
}
// clear elSelected variable
@ -1252,18 +1248,18 @@ function refreshAllEditors() {
// dynamically loaded editors
async function editStates() {
if (customization) return;
const Editor = await import("../dynamic/editors/states-editor.js?v=1.99.05");
const Editor = await import("../dynamic/editors/states-editor.js?v=1.104.0");
Editor.open();
}
async function editCultures() {
if (customization) return;
const Editor = await import("../dynamic/editors/cultures-editor.js?v=1.99.05");
const Editor = await import("../dynamic/editors/cultures-editor.js?v=1.104.0");
Editor.open();
}
async function editReligions() {
if (customization) return;
const Editor = await import("../dynamic/editors/religions-editor.js?v=1.99.05");
const Editor = await import("../dynamic/editors/religions-editor.js?v=1.104.0");
Editor.open();
}

View file

@ -161,12 +161,14 @@ function showMapTooltip(point, e, i, g) {
if (group === "terrain") return tip("Click to edit the Relief Icon");
if (subgroup === "burgLabels" || subgroup === "burgIcons") {
const burg = +path[path.length - 10].dataset.id;
const b = pack.burgs[burg];
const population = si(b.population * populationRate * urbanization);
tip(`${b.name}. Population: ${population}. Click to edit`);
if (burgsOverview?.offsetParent) highlightEditorLine(burgsOverview, burg, 5000);
return;
const burgId = +path[path.length - 10].dataset.id;
if (burgId) {
const burg = pack.burgs[burgId];
const population = si(burg.population * populationRate * urbanization);
tip(`${burg.name}. Population: ${population}. Click to edit`);
if (burgsOverview?.offsetParent) highlightEditorLine(burgsOverview, burgId, 5000);
return;
}
}
if (group === "labels") return tip("Click to edit the Label");
@ -211,9 +213,9 @@ function showMapTooltip(point, e, i, g) {
if (group === "ice") return tip("Click to edit the Ice");
// covering elements
if (layerIsOn("togglePrec") && land) tip("Annual Precipitation: " + getFriendlyPrecipitation(i));
if (layerIsOn("togglePrecipitation") && land) tip("Annual Precipitation: " + getFriendlyPrecipitation(i));
else if (layerIsOn("togglePopulation")) tip(getPopulationTip(i));
else if (layerIsOn("toggleTemp")) tip("Temperature: " + convertTemperature(grid.cells.temp[g]));
else if (layerIsOn("toggleTemperature")) tip("Temperature: " + convertTemperature(grid.cells.temp[g]));
else if (layerIsOn("toggleBiomes") && pack.cells.biome[i]) {
const biome = pack.cells.biome[i];
tip("Biome: " + biomesData.name[biome]);

View file

@ -215,8 +215,7 @@ function editHeightmap(options) {
pack.religions = [];
const erosionAllowed = allowErosion.checked;
markFeatures();
markupGridOcean();
Features.markupGrid();
if (erosionAllowed) {
addLakesInDeepDepressions();
openNearSeaLakes();
@ -225,7 +224,7 @@ function editHeightmap(options) {
calculateTemperatures();
generatePrecipitation();
reGraph();
drawCoastline();
Features.markupPack();
Rivers.generate(erosionAllowed);
@ -237,8 +236,6 @@ function editHeightmap(options) {
}
}
drawRivers();
Lakes.defineGroup();
Biomes.define();
rankCells();
@ -249,15 +246,14 @@ function editHeightmap(options) {
Routes.generate();
Religions.generate();
BurgsAndStates.defineStateForms();
BurgsAndStates.generateProvinces();
Provinces.generate();
Provinces.getPoles();
BurgsAndStates.defineBurgFeatures();
drawStates();
drawBorders();
drawStateLabels();
Rivers.specify();
Lakes.generateName();
Features.specify();
Military.generate();
Markers.generate();
@ -338,14 +334,13 @@ function editHeightmap(options) {
zone.selectAll("*").remove();
});
markFeatures();
markupGridOcean();
Features.markupGrid();
if (erosionAllowed) addLakesInDeepDepressions();
OceanLayers();
calculateTemperatures();
generatePrecipitation();
reGraph();
drawCoastline();
Features.markupPack();
if (erosionAllowed) Rivers.generate(true);
@ -440,8 +435,6 @@ function editHeightmap(options) {
}
drawStateLabels();
drawStates();
drawBorders();
if (erosionAllowed) {
Rivers.specify();
@ -489,10 +482,14 @@ function editHeightmap(options) {
updateHistory();
}
function getColor(value, scheme = getColorScheme()) {
return scheme(1 - (value < 20 ? value - 5 : value) / 100);
}
// draw or update heightmap
function mockHeightmap() {
const data = renderOcean.checked ? grid.cells.i : grid.cells.i.filter(i => grid.cells.h[i] >= 20);
const scheme = getColorScheme();
viewbox
.select("#heights")
.selectAll("polygon")
@ -500,13 +497,12 @@ function editHeightmap(options) {
.join("polygon")
.attr("points", d => getGridPolygon(d))
.attr("id", d => "cell" + d)
.attr("fill", d => getColor(grid.cells.h[d], scheme));
.attr("fill", d => getColor(grid.cells.h[d]));
}
// draw or update heightmap for a selection of cells
function mockHeightmapSelection(selection) {
const ocean = renderOcean.checked;
const scheme = getColorScheme();
selection.forEach(function (i) {
let cell = viewbox.select("#heights").select("#cell" + i);
@ -518,7 +514,7 @@ function editHeightmap(options) {
.append("polygon")
.attr("points", getGridPolygon(i))
.attr("id", "cell" + i);
cell.attr("fill", getColor(grid.cells.h[i], scheme));
cell.attr("fill", getColor(grid.cells.h[i]));
});
}
@ -1349,7 +1345,7 @@ function editHeightmap(options) {
return lum | 0; // land
};
const scheme = d3.range(101).map(i => getColor(i, color()));
const scheme = d3.range(101).map(i => getColor(i));
const hues = scheme.map(rgb => d3.hsl(rgb).h | 0);
const getHeightByScheme = function (color) {
let height = scheme.indexOf(color);

View file

@ -75,13 +75,13 @@ function handleKeyup(event) {
else if (code === "KeyD") toggleBorders();
else if (code === "KeyR") toggleReligions();
else if (code === "KeyU") toggleRoutes();
else if (code === "KeyT") toggleTemp();
else if (code === "KeyT") toggleTemperature();
else if (code === "KeyN") togglePopulation();
else if (code === "KeyJ") toggleIce();
else if (code === "KeyA") togglePrec();
else if (code === "KeyA") togglePrecipitation();
else if (code === "KeyY") toggleEmblems();
else if (code === "KeyL") toggleLabels();
else if (code === "KeyI") toggleIcons();
else if (code === "KeyI") toggleBurgIcons();
else if (code === "KeyM") toggleMilitary();
else if (code === "KeyK") toggleMarkers();
else if (code === "Equal" && !customization) toggleRulers();

View file

@ -23,17 +23,15 @@ function editLake() {
modules.editLake = true;
// add listeners
document.getElementById("lakeName").addEventListener("input", changeName);
document.getElementById("lakeNameCulture").addEventListener("click", generateNameCulture);
document.getElementById("lakeNameRandom").addEventListener("click", generateNameRandom);
document.getElementById("lakeGroup").addEventListener("change", changeLakeGroup);
document.getElementById("lakeGroupAdd").addEventListener("click", toggleNewGroupInput);
document.getElementById("lakeGroupName").addEventListener("change", createNewGroup);
document.getElementById("lakeGroupRemove").addEventListener("click", removeLakeGroup);
document.getElementById("lakeEditStyle").addEventListener("click", editGroupStyle);
document.getElementById("lakeLegend").addEventListener("click", editLakeLegend);
byId("lakeName").on("input", changeName);
byId("lakeNameCulture").on("click", generateNameCulture);
byId("lakeNameRandom").on("click", generateNameRandom);
byId("lakeGroup").on("change", changeLakeGroup);
byId("lakeGroupAdd").on("click", toggleNewGroupInput);
byId("lakeGroupName").on("change", createNewGroup);
byId("lakeGroupRemove").on("click", removeLakeGroup);
byId("lakeEditStyle").on("click", editGroupStyle);
byId("lakeLegend").on("click", editLakeLegend);
function getLake() {
const lakeId = +elSelected.attr("data-f");
@ -41,85 +39,91 @@ function editLake() {
}
function updateLakeValues() {
const cells = pack.cells;
const {cells, vertices, rivers} = pack;
const l = getLake();
document.getElementById("lakeName").value = l.name;
document.getElementById("lakeArea").value = si(getArea(l.area)) + " " + getAreaUnit();
byId("lakeName").value = l.name;
byId("lakeArea").value = si(getArea(l.area)) + " " + getAreaUnit();
const length = d3.polygonLength(l.vertices.map(v => pack.vertices.p[v]));
document.getElementById("lakeShoreLength").value = si(length * distanceScale) + " " + distanceUnitInput.value;
const length = d3.polygonLength(l.vertices.map(v => vertices.p[v]));
byId("lakeShoreLength").value = si(length * distanceScale) + " " + distanceUnitInput.value;
const lakeCells = Array.from(cells.i.filter(i => cells.f[i] === l.i));
const heights = lakeCells.map(i => cells.h[i]);
document.getElementById("lakeElevation").value = getHeight(l.height);
document.getElementById("lakeAverageDepth").value = getHeight(d3.mean(heights), "abs");
document.getElementById("lakeMaxDepth").value = getHeight(d3.min(heights), "abs");
byId("lakeElevation").value = getHeight(l.height);
byId("lakeAverageDepth").value = getHeight(d3.mean(heights), "abs");
byId("lakeMaxDepth").value = getHeight(d3.min(heights), "abs");
document.getElementById("lakeFlux").value = l.flux;
document.getElementById("lakeEvaporation").value = l.evaporation;
byId("lakeFlux").value = l.flux;
byId("lakeEvaporation").value = l.evaporation;
const inlets = l.inlets && l.inlets.map(inlet => pack.rivers.find(river => river.i === inlet)?.name);
const outlet = l.outlet ? pack.rivers.find(river => river.i === l.outlet)?.name : "no";
document.getElementById("lakeInlets").value = inlets ? inlets.length : "no";
document.getElementById("lakeInlets").title = inlets ? inlets.join(", ") : "";
document.getElementById("lakeOutlet").value = outlet;
const inlets = l.inlets && l.inlets.map(inlet => rivers.find(river => river.i === inlet)?.name);
const outlet = l.outlet ? rivers.find(river => river.i === l.outlet)?.name : "no";
byId("lakeInlets").value = inlets ? inlets.length : "no";
byId("lakeInlets").title = inlets ? inlets.join(", ") : "";
byId("lakeOutlet").value = outlet;
}
function drawLakeVertices() {
const v = getLake().vertices; // lake outer vertices
const vertices = getLake().vertices;
const c = [...new Set(v.map(v => pack.vertices.c[v]).flat())];
const neibCells = unique(vertices.map(v => pack.vertices.c[v]).flat());
debug
.select("#vertices")
.selectAll("polygon")
.data(c)
.data(neibCells)
.enter()
.append("polygon")
.attr("points", d => getPackPolygon(d))
.attr("points", getPackPolygon)
.attr("data-c", d => d);
debug
.select("#vertices")
.selectAll("circle")
.data(v)
.data(vertices)
.enter()
.append("circle")
.attr("cx", d => pack.vertices.p[d][0])
.attr("cy", d => pack.vertices.p[d][1])
.attr("r", 0.4)
.attr("data-v", d => d)
.call(d3.drag().on("drag", dragVertex))
.call(d3.drag().on("drag", handleVertexDrag).on("end", handleVertexDragEnd))
.on("mousemove", () =>
tip("Drag to move the vertex, please use for fine-tuning only. Edit heightmap to change actual cell heights")
tip("Drag to move the vertex. Please use for fine-tuning only! Edit heightmap to change actual cell heights")
);
}
function dragVertex() {
const x = rn(d3.event.x, 2),
y = rn(d3.event.y, 2);
function handleVertexDrag() {
const x = rn(d3.event.x, 2);
const y = rn(d3.event.y, 2);
this.setAttribute("cx", x);
this.setAttribute("cy", y);
const v = +this.dataset.v;
pack.vertices.p[v] = [x, y];
debug
.select("#vertices")
.selectAll("polygon")
.attr("points", d => getPackPolygon(d));
redrawLake();
const vertexId = d3.select(this).datum();
pack.vertices.p[vertexId] = [x, y];
const feature = getLake();
// update lake path
defs.select("#featurePaths > path#feature_" + feature.i).attr("d", getFeaturePath(feature));
// update area
const points = feature.vertices.map(vertex => pack.vertices.p[vertex]);
feature.area = Math.abs(d3.polygonArea(points));
byId("lakeArea").value = si(getArea(feature.area)) + " " + getAreaUnit();
// update cell
debug.select("#vertices").selectAll("polygon").attr("points", getPackPolygon);
}
function redrawLake() {
lineGen.curve(d3.curveBasisClosed);
const feature = getLake();
const points = feature.vertices.map(v => pack.vertices.p[v]);
const d = round(lineGen(points));
elSelected.attr("d", d);
defs.select("mask#land > path#land_" + feature.i).attr("d", d); // update land mask
feature.area = Math.abs(d3.polygonArea(points));
document.getElementById("lakeArea").value = si(getArea(feature.area)) + " " + getAreaUnit();
function handleVertexDragEnd() {
if (layerIsOn("toggleStates")) drawStates();
if (layerIsOn("toggleProvinces")) drawProvinces();
if (layerIsOn("toggleBorders")) drawBorders();
if (layerIsOn("toggleBiomes")) drawBiomes();
if (layerIsOn("toggleReligions")) drawReligions();
if (layerIsOn("toggleCultures")) drawCultures();
}
function changeName() {
@ -138,7 +142,7 @@ function editLake() {
function selectLakeGroup(node) {
const group = node.parentNode.id;
const select = document.getElementById("lakeGroup");
const select = byId("lakeGroup");
select.options.length = 0; // remove all options
lakes.selectAll("g").each(function () {
@ -147,7 +151,7 @@ function editLake() {
}
function changeLakeGroup() {
document.getElementById(this.value).appendChild(elSelected.node());
byId(this.value).appendChild(elSelected.node());
getLake().group = this.value;
}
@ -172,7 +176,7 @@ function editLake() {
.replace(/ /g, "_")
.replace(/[^\w\s]/gi, "");
if (document.getElementById(group)) {
if (byId(group)) {
tip("Element with this id already exists. Please provide a unique name", false, "error");
return;
}
@ -186,23 +190,23 @@ function editLake() {
const oldGroup = elSelected.node().parentNode;
const basic = ["freshwater", "salt", "sinkhole", "frozen", "lava", "dry"].includes(oldGroup.id);
if (!basic && oldGroup.childElementCount === 1) {
document.getElementById("lakeGroup").selectedOptions[0].remove();
document.getElementById("lakeGroup").options.add(new Option(group, group, false, true));
byId("lakeGroup").selectedOptions[0].remove();
byId("lakeGroup").options.add(new Option(group, group, false, true));
oldGroup.id = group;
toggleNewGroupInput();
document.getElementById("lakeGroupName").value = "";
byId("lakeGroupName").value = "";
return;
}
// create a new group
const newGroup = elSelected.node().parentNode.cloneNode(false);
document.getElementById("lakes").appendChild(newGroup);
byId("lakes").appendChild(newGroup);
newGroup.id = group;
document.getElementById("lakeGroup").options.add(new Option(group, group, false, true));
document.getElementById(group).appendChild(elSelected.node());
byId("lakeGroup").options.add(new Option(group, group, false, true));
byId(group).appendChild(elSelected.node());
toggleNewGroupInput();
document.getElementById("lakeGroupName").value = "";
byId("lakeGroupName").value = "";
}
function removeLakeGroup() {
@ -221,14 +225,14 @@ function editLake() {
buttons: {
Remove: function () {
$(this).dialog("close");
const freshwater = document.getElementById("freshwater");
const groupEl = document.getElementById(group);
const freshwater = byId("freshwater");
const groupEl = byId(group);
while (groupEl.childNodes.length) {
freshwater.appendChild(groupEl.childNodes[0]);
}
groupEl.remove();
document.getElementById("lakeGroup").selectedOptions[0].remove();
document.getElementById("lakeGroup").value = "freshwater";
byId("lakeGroup").selectedOptions[0].remove();
byId("lakeGroup").value = "freshwater";
},
Cancel: function () {
$(this).dialog("close");

File diff suppressed because it is too large Load diff

View file

@ -530,3 +530,32 @@ class Planimeter extends Measurer {
this.el.select("text").attr("x", c[0]).attr("y", c[1]).text(area);
}
}
function createDefaultRuler() {
TIME && console.time("createDefaultRuler");
const {features, vertices} = pack;
const areas = features.map(f => (f.land ? f.area || 0 : -Infinity));
const largestLand = areas.indexOf(Math.max(...areas));
const featureVertices = features[largestLand].vertices;
const MIN_X = 100;
const MAX_X = graphWidth - 100;
const MIN_Y = 100;
const MAX_Y = graphHeight - 100;
let leftmostVertex = [graphWidth - MIN_X, graphHeight / 2];
let rightmostVertex = [MIN_X, graphHeight / 2];
for (const vertex of featureVertices) {
const [x, y] = vertices.p[vertex];
if (y < MIN_Y || y > MAX_Y) continue;
if (x < leftmostVertex[0] && x >= MIN_X) leftmostVertex = [x, y];
if (x > rightmostVertex[0] && x <= MAX_X) rightmostVertex = [x, y];
}
rulers = new Rulers();
rulers.create(Ruler, [leftmostVertex, rightmostVertex]);
TIME && console.timeEnd("createDefaultRuler");
}

View file

@ -8,7 +8,7 @@ function editProvinces() {
if (layerIsOn("toggleCultures")) toggleCultures();
provs.selectAll("text").call(d3.drag().on("drag", dragLabel)).classed("draggable", true);
const body = document.getElementById("provincesBodySection");
const body = byId("provincesBodySection");
refreshProvincesEditor();
if (modules.editProvinces) return;
@ -23,22 +23,22 @@ function editProvinces() {
});
// add listeners
document.getElementById("provincesEditorRefresh").addEventListener("click", refreshProvincesEditor);
document.getElementById("provincesEditStyle").addEventListener("click", () => editStyle("provs"));
document.getElementById("provincesFilterState").addEventListener("change", provincesEditorAddLines);
document.getElementById("provincesPercentage").addEventListener("click", togglePercentageMode);
document.getElementById("provincesChart").addEventListener("click", showChart);
document.getElementById("provincesToggleLabels").addEventListener("click", toggleLabels);
document.getElementById("provincesExport").addEventListener("click", downloadProvincesData);
document.getElementById("provincesRemoveAll").addEventListener("click", removeAllProvinces);
document.getElementById("provincesManually").addEventListener("click", enterProvincesManualAssignent);
document.getElementById("provincesManuallyApply").addEventListener("click", applyProvincesManualAssignent);
document.getElementById("provincesManuallyCancel").addEventListener("click", () => exitProvincesManualAssignment());
document.getElementById("provincesRelease").addEventListener("click", triggerProvincesRelease);
document.getElementById("provincesAdd").addEventListener("click", enterAddProvinceMode);
document.getElementById("provincesRecolor").addEventListener("click", recolorProvinces);
byId("provincesEditorRefresh").on("click", refreshProvincesEditor);
byId("provincesEditStyle").on("click", () => editStyle("provs"));
byId("provincesFilterState").on("change", provincesEditorAddLines);
byId("provincesPercentage").on("click", togglePercentageMode);
byId("provincesChart").on("click", showChart);
byId("provincesToggleLabels").on("click", toggleLabels);
byId("provincesExport").on("click", downloadProvincesData);
byId("provincesRemoveAll").on("click", removeAllProvinces);
byId("provincesManually").on("click", enterProvincesManualAssignent);
byId("provincesManuallyApply").on("click", applyProvincesManualAssignent);
byId("provincesManuallyCancel").on("click", () => exitProvincesManualAssignment());
byId("provincesRelease").on("click", triggerProvincesRelease);
byId("provincesAdd").on("click", enterAddProvinceMode);
byId("provincesRecolor").on("click", recolorProvinces);
body.addEventListener("click", function (ev) {
body.on("click", function (ev) {
if (customization) return;
const el = ev.target,
cl = el.classList,
@ -58,7 +58,7 @@ function editProvinces() {
else if (cl.contains("icon-lock") || cl.contains("icon-lock-open")) updateLockStatus(p, cl);
});
body.addEventListener("change", function (ev) {
body.on("change", function (ev) {
const el = ev.target,
cl = el.classList,
line = el.parentNode,
@ -100,7 +100,7 @@ function editProvinces() {
}
function updateFilter() {
const stateFilter = document.getElementById("provincesFilterState");
const stateFilter = byId("provincesFilterState");
const selectedState = stateFilter.value || 1;
stateFilter.options.length = 0; // remove all options
stateFilter.options.add(new Option(`all`, -1, false, selectedState == -1));
@ -111,7 +111,7 @@ function editProvinces() {
// add line for each province
function provincesEditorAddLines() {
const unit = " " + getAreaUnit();
const selectedState = +document.getElementById("provincesFilterState").value;
const selectedState = +byId("provincesFilterState").value;
let filtered = pack.provinces.filter(p => p.i && !p.removed); // all valid burgs
if (selectedState != -1) filtered = filtered.filter(p => p.state === selectedState); // filtered by state
body.innerHTML = "";
@ -194,9 +194,9 @@ function editProvinces() {
byId("provincesFooterPopulation").dataset.population = totalPopulation;
body.querySelectorAll("div.states").forEach(el => {
el.addEventListener("click", selectProvinceOnLineClick);
el.addEventListener("mouseenter", ev => provinceHighlightOn(ev));
el.addEventListener("mouseleave", ev => provinceHighlightOff(ev));
el.on("click", selectProvinceOnLineClick);
el.on("mouseenter", ev => provinceHighlightOn(ev));
el.on("mouseleave", ev => provinceHighlightOff(ev));
});
if (body.dataset.type === "percentage") {
@ -306,7 +306,7 @@ function editProvinces() {
const {cell: center, culture} = burgs[burgId];
const color = getRandomColor();
const coa = province.coa;
const coaEl = document.getElementById("provinceCOA" + provinceId);
const coaEl = byId("provinceCOA" + provinceId);
if (coaEl) coaEl.id = "stateCOA" + newStateId;
emblems.select(`#provinceEmblems > use[data-i='${provinceId}']`).remove();
@ -454,6 +454,7 @@ function editProvinces() {
p.burgs.forEach(b => (pack.burgs[b].population = population));
}
if (layerIsOn("togglePopulation")) drawPopulation();
refreshProvincesEditor();
}
}
@ -482,7 +483,7 @@ function editProvinces() {
unfog("focusProvince" + p);
const coaId = "provinceCOA" + p;
if (document.getElementById(coaId)) document.getElementById(coaId).remove();
if (byId(coaId)) byId(coaId).remove();
emblems.select(`#provinceEmblems > use[data-i='${p}']`).remove();
pack.provinces[p] = {i: p, removed: true};
@ -490,8 +491,7 @@ function editProvinces() {
const g = provs.select("#provincesBody");
g.select("#province" + p).remove();
g.select("#province-gap" + p).remove();
if (!layerIsOn("toggleBorders")) toggleBorders();
else drawBorders();
if (layerIsOn("toggleBorders")) drawBorders();
refreshProvincesEditor();
$(this).dialog("close");
},
@ -504,13 +504,13 @@ function editProvinces() {
function editProvinceName(province) {
const p = pack.provinces[province];
document.getElementById("provinceNameEditor").dataset.province = province;
document.getElementById("provinceNameEditorShort").value = p.name;
byId("provinceNameEditor").dataset.province = province;
byId("provinceNameEditorShort").value = p.name;
applyOption(provinceNameEditorSelectForm, p.formName);
document.getElementById("provinceNameEditorFull").value = p.fullName;
byId("provinceNameEditorFull").value = p.fullName;
const cultureId = pack.cells.culture[p.center];
document.getElementById("provinceCultureDisplay").innerText = pack.cultures[cultureId].name;
byId("provinceCultureDisplay").innerText = pack.cultures[cultureId].name;
$("#provinceNameEditor").dialog({
resizable: false,
@ -531,22 +531,22 @@ function editProvinces() {
modules.editProvinceName = true;
// add listeners
document.getElementById("provinceNameEditorShortCulture").addEventListener("click", regenerateShortNameCulture);
document.getElementById("provinceNameEditorShortRandom").addEventListener("click", regenerateShortNameRandom);
document.getElementById("provinceNameEditorAddForm").addEventListener("click", addCustomForm);
document.getElementById("provinceNameEditorFullRegenerate").addEventListener("click", regenerateFullName);
byId("provinceNameEditorShortCulture").on("click", regenerateShortNameCulture);
byId("provinceNameEditorShortRandom").on("click", regenerateShortNameRandom);
byId("provinceNameEditorAddForm").on("click", addCustomForm);
byId("provinceNameEditorFullRegenerate").on("click", regenerateFullName);
function regenerateShortNameCulture() {
const province = +provinceNameEditor.dataset.province;
const culture = pack.cells.culture[pack.provinces[province].center];
const name = Names.getState(Names.getCultureShort(culture), culture);
document.getElementById("provinceNameEditorShort").value = name;
byId("provinceNameEditorShort").value = name;
}
function regenerateShortNameRandom() {
const base = rand(nameBases.length - 1);
const name = Names.getState(Names.getBase(base), undefined, base);
document.getElementById("provinceNameEditorShort").value = name;
byId("provinceNameEditorShort").value = name;
}
function addCustomForm() {
@ -558,9 +558,9 @@ function editProvinces() {
}
function regenerateFullName() {
const short = document.getElementById("provinceNameEditorShort").value;
const form = document.getElementById("provinceNameEditorSelectForm").value;
document.getElementById("provinceNameEditorFull").value = getFullName();
const short = byId("provinceNameEditorShort").value;
const form = byId("provinceNameEditorSelectForm").value;
byId("provinceNameEditorFull").value = getFullName();
function getFullName() {
if (!form) return short;
@ -570,9 +570,9 @@ function editProvinces() {
}
function applyNameChange(p) {
p.name = document.getElementById("provinceNameEditorShort").value;
p.formName = document.getElementById("provinceNameEditorSelectForm").value;
p.fullName = document.getElementById("provinceNameEditorFull").value;
p.name = byId("provinceNameEditorShort").value;
p.formName = byId("provinceNameEditorSelectForm").value;
p.fullName = byId("provinceNameEditorFull").value;
provs.select("#provinceLabel" + p.i).text(p.name);
refreshProvincesEditor();
}
@ -651,7 +651,7 @@ function editProvinces() {
.attr("height", height)
.attr("font-size", "10px");
const graph = svg.append("g").attr("transform", `translate(10, 0)`);
document.getElementById("provincesTreeType").addEventListener("change", updateChart);
byId("provincesTreeType").on("change", updateChart);
treeLayout(root);
@ -688,7 +688,7 @@ function editProvinces() {
function hideInfo(ev) {
provinceHighlightOff(ev);
if (!document.getElementById("provinceInfo")) return;
if (!byId("provinceInfo")) return;
provinceInfo.innerHTML = "&#8205;";
d3.select(ev.target).select("rect").classed("selected", 0);
}
@ -816,7 +816,7 @@ function editProvinces() {
stateBorders.select("path").attr("stroke", "#000").attr("stroke-width", 1.2);
customization = 11;
provs.select("g#provincesBody").append("g").attr("id", "temp");
provs.select("g#provincesBody").append("g").attr("id", "temp").attr("stroke-width", 0.3);
provs
.select("g#provincesBody")
.append("g")
@ -826,7 +826,7 @@ function editProvinces() {
.attr("stroke-width", 1);
document.querySelectorAll("#provincesBottom > *").forEach(el => (el.style.display = "none"));
document.getElementById("provincesManuallyButtons").style.display = "inline-block";
byId("provincesManuallyButtons").style.display = "inline-block";
provincesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
provincesHeader.querySelector("div[data-sortby='state']").style.left = "7.7em";
@ -950,10 +950,10 @@ function editProvinces() {
pack.cells.province[i] = +this.dataset.province;
});
if (!layerIsOn("toggleBorders")) toggleBorders();
else drawBorders();
if (!layerIsOn("toggleProvinces")) toggleProvinces();
else drawProvinces();
Provinces.getPoles();
if (layerIsOn("toggleBorders")) drawBorders();
if (layerIsOn("toggleProvinces")) drawProvinces();
exitProvincesManualAssignment();
refreshProvincesEditor();
}
@ -970,7 +970,7 @@ function editProvinces() {
debug.selectAll("path.selected").remove();
document.querySelectorAll("#provincesBottom > *").forEach(el => (el.style.display = "inline-block"));
document.getElementById("provincesManuallyButtons").style.display = "none";
byId("provincesManuallyButtons").style.display = "none";
provincesEditor.querySelectorAll(".hide:not(.show)").forEach(el => el.classList.remove("hidden"));
provincesHeader.querySelector("div[data-sortby='state']").style.left = "22em";
@ -1044,12 +1044,11 @@ function editProvinces() {
cells.province[c] = province;
});
if (!layerIsOn("toggleBorders")) toggleBorders();
else drawBorders();
if (!layerIsOn("toggleProvinces")) toggleProvinces();
else drawProvinces();
if (layerIsOn("toggleBorders")) drawBorders();
if (layerIsOn("toggleProvinces")) drawProvinces();
collectStatistics();
document.getElementById("provincesFilterState").value = state;
byId("provincesFilterState").value = state;
provincesEditorAddLines();
}
@ -1062,7 +1061,7 @@ function editProvinces() {
}
function recolorProvinces() {
const state = +document.getElementById("provincesFilterState").value;
const state = +byId("provincesFilterState").value;
pack.provinces.forEach(p => {
if (!p || p.removed) return;
@ -1120,8 +1119,7 @@ function editProvinces() {
pack.states.forEach(s => (s.provinces = []));
unfog();
if (!layerIsOn("toggleBorders")) toggleBorders();
else drawBorders();
if (layerIsOn("toggleBorders")) drawBorders();
provs.select("#provincesBody").remove();
turnButtonOff("toggleProvinces");

View file

@ -246,7 +246,7 @@ function editRegiment(selector) {
reg.name = Military.getName(reg, military);
military.push(reg);
Military.generateNote(reg, pack.states[state]); // add legend
Military.drawRegiment(reg, state);
drawRegiment(reg, state);
if (regimentsOverviewRefresh.offsetParent) regimentsOverviewRefresh.click();
toggleAdd();
}
@ -296,7 +296,7 @@ function editRegiment(selector) {
(defender.px = defender.x), (defender.py = defender.y);
// move attacker to defender
Military.moveRegiment(attacker, defender.x, defender.y - 8);
moveRegiment(attacker, defender.x, defender.y - 8);
// draw battle icon
const attack = d3

View file

@ -179,7 +179,7 @@ function overviewRegiments(state) {
reg.name = Military.getName(reg, military);
military.push(reg);
Military.generateNote(reg, pack.states[state]); // add legend
Military.drawRegiment(reg, state);
drawRegiment(reg, state);
toggleAdd();
}

View file

@ -70,6 +70,10 @@ function getColorScheme(scheme = "bright") {
return heightmapColorSchemes[scheme];
}
function getColor(value, scheme = getColorScheme("bright")) {
return scheme(1 - (value < 20 ? value - 5 : value) / 100);
}
// Toggle style sections on element select
styleElementSelect.on("change", selectStyleElement);
@ -114,6 +118,7 @@ function selectStyleElement() {
"armies",
"routes",
"lakes",
"biomes",
"borders",
"cults",
"relig",
@ -952,7 +957,7 @@ styleArmiesSize.on("input", e => {
armies.selectAll("g").remove(); // clear armies layer
pack.states.forEach(s => {
if (!s.i || s.removed || !s.military.length) return;
Military.drawRegiments(s.military, s.i);
drawRegiments(s.military, s.i);
});
});

View file

@ -142,6 +142,7 @@ window.UISubmap = (function () {
fullMap: true,
noLabels: true,
noScaleBar: true,
noVignette: true,
noIce: true
});
@ -282,7 +283,7 @@ window.UISubmap = (function () {
oldstate = null; // destroy old state to free memory
restoreLayers();
drawLayers();
if (ThreeD.options.isOn) ThreeD.redraw();
if ($("#worldConfigurator").is(":visible")) editWorld();
}

View file

@ -75,8 +75,10 @@ toolsContent.addEventListener("click", function (event) {
});
function processFeatureRegeneration(event, button) {
if (button === "regenerateStateLabels") drawStateLabels();
else if (button === "regenerateReliefIcons") {
if (button === "regenerateStateLabels") {
$("#labels").fadeIn();
drawStateLabels();
} else if (button === "regenerateReliefIcons") {
ReliefIcons.draw();
if (!layerIsOn("toggleRelief")) toggleRelief();
} else if (button === "regenerateRoutes") {
@ -126,14 +128,14 @@ function regenerateRoutes() {
function regenerateRivers() {
Rivers.generate();
Lakes.defineGroup();
Rivers.specify();
if (!layerIsOn("toggleRivers")) toggleRivers();
else drawRivers();
Features.specify();
if (layerIsOn("toggleRivers")) drawRivers();
}
function recalculatePopulation() {
rankCells();
pack.burgs.forEach(b => {
if (!b.i || b.removed || b.lock) return;
const i = b.cell;
@ -143,6 +145,8 @@ function recalculatePopulation() {
if (b.port) b.population = b.population * 1.3; // increase port population
b.population = rn(b.population * gauss(2, 3, 0.6, 20, 3), 3);
});
layerIsOn("togglePopulation") ? drawPopulation() : togglePopulation();
}
function regenerateStates() {
@ -152,12 +156,14 @@ function regenerateStates() {
pack.states = newStates;
BurgsAndStates.expandStates();
BurgsAndStates.normalizeStates();
BurgsAndStates.getPoles();
BurgsAndStates.collectStatistics();
BurgsAndStates.assignColors();
BurgsAndStates.generateCampaigns();
BurgsAndStates.generateDiplomacy();
BurgsAndStates.defineStateForms();
BurgsAndStates.generateProvinces(true);
Provinces.generate(true);
Provinces.getPoles();
layerIsOn("toggleStates") ? drawStates() : toggleStates();
layerIsOn("toggleBorders") ? drawBorders() : toggleBorders();
@ -332,9 +338,11 @@ function recreateStates() {
function regenerateProvinces() {
unfog();
BurgsAndStates.generateProvinces(true, true);
drawBorders();
if (layerIsOn("toggleProvinces")) drawProvinces();
Provinces.generate(true, true);
Provinces.getPoles();
if (layerIsOn("toggleBorders")) drawBorders();
layerIsOn("toggleProvinces") ? drawProvinces() : toggleProvinces();
// remove emblems
document.querySelectorAll("[id^=provinceCOA]").forEach(el => el.remove());
@ -437,9 +445,11 @@ function regenerateBurgs() {
BurgsAndStates.specifyBurgs();
BurgsAndStates.defineBurgFeatures();
BurgsAndStates.drawBurgs();
regenerateRoutes();
drawBurgIcons();
drawBurgLabels();
// remove emblems
document.querySelectorAll("[id^=burgCOA]").forEach(el => el.remove());
emblems.selectAll("use").remove();
@ -498,13 +508,13 @@ function regenerateEmblems() {
province.coa.shield = COA.getShield(culture, province.state);
});
if (layerIsOn("toggleEmblems")) drawEmblems(); // redrawEmblems
layerIsOn("toggleEmblems") ? drawEmblems() : toggleEmblems();
}
function regenerateReligions() {
Religions.generate();
if (!layerIsOn("toggleReligions")) toggleReligions();
else drawReligions();
if (layerIsOn("toggleReligions")) drawReligions();
else toggleReligions();
refreshAllEditors();
}
@ -520,7 +530,9 @@ function regenerateCultures() {
function regenerateMilitary() {
Military.generate();
if (!layerIsOn("toggleMilitary")) toggleMilitary();
if (layerIsOn("toggleMilitary")) drawMilitary();
else toggleMilitary();
if (byId("militaryOverviewRefresh").offsetParent) militaryOverviewRefresh.click();
}

View file

@ -66,11 +66,11 @@ function editUnits() {
function changeHeightExponent() {
calculateTemperatures();
if (layerIsOn("toggleTemp")) drawTemp();
if (layerIsOn("toggleTemperature")) drawTemperature();
}
function changeTemperatureScale() {
if (layerIsOn("toggleTemp")) drawTemp();
if (layerIsOn("toggleTemperature")) drawTemperature();
}
function changePopulationRate() {

View file

@ -86,13 +86,13 @@ function editWorld() {
generatePrecipitation();
const heights = new Uint8Array(pack.cells.h);
Rivers.generate();
Lakes.defineGroup();
Rivers.specify();
pack.cells.h = new Float32Array(heights);
Biomes.define();
Features.specify();
if (layerIsOn("toggleTemp")) drawTemp();
if (layerIsOn("togglePrec")) drawPrec();
if (layerIsOn("toggleTemperature")) drawTemperature();
if (layerIsOn("togglePrecipitation")) drawPrecipitation();
if (layerIsOn("toggleBiomes")) drawBiomes();
if (layerIsOn("toggleCoordinates")) drawCoordinates();
if (layerIsOn("toggleRivers")) drawRivers();

View file

@ -472,6 +472,7 @@ function editZones() {
burgs.forEach(b => (b.population = population));
}
if (layerIsOn("togglePopulation")) drawPopulation();
zonesEditorAddLines();
}
}

View file

@ -3,6 +3,7 @@
// clip polygon by graph bbox
function clipPoly(points, secure = 0) {
if (points.length < 2) return points;
return polygonclip(points, [0, 0, graphWidth, graphHeight], secure);
}

View file

@ -1,16 +1,16 @@
"use strict";
// get continuous paths for all cells at once based on getType(cellId) comparison
function getVertexPaths({getType, options}) {
const {cells, vertices} = pack;
const paths = {};
// get continuous paths (isolines) for all cells at once based on getType(cellId) comparison
function getIsolines(graph, getType, options = {polygons: false, fill: false, halo: false, waterGap: false}) {
const {cells, vertices} = graph;
const isolines = {};
const checkedCells = new Uint8Array(cells.c.length);
const checkedCells = new Uint8Array(cells.i.length);
const addToChecked = cellId => (checkedCells[cellId] = 1);
const isChecked = cellId => checkedCells[cellId] === 1;
for (let cellId = 0; cellId < cells.c.length; cellId++) {
if (isChecked(cellId) || getType(cellId) === 0) continue;
for (const cellId of cells.i) {
if (isChecked(cellId) || !getType(cellId)) continue;
addToChecked(cellId);
const type = getType(cellId);
@ -20,70 +20,73 @@ function getVertexPaths({getType, options}) {
const onborderCell = cells.c[cellId].find(ofDifferentType);
if (onborderCell === undefined) continue;
const feature = pack.features[cells.f[onborderCell]];
if (feature.type === "lake") {
if (!feature.shoreline) Lakes.getShoreline(feature);
if (feature.shoreline.every(ofSameType)) continue; // inner lake
}
// check if inner lake. Note there is no shoreline for grid features
const feature = graph.features[cells.f[onborderCell]];
if (feature.type === "lake" && feature.shoreline?.every(ofSameType)) continue;
const startingVertex = cells.v[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({startingVertex, ofSameType, addToChecked, closeRing: true});
const vertexChain = connectVertices({vertices, startingVertex, ofSameType, addToChecked, closeRing: true});
if (vertexChain.length < 3) continue;
addPath(type, vertexChain);
addIsoline(type, vertices, vertexChain);
}
return Object.entries(paths);
return isolines;
function getBorderPath(vertexChain, discontinue) {
let discontinued = true;
let lastOperation = "";
const path = vertexChain.map(vertex => {
if (discontinue(vertex)) {
discontinued = true;
return "";
}
function addIsoline(type, vertices, vertexChain) {
if (!isolines[type]) isolines[type] = {};
const operation = discontinued ? "M" : "L";
const command = operation === lastOperation ? "" : operation;
if (options.polygons) {
if (!isolines[type].polygons) isolines[type].polygons = [];
isolines[type].polygons.push(vertexChain.map(vertexId => vertices.p[vertexId]));
}
discontinued = false;
lastOperation = operation;
if (options.fill) {
if (!isolines[type].fill) isolines[type].fill = "";
isolines[type].fill += getFillPath(vertices, vertexChain);
}
return ` ${command}${getVertexPoint(vertex)}`;
});
if (options.waterGap) {
if (!isolines[type].waterGap) isolines[type].waterGap = "";
const isLandVertex = vertexId => vertices.c[vertexId].every(i => cells.h[i] >= 20);
isolines[type].waterGap += getBorderPath(vertices, vertexChain, isLandVertex);
}
return path.join("").trim();
}
function isBorderVertex(vertex) {
const adjacentCells = vertices.c[vertex];
return adjacentCells.some(i => cells.b[i]);
}
function isLandVertex(vertex) {
const adjacentCells = vertices.c[vertex];
return adjacentCells.every(i => cells.h[i] >= MIN_LAND_HEIGHT);
}
function addPath(index, vertexChain) {
if (!paths[index]) paths[index] = {fill: "", waterGap: "", halo: ""};
if (options.fill) paths[index].fill += getFillPath(vertexChain);
if (options.halo) paths[index].halo += getBorderPath(vertexChain, isBorderVertex);
if (options.waterGap) paths[index].waterGap += getBorderPath(vertexChain, isLandVertex);
if (options.halo) {
if (!isolines[type].halo) isolines[type].halo = "";
const isBorderVertex = vertexId => vertices.c[vertexId].some(i => cells.b[i]);
isolines[type].halo += getBorderPath(vertices, vertexChain, isBorderVertex);
}
}
}
function getVertexPoint(vertexId) {
return pack.vertices.p[vertexId];
}
function getFillPath(vertexChain) {
const points = vertexChain.map(getVertexPoint);
function getFillPath(vertices, vertexChain) {
const points = vertexChain.map(vertexId => vertices.p[vertexId]);
const firstPoint = points.shift();
return `M${firstPoint} L${points.join(" ")}`;
return `M${firstPoint} L${points.join(" ")} Z`;
}
function getBorderPath(vertices, vertexChain, discontinue) {
let discontinued = true;
let lastOperation = "";
const path = vertexChain.map(vertexId => {
if (discontinue(vertexId)) {
discontinued = true;
return "";
}
const operation = discontinued ? "M" : "L";
const command = operation === lastOperation ? "" : operation;
discontinued = false;
lastOperation = operation;
return ` ${command}${vertices.p[vertexId]}`;
});
return path.join("").trim();
}
// get single path for an non-continuous array of cells
@ -107,26 +110,36 @@ function getVertexPath(cellsArray) {
if (onborderCell === undefined) continue;
const feature = pack.features[cells.f[onborderCell]];
if (feature.type === "lake") {
if (!feature.shoreline) Lakes.getShoreline(feature);
if (feature.type === "lake" && feature.shoreline) {
if (feature.shoreline.every(ofSameType)) continue; // inner lake
}
const startingVertex = cells.v[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({startingVertex, ofSameType, addToChecked, closeRing: true});
const vertexChain = connectVertices({vertices, startingVertex, ofSameType, addToChecked, closeRing: true});
if (vertexChain.length < 3) continue;
path += getFillPath(vertexChain);
path += getFillPath(vertices, vertexChain);
}
return path;
}
function connectVertices({startingVertex, ofSameType, addToChecked, closeRing}) {
const vertices = pack.vertices;
const MAX_ITERATIONS = pack.cells.i.length;
function getPolesOfInaccessibility(graph, getType) {
const isolines = getIsolines(graph, getType, {polygons: true});
const poles = Object.entries(isolines).map(([id, isoline]) => {
const multiPolygon = isoline.polygons.sort((a, b) => b.length - a.length);
const [x, y] = polylabel(multiPolygon, 20);
return [id, [rn(x), rn(y)]];
});
return Object.fromEntries(poles);
}
function connectVertices({vertices, startingVertex, ofSameType, addToChecked, closeRing}) {
const MAX_ITERATIONS = vertices.c.length;
const chain = []; // vertices chain to form a path
let next = startingVertex;
@ -145,6 +158,11 @@ function connectVertices({startingVertex, ofSameType, addToChecked, closeRing})
else if (v2 !== previous && c2 !== c3) next = v2;
else if (v3 !== previous && c1 !== c3) next = v3;
if (!vertices.c[next]) {
ERROR && console.error("ConnectVertices: next vertex is out of bounds");
break;
}
if (next === current) {
ERROR && console.error("ConnectVertices: next vertex is not found");
break;

View file

@ -12,7 +12,7 @@
*
* Example: 1.102.2 -> Major version 1, Minor version 102, Patch version 2
*/
const VERSION = "1.103.8";
const VERSION = "1.104.0";
if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format or parsing function");
{