Merge branch 'master' of https://github.com/Azgaar/Fantasy-Map-Generator into ai-assistant

This commit is contained in:
Azgaar 2024-09-22 13:24:42 +02:00
commit dadb0cad8c
61 changed files with 2831 additions and 2928 deletions

View file

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

View file

@ -138,7 +138,11 @@
} }
</style> </style>
<<<<<<< HEAD
<link rel="preload" href="index.css?v=1.102.00" as="style" onload="this.onload=null; this.rel='stylesheet'" /> <link rel="preload" href="index.css?v=1.102.00" 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'" />
>>>>>>> 97e504d2aaa3575761ffebbb10698bdbc831576b
<link rel="preload" href="icons.css" 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'" /> <link rel="preload" href="libs/jquery-ui.css" as="style" onload="this.onload=null; this.rel='stylesheet'" />
</head> </head>
@ -344,6 +348,10 @@
</g> </g>
<g id="deftemp"> <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="land"></mask>
<mask id="water"> <mask id="water">
<rect x="0" y="0" width="100%" height="100%" fill="white" /> <rect x="0" y="0" width="100%" height="100%" fill="white" />
@ -351,9 +359,6 @@
<mask id="fog" style="stroke-width: 10; stroke: black; stroke-linejoin: round; stroke-opacity: 0.1"> <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" /> <rect x="0" y="0" width="100%" height="100%" fill="white" stroke="none" />
</mask> </mask>
<g id="textPaths"></g>
<g id="statePaths"></g>
<g id="defs-emblems"></g>
</g> </g>
<pattern id="oceanic" width="100" height="100" patternUnits="userSpaceOnUse"> <pattern id="oceanic" width="100" height="100" patternUnits="userSpaceOnUse">
@ -438,7 +443,7 @@
<select <select
data-tip="Select a map layers preset" data-tip="Select a map layers preset"
id="layersPreset" id="layersPreset"
onchange="changePreset(this.value)" onchange="handleLayersPresetChange(this.value)"
style="width: 45%" style="width: 45%"
> >
<option value="political" selected>Political map</option> <option value="political" selected>Political map</option>
@ -478,7 +483,6 @@
id="toggleTexture" id="toggleTexture"
data-tip="Texture overlay: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style" data-tip="Texture overlay: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="X" data-shortcut="X"
class="buttonoff"
onclick="toggleTexture(event)" onclick="toggleTexture(event)"
> >
Te<u>x</u>ture Te<u>x</u>ture
@ -487,7 +491,6 @@
id="toggleHeight" id="toggleHeight"
data-tip="Heightmap: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style" data-tip="Heightmap: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="H" data-shortcut="H"
class="buttonoff"
onclick="toggleHeight(event)" onclick="toggleHeight(event)"
> >
<u>H</u>eightmap <u>H</u>eightmap
@ -496,7 +499,6 @@
id="toggleBiomes" id="toggleBiomes"
data-tip="Biomes: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style" data-tip="Biomes: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="B" data-shortcut="B"
class="buttonoff"
onclick="toggleBiomes(event)" onclick="toggleBiomes(event)"
> >
<u>B</u>iomes <u>B</u>iomes
@ -505,7 +507,6 @@
id="toggleCells" id="toggleCells"
data-tip="Cells structure: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style" data-tip="Cells structure: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="E" data-shortcut="E"
class="buttonoff"
onclick="toggleCells(event)" onclick="toggleCells(event)"
> >
C<u>e</u>lls C<u>e</u>lls
@ -514,7 +515,6 @@
id="toggleGrid" id="toggleGrid"
data-tip="Grid: click to toggle, drag to raise or lower. Ctrl + click to edit layer style and select type" data-tip="Grid: click to toggle, drag to raise or lower. Ctrl + click to edit layer style and select type"
data-shortcut="G" data-shortcut="G"
class="buttonoff"
onclick="toggleGrid(event)" onclick="toggleGrid(event)"
> >
<u>G</u>rid <u>G</u>rid
@ -523,7 +523,6 @@
id="toggleCoordinates" id="toggleCoordinates"
data-tip="Coordinate grid: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style" data-tip="Coordinate grid: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="O" data-shortcut="O"
class="buttonoff"
onclick="toggleCoordinates(event)" onclick="toggleCoordinates(event)"
> >
C<u>o</u>ordinates C<u>o</u>ordinates
@ -532,7 +531,6 @@
id="toggleCompass" id="toggleCompass"
data-tip="Wind (Compass) Rose: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style" data-tip="Wind (Compass) Rose: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="W" data-shortcut="W"
class="buttonoff"
onclick="toggleCompass(event)" onclick="toggleCompass(event)"
> >
<u>W</u>ind Rose <u>W</u>ind Rose
@ -549,7 +547,6 @@
id="toggleRelief" 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-tip="Relief and biome icons: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="F" data-shortcut="F"
class="buttonoff"
onclick="toggleRelief(event)" onclick="toggleRelief(event)"
> >
Relie<u>f</u> Relie<u>f</u>
@ -558,7 +555,6 @@
id="toggleReligions" id="toggleReligions"
data-tip="Religions: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style" data-tip="Religions: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="R" data-shortcut="R"
class="buttonoff"
onclick="toggleReligions(event)" onclick="toggleReligions(event)"
> >
<u>R</u>eligions <u>R</u>eligions
@ -567,7 +563,6 @@
id="toggleCultures" id="toggleCultures"
data-tip="Cultures: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style" data-tip="Cultures: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="C" data-shortcut="C"
class="buttonoff"
onclick="toggleCultures(event)" onclick="toggleCultures(event)"
> >
<u>C</u>ultures <u>C</u>ultures
@ -584,7 +579,6 @@
id="toggleProvinces" id="toggleProvinces"
data-tip="Provinces: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style" data-tip="Provinces: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="P" data-shortcut="P"
class="buttonoff"
onclick="toggleProvinces(event)" onclick="toggleProvinces(event)"
> >
<u>P</u>rovinces <u>P</u>rovinces
@ -593,7 +587,6 @@
id="toggleZones" id="toggleZones"
data-tip="Zones: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style" data-tip="Zones: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="Z" data-shortcut="Z"
class="buttonoff"
onclick="toggleZones(event)" onclick="toggleZones(event)"
> >
<u>Z</u>ones <u>Z</u>ones
@ -610,17 +603,15 @@
id="toggleRoutes" id="toggleRoutes"
data-tip="Trade routes: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style" data-tip="Trade routes: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="U" data-shortcut="U"
class="buttonoff"
onclick="toggleRoutes(event)" onclick="toggleRoutes(event)"
> >
Ro<u>u</u>tes Ro<u>u</u>tes
</li> </li>
<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-tip="Temperature map: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="T" data-shortcut="T"
class="buttonoff" onclick="toggleTemperature(event)"
onclick="toggleTemp(event)"
> >
<u>T</u>emperature <u>T</u>emperature
</li> </li>
@ -628,7 +619,6 @@
id="togglePopulation" id="togglePopulation"
data-tip="Population map: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style" data-tip="Population map: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="N" data-shortcut="N"
class="buttonoff"
onclick="togglePopulation(event)" onclick="togglePopulation(event)"
> >
Populatio<u>n</u> Populatio<u>n</u>
@ -637,17 +627,15 @@
id="toggleIce" id="toggleIce"
data-tip="Icebergs and glaciers: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style" data-tip="Icebergs and glaciers: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="J" data-shortcut="J"
class="buttonoff"
onclick="toggleIce(event)" onclick="toggleIce(event)"
> >
Ice Ice
</li> </li>
<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-tip="Precipitation map: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="A" data-shortcut="A"
class="buttonoff" onclick="togglePrecipitation(event)"
onclick="togglePrec(event)"
> >
Precipit<u>a</u>tion Precipit<u>a</u>tion
</li> </li>
@ -655,7 +643,6 @@
id="toggleEmblems" id="toggleEmblems"
data-tip="Emblems: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style" data-tip="Emblems: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="Y" data-shortcut="Y"
class="buttonoff"
onclick="toggleEmblems(event)" onclick="toggleEmblems(event)"
> >
Emblems Emblems
@ -669,10 +656,10 @@
<u>L</u>abels <u>L</u>abels
</li> </li>
<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-tip="Burg icons: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="I" data-shortcut="I"
onclick="toggleIcons(event)" onclick="toggleBurgIcons(event)"
> >
<u>I</u>cons <u>I</u>cons
</li> </li>
@ -680,7 +667,6 @@
id="toggleMilitary" id="toggleMilitary"
data-tip="Military forces: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style" data-tip="Military forces: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="M" data-shortcut="M"
class="buttonoff"
onclick="toggleMilitary(event)" onclick="toggleMilitary(event)"
> >
<u>M</u>ilitary <u>M</u>ilitary
@ -689,7 +675,6 @@
id="toggleMarkers" id="toggleMarkers"
data-tip="Markers: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style" data-tip="Markers: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="K" data-shortcut="K"
class="buttonoff"
onclick="toggleMarkers(event)" onclick="toggleMarkers(event)"
> >
Mar<u>k</u>ers Mar<u>k</u>ers
@ -698,7 +683,6 @@
id="toggleRulers" id="toggleRulers"
data-tip="Rulers: click to toggle, drag to move, click on label to delete. Ctrl + click to edit layer style" data-tip="Rulers: click to toggle, drag to move, click on label to delete. Ctrl + click to edit layer style"
data-shortcut="= (equal sign)" data-shortcut="= (equal sign)"
class="buttonoff"
onclick="toggleRulers(event)" onclick="toggleRulers(event)"
> >
Rulers Rulers
@ -1082,6 +1066,13 @@
<option value="pointyHex">Hex grid (pointy)</option> <option value="pointyHex">Hex grid (pointy)</option>
<option value="flatHex">Hex grid (flat)</option> <option value="flatHex">Hex grid (flat)</option>
<option value="square">Square grid</option> <option value="square">Square grid</option>
<option value="square45deg">Square 45 degrees grid</option>
<option value="squareTruncated">Truncated square grid</option>
<option value="squareTetrakis">Tetrakis square grid</option>
<option value="triangleHorizontal">Triangle grid (horizontal)</option>
<option value="triangleVertical">Triangle grid (vertical)</option>
<option value="trihexagonal">Trihexagonal grid</option>
<option value="rhombille">Rhombille grid</option>
</select> </select>
</td> </td>
</tr> </tr>
@ -1094,6 +1085,9 @@
id="styleGridSizeFriendly" id="styleGridSizeFriendly"
data-tip="Distance between grid cell centers (in map scale)" data-tip="Distance between grid cell centers (in map scale)"
></output> ></output>
<a href="https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Scale-and-distance#grids" target="_blank">
<span data-tip="Open wiki article scale and distance to know about grid scale" class="icon-info-circled pointer"></span>
</a>
</td> </td>
</tr> </tr>
@ -1529,7 +1523,9 @@
<td></td> <td></td>
</tr> </tr>
<tr data-tip="Map seed number. Seed produces the same map only if canvas size and options are the same"> <tr
data-tip="Map seed number. Press 'Enter' to apply. Seed produces the same map only if canvas size and options are the same"
>
<td> <td>
<i <i
data-tip="Show seed history to apply a previous seed" data-tip="Show seed history to apply a previous seed"
@ -1595,7 +1591,7 @@
<tr data-tip="Define current year and era name"> <tr data-tip="Define current year and era name">
<td> <td>
<i data-locked="0" id="lock_era" class="icon-lock-open"></i> <i data-locked="0" id="lock_year" data-ids="year,era" class="icon-lock-open"></i>
</td> </td>
<td>Year and era</td> <td>Year and era</td>
<td> <td>
@ -2008,8 +2004,8 @@
</button> </button>
<button <button
id="optionsReset" id="optionsReset"
data-tip="Click to restore default options (page will be reloaded)" data-tip="Click to restore default options and reload the page"
onclick="restoreDefaultOptions()" onclick="cleanupData()"
> >
Reset to defaults Reset to defaults
</button> </button>
@ -2093,7 +2089,7 @@
id="regenerateStateLabels" id="regenerateStateLabels"
data-tip="Click to update state labels placement based on current borders" data-tip="Click to update state labels placement based on current borders"
> >
Labels State Labels
</button> </button>
<button id="regenerateMarkers" data-tip="Click to regenerate unlocked markers"> <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> Markers <i id="configRegenerateMarkers" class="icon-cog" data-tip="Click to set number multiplier"></i>
@ -2126,7 +2122,7 @@
<button id="regenerateRoutes" data-tip="Click to regenerate all unlocked routes">Routes</button> <button id="regenerateRoutes" data-tip="Click to regenerate all unlocked routes">Routes</button>
<button <button
id="regenerateStates" 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 States
</button> </button>
@ -3521,7 +3517,8 @@
<button id="burgEditEmblem" data-tip="Edit emblem" class="icon-shield-alt"></button> <button id="burgEditEmblem" data-tip="Edit emblem" class="icon-shield-alt"></button>
<button id="burgTogglePreview" data-tip="Toggle preview" class="icon-map"></button> <button id="burgTogglePreview" data-tip="Toggle preview" class="icon-map"></button>
<button id="burgRelocate" data-tip="Relocate burg" class="icon-target"></button> <button id="burgLocate" data-tip="Zoom map and center view in the burg" class="icon-target"></button>
<button id="burgRelocate" data-tip="Relocate burg. Click on map to move the burg" class="icon-map-pin"></button>
<button id="burglLegend" data-tip="Edit free text notes (legend) for this burg" class="icon-edit"></button> <button id="burglLegend" data-tip="Edit free text notes (legend) for this burg" class="icon-edit"></button>
<button id="burgLock" class="icon-lock-open" onmouseover="showElementLockTip(event)"></button> <button id="burgLock" class="icon-lock-open" onmouseover="showElementLockTip(event)"></button>
<button <button
@ -5733,12 +5730,13 @@
</p> </p>
<p><b>Latitude:</b> <span id="infoLat"></span></p> <p><b>Latitude:</b> <span id="infoLat"></span></p>
<p><b>Longitude:</b> <span id="infoLon"></span></p> <p><b>Longitude:</b> <span id="infoLon"></span></p>
<p><b>Geozone:</b> <span id="infoGeozone"></span></p>
<p><b>Area:</b> <span id="infoArea">0</span></p> <p><b>Area:</b> <span id="infoArea">0</span></p>
<p><b>Type:</b> <span id="infoFeature">n/a</span></p> <p><b>Type:</b> <span id="infoFeature">n/a</span></p>
<p><b>Precipitation:</b> <span id="infoPrec">0</span></p> <p><b>Precipitation:</b> <span id="infoPrec">0</span></p>
<p><b>River:</b> <span id="infoRiver">no</span></p> <p><b>River:</b> <span id="infoRiver">no</span></p>
<p><b>Population:</b> <span id="infoPopulation">0</span></p> <p><b>Population:</b> <span id="infoPopulation">0</span></p>
<p><b>Elevation:</b> <span id="infoEvelation">0</span></p> <p><b>Elevation:</b> <span id="infoElevation">0</span></p>
<p><b>Depth:</b> <span id="infoDepth">0</span></p> <p><b>Depth:</b> <span id="infoDepth">0</span></p>
<p><b>Temperature:</b> <span id="infoTemp">0</span></p> <p><b>Temperature:</b> <span id="infoTemp">0</span></p>
<p><b>Biome:</b> <span id="infoBiome">n/a</span></p> <p><b>Biome:</b> <span id="infoBiome">n/a</span></p>
@ -7850,6 +7848,27 @@
<pattern id="pattern_flatHex" width="43.4" height="25" patternUnits="userSpaceOnUse" fill="none"> <pattern id="pattern_flatHex" width="43.4" height="25" patternUnits="userSpaceOnUse" fill="none">
<path d="M 43.4,0 36.2,12.5 43.4,25 M 21.7,12.5 H 36.2 Z M 0,0 H 14.5 L 21.7,12.5 14.5,25 H 0" /> <path d="M 43.4,0 36.2,12.5 43.4,25 M 21.7,12.5 H 36.2 Z M 0,0 H 14.5 L 21.7,12.5 14.5,25 H 0" />
</pattern> </pattern>
<pattern id="pattern_square45deg" width="35.355" height="35.355" patternUnits="userSpaceOnUse" fill="none">
<path d="M 0 0 L 35.355 35.355 M 0 35.355 L 35.355 0" />
</pattern>
<pattern id="pattern_squareTruncated" width="25" height="25" patternUnits="userSpaceOnUse" fill="none">
<path d="M 8.33 25 L 0 16.66 V 8.33 L 8.33 0 16.66 0 25 8.33 M 16.66 25 L 25 16.66 L 25 8.33 M 8.33 25 L 16.66 25" />
</pattern>
<pattern id="pattern_squareTetrakis" width="25" height="25" patternUnits="userSpaceOnUse" fill="none">
<path d="M 25 0 L 0 0 0 25 M 0 0 L 25 25 M 0 25 L 25 0 M 12.5 0 L 12.5 25 M 0 12.5 L 25 12.5 M 0 25 L 25 25 L 25 0" />
</pattern>
<pattern id="pattern_triangleHorizontal" width="41.76" height="72.33" patternUnits="userSpaceOnUse" fill="none">
<path d="M 41.76 36.165 H 0 L 20.88 0 41.76 36.165 20.88 72.33 0 36.165 M 0 0 H 72.33 M 0 72.33 L 41.76 72.33" />
</pattern>
<pattern id="pattern_triangleVertical" width="72.33" height="41.76" patternUnits="userSpaceOnUse" fill="none">
<path d="M 36.165 0 L 0 20.88 36.165 41.76 72.33 20.88 36.165 0 V 41.76 M 0 0 V 72.33 M 72.33 0 L 72.33 41.76">
</pattern>
<pattern id="pattern_trihexagonal" width="25" height="43.4" patternUnits="userSpaceOnUse" fill="none">
<path d="M 25 10.85 H 0 L 18.85 43.4 25 32.55 H 0 L 18.85 0 25 10.85" />
</pattern>
<pattern id="pattern_rhombille" width="82.5" height="50" patternUnits="userSpaceOnUse" fill="none">
<path d="M 13.8 50 L 0 25 13.8 0 H 41.2 L 27.5 25 41.2 50 55 25 41.2 0 68.8 0 82.5 25 68.8 50 M 0 25 H 27.5 M 55 25 H 82.5 M 13.8 50 H 41.2 L 68.8 50" />
</pattern>
</g> </g>
<g id="defs-hatching"> <g id="defs-hatching">
@ -8014,7 +8033,7 @@
<script src="libs/indexedDB.js?v=1.99.00"></script> <script src="libs/indexedDB.js?v=1.99.00"></script>
<script src="utils/shorthands.js?v=1.99.00"></script> <script src="utils/shorthands.js?v=1.99.00"></script>
<script src="utils/commonUtils.js?v=1.99.00"></script> <script src="utils/commonUtils.js?v=1.103.0"></script>
<script src="utils/arrayUtils.js?v=1.99.00"></script> <script src="utils/arrayUtils.js?v=1.99.00"></script>
<script src="utils/functionUtils.js?v=1.99.00"></script> <script src="utils/functionUtils.js?v=1.99.00"></script>
<script src="utils/colorUtils.js?v=1.99.00"></script> <script src="utils/colorUtils.js?v=1.99.00"></script>
@ -8026,54 +8045,55 @@
<script src="utils/stringUtils.js?v=1.99.00"></script> <script src="utils/stringUtils.js?v=1.99.00"></script>
<script src="utils/languageUtils.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/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 defer src="utils/debugUtils.js?v=1.99.00"></script>
<script src="modules/voronoi.js"></script> <script src="modules/voronoi.js"></script>
<script src="config/heightmap-templates.js"></script> <script src="config/heightmap-templates.js"></script>
<script src="config/precreated-heightmaps.js"></script> <script src="config/precreated-heightmaps.js"></script>
<script src="modules/heightmap-generator.js?v=1.99.00"></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.8"></script>
<script src="modules/river-generator.js?v=1.99.05"></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/lakes.js?v=1.99.00"></script>
<script src="modules/biomes.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/names-generator.js?v=1.87.14"></script>
<script src="modules/cultures-generator.js?v=1.99.05"></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.104.0"></script>
<script src="modules/burgs-and-states.js?v=1.99.05"></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/routes-generator.js?v=1.104.10"></script>
<script src="modules/religions-generator.js?v=1.99.05"></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/military-generator.js?v=1.104.0"></script>
<script src="modules/markers-generator.js?v=1.99.00"></script> <script src="modules/markers-generator.js?v=1.104.0"></script>
<script src="modules/zones-generator.js?v=1.100.00"></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/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/polylabel.min.js"></script>
<script src="libs/lineclip.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/fonts.js?v=1.99.03"></script>
<script src="modules/ui/layers.js?v=1.101.00"></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/measurers.js?v=1.99.00"></script>
<script src="modules/ui/style-presets.js?v=1.100.00"></script> <script src="modules/ui/style-presets.js?v=1.100.00"></script>
<script src="modules/ui/general.js?v=1.100.00"></script> <script src="modules/ui/general.js?v=1.100.00"></script>
<script src="modules/ui/options.js?v=1.102.00"></script> <script src="modules/ui/options.js?v=1.102.00"></script>
<script src="main.js?v=1.102.00"></script> <script src="main.js?v=1.105.0"></script>
<script defer src="modules/relief-icons.js?v=1.99.05"></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/editors.js?v=1.104.3"></script>
<script defer src="modules/ui/tools.js?v=1.100.00"></script> <script defer src="modules/ui/tools.js?v=1.104.0"></script>
<script defer src="modules/ui/world-configurator.js?v=1.99.00"></script> <script defer src="modules/ui/world-configurator.js?v=1.104.0"></script>
<script defer src="modules/ui/heightmap-editor.js?v=1.100.00"></script> <script defer src="modules/ui/heightmap-editor.js?v=1.104.9"></script>
<script defer src="modules/ui/provinces-editor.js?v=1.99.05"></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/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/namesbase-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/elevation-profile.js?v=1.99.00"></script> <script defer src="modules/ui/elevation-profile.js?v=1.99.00"></script>
<script defer src="modules/ui/temperature-graph.js?v=1.99.00"></script> <script defer src="modules/ui/temperature-graph.js?v=1.99.00"></script>
<script defer src="modules/ui/routes-editor.js?v=1.99.02"></script> <script defer src="modules/ui/routes-editor.js?v=1.104.3"></script>
<script defer src="modules/ui/routes-creator.js?v=1.99.02"></script> <script defer src="modules/ui/routes-creator.js?v=1.104.3"></script>
<script defer src="modules/ui/route-group-editor.js?v=1.99.00"></script> <script defer src="modules/ui/route-group-editor.js?v=1.103.8"></script>
<script defer src="modules/ui/ice-editor.js?v=1.99.00"></script> <script defer src="modules/ui/ice-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/lakes-editor.js?v=1.99.00"></script> <script defer src="modules/ui/lakes-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/coastline-editor.js?v=1.99.00"></script> <script defer src="modules/ui/coastline-editor.js?v=1.99.00"></script>
@ -8081,31 +8101,47 @@
<script defer src="modules/ui/rivers-editor.js?v=1.99.00"></script> <script defer src="modules/ui/rivers-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/rivers-creator.js?v=1.99.00"></script> <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/relief-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/burg-editor.js?v=1.100.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/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/ai-generator.js?v=1.99.09"></script>
<script defer src="modules/ui/diplomacy-editor.js?v=1.99.00"></script> <script defer src="modules/ui/diplomacy-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/zones-editor.js?v=1.100.00"></script> <script defer src="modules/ui/zones-editor.js?v=1.100.00"></script>
<script defer src="modules/ui/burgs-overview.js?v=1.100.00"></script> <script defer src="modules/ui/burgs-overview.js?v=1.100.00"></script>
<script defer src="modules/ui/routes-overview.js?v=1.100.00"></script> <script defer src="modules/ui/routes-overview.js?v=1.104.3"></script>
<script defer src="modules/ui/rivers-overview.js?v=1.99.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/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/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/regiment-editor.js?v=1.104.14"></script>
<script defer src="modules/ui/battle-screen.js?v=1.99.00"></script> <script defer src="modules/ui/battle-screen.js?v=1.99.00"></script>
<script defer src="modules/ui/emblems-editor.js?v=1.99.00"></script> <script defer src="modules/ui/emblems-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/markers-editor.js?v=1.99.00"></script> <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/3d.js?v=1.99.00"></script>
<script defer src="modules/ui/submap.js?v=1.99.10"></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="modules/coa-renderer.js?v=1.99.00"></script>
<script defer src="libs/rgbquant.min.js"></script> <script defer src="libs/rgbquant.min.js"></script>
<script defer src="libs/jquery.ui.touch-punch.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/save.js?v=1.100.00"></script>
<script defer src="modules/io/load.js?v=1.100.00"></script> <script defer src="modules/io/load.js?v=1.104.12"></script>
<script defer src="modules/io/cloud.js?v=1.99.00"></script> <script defer src="modules/io/cloud.js?v=1.99.00"></script>
<<<<<<< HEAD
<script defer src="modules/io/export.js?v=1.99.14"></script> <script defer src="modules/io/export.js?v=1.99.14"></script>
=======
<script defer src="modules/io/export.js?v=1.100.00"></script>
<script defer src="modules/renderers/draw-features.js?v=1.104.15"></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.13"></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>
>>>>>>> 97e504d2aaa3575761ffebbb10698bdbc831576b
</body> </body>
</html> </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;
}

336
main.js
View file

@ -14,6 +14,7 @@ const ERROR = true;
const MOBILE = window.innerWidth < 600 || navigator.userAgentData?.mobile; const MOBILE = window.innerWidth < 600 || navigator.userAgentData?.mobile;
// typed arrays max values // typed arrays max values
const INT8_MAX = 127;
const UINT8_MAX = 255; const UINT8_MAX = 255;
const UINT16_MAX = 65535; const UINT16_MAX = 65535;
const UINT32_MAX = 4294967295; 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 searoutes = routes.append("g").attr("id", "searoutes");
let temperature = viewbox.append("g").attr("id", "temperature"); let temperature = viewbox.append("g").attr("id", "temperature");
let coastline = viewbox.append("g").attr("id", "coastline"); 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 prec = viewbox.append("g").attr("id", "prec").style("display", "none");
let population = viewbox.append("g").attr("id", "population"); let population = viewbox.append("g").attr("id", "population");
let emblems = viewbox.append("g").attr("id", "emblems").style("display", "none"); 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 icons = viewbox.append("g").attr("id", "icons");
let burgIcons = icons.append("g").attr("id", "burgIcons"); let burgIcons = icons.append("g").attr("id", "burgIcons");
let anchors = icons.append("g").attr("id", "anchors"); 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 markers = viewbox.append("g").attr("id", "markers");
let fogging = viewbox let fogging = viewbox
.append("g") .append("g")
@ -314,7 +315,8 @@ async function checkLoadParameters() {
async function generateMapOnLoad() { async function generateMapOnLoad() {
await applyStyleOnLoad(); // apply previously selected default or custom style await applyStyleOnLoad(); // apply previously selected default or custom style
await generate(); // generate map await generate(); // generate map
applyPreset(); // apply saved layers preset applyLayersPreset(); // apply saved layers preset and reder layers
drawLayers();
fitMapToScreen(); fitMapToScreen();
focusOn(); // based on searchParams focus on point, cell or burg from MFCG focusOn(); // based on searchParams focus on point, cell or burg from MFCG
toggleAssistant(); toggleAssistant();
@ -447,7 +449,9 @@ function findBurgForMFCG(params) {
function handleZoom(isScaleChanged, isPositionChanged) { function handleZoom(isScaleChanged, isPositionChanged) {
viewbox.attr("transform", `translate(${viewX} ${viewY}) scale(${scale})`); viewbox.attr("transform", `translate(${viewX} ${viewY}) scale(${scale})`);
if (isPositionChanged) drawCoordinates(); if (isPositionChanged) {
if (layerIsOn("toggleCoordinates")) drawCoordinates();
}
if (isScaleChanged) { if (isScaleChanged) {
invokeActiveZooming(); invokeActiveZooming();
@ -559,21 +563,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 // add drag to upload logic, pull request from @evyatron
void (function addDragToUpload() { void (function addDragToUpload() {
document.addEventListener("dragover", function (e) { document.addEventListener("dragover", function (e) {
@ -639,8 +628,7 @@ async function generate(options) {
grid.cells.h = await HeightmapGenerator.generate(grid); grid.cells.h = await HeightmapGenerator.generate(grid);
pack = {}; // reset pack pack = {}; // reset pack
markFeatures(); Features.markupGrid();
markupGridOcean();
addLakesInDeepDepressions(); addLakesInDeepDepressions();
openNearSeaLakes(); openNearSeaLakes();
@ -651,11 +639,10 @@ async function generate(options) {
generatePrecipitation(); generatePrecipitation();
reGraph(); reGraph();
drawCoastline(); Features.markupPack();
createDefaultRuler();
Rivers.generate(); Rivers.generate();
drawRivers();
Lakes.defineGroup();
Biomes.define(); Biomes.define();
rankCells(); rankCells();
@ -665,15 +652,12 @@ async function generate(options) {
Routes.generate(); Routes.generate();
Religions.generate(); Religions.generate();
BurgsAndStates.defineStateForms(); BurgsAndStates.defineStateForms();
BurgsAndStates.generateProvinces(); Provinces.generate();
Provinces.getPoles();
BurgsAndStates.defineBurgFeatures(); BurgsAndStates.defineBurgFeatures();
drawStates();
drawBorders();
drawStateLabels();
Rivers.specify(); Rivers.specify();
Lakes.generateName(); Features.specify();
Military.generate(); Military.generate();
Markers.generate(); Markers.generate();
@ -697,10 +681,7 @@ async function generate(options) {
title: "Generation error", title: "Generation error",
width: "32em", width: "32em",
buttons: { buttons: {
"Clear data": function () { "Cleanup data": cleanupData,
localStorage.clear();
localStorage.setItem("version", VERSION);
},
Regenerate: function () { Regenerate: function () {
regenerateMap("generation error"); regenerateMap("generation error");
$(this).dialog("close"); $(this).dialog("close");
@ -731,69 +712,6 @@ function setSeed(precreatedSeed) {
Math.random = aleaPRNG(seed); 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() { function addLakesInDeepDepressions() {
TIME && console.time("addLakesInDeepDepressions"); TIME && console.time("addLakesInDeepDepressions");
const {cells, features} = grid; const {cells, features} = grid;
@ -1235,222 +1153,6 @@ function reGraph() {
TIME && console.timeEnd("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) { function isWetLand(moisture, temperature, height) {
if (moisture > 40 && temperature > -2 && height < 25) return true; //near coast if (moisture > 40 && temperature > -2 && height < 25) return true; //near coast
if (moisture > 24 && temperature > -2 && height > 24 && height < 60) return true; //off coast if (moisture > 24 && temperature > -2 && height > 24 && height < 60) return true; //off coast
@ -1508,7 +1210,8 @@ function showStatistics() {
const stats = ` Seed: ${seed} const stats = ` Seed: ${seed}
Canvas size: ${graphWidth}x${graphHeight} px Canvas size: ${graphWidth}x${graphHeight} px
Heightmap: ${heightmap} (${isRandomTemplate}${heightmapType}) Heightmap: ${heightmap}
Template: ${isRandomTemplate}${heightmapType}
Points: ${grid.points.length} Points: ${grid.points.length}
Cells: ${pack.cells.i.length} Cells: ${pack.cells.i.length}
Map size: ${mapSizeOutput.value}% Map size: ${mapSizeOutput.value}%
@ -1516,7 +1219,7 @@ function showStatistics() {
Provinces: ${pack.provinces.length - 1} Provinces: ${pack.provinces.length - 1}
Burgs: ${pack.burgs.length - 1} Burgs: ${pack.burgs.length - 1}
Religions: ${pack.religions.length - 1} Religions: ${pack.religions.length - 1}
Culture set: ${culturesSet.selectedOptions[0].innerText} Culture set: ${culturesSet.value}
Cultures: ${pack.cultures.length - 1}`; Cultures: ${pack.cultures.length - 1}`;
mapId = Date.now(); // unique map id is it's creation date number mapId = Date.now(); // unique map id is it's creation date number
@ -1536,7 +1239,7 @@ const regenerateMap = debounce(async function (options) {
resetZoom(1000); resetZoom(1000);
undraw(); undraw();
await generate(options); await generate(options);
restoreLayers(); drawLayers();
if (ThreeD.options.isOn) ThreeD.redraw(); if (ThreeD.options.isOn) ThreeD.redraw();
if ($("#worldConfigurator").is(":visible")) editWorld(); if ($("#worldConfigurator").is(":visible")) editWorld();
@ -1555,6 +1258,5 @@ function undraw() {
.forEach(el => el.remove()); .forEach(el => el.remove());
byId("coas").innerHTML = ""; // remove auto-generated emblems byId("coas").innerHTML = ""; // remove auto-generated emblems
notes = []; notes = [];
rulers = new Rulers();
unfog(); unfog();
} }

View file

@ -13,6 +13,8 @@ window.BurgsAndStates = (() => {
placeTowns(); placeTowns();
expandStates(); expandStates();
normalizeStates(); normalizeStates();
getPoles();
specifyBurgs(); specifyBurgs();
collectStatistics(); collectStatistics();
@ -20,7 +22,6 @@ window.BurgsAndStates = (() => {
generateCampaigns(); generateCampaigns();
generateDiplomacy(); generateDiplomacy();
drawBurgs();
function placeCapitals() { function placeCapitals() {
TIME && console.time("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) // expand cultures across the map (Dijkstra-like algorithm)
const expandStates = () => { const expandStates = () => {
TIME && console.time("expandStates"); TIME && console.time("expandStates");
@ -468,8 +372,7 @@ window.BurgsAndStates = (() => {
const normalizeStates = () => { const normalizeStates = () => {
TIME && console.time("normalizeStates"); TIME && console.time("normalizeStates");
const cells = pack.cells, const {cells, burgs} = pack;
burgs = pack.burgs;
for (const i of cells.i) { for (const i of cells.i) {
if (cells.h[i] < 20 || cells.burg[i]) continue; // do not overwrite burgs if (cells.h[i] < 20 || cells.burg[i]) continue; // do not overwrite burgs
@ -486,26 +389,30 @@ window.BurgsAndStates = (() => {
TIME && console.timeEnd("normalizeStates"); TIME && console.timeEnd("normalizeStates");
}; };
// Resets the cultures of all burgs and states to their // calculate pole of inaccessibility for each state
// cell or center cell's (respectively) culture. 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 = () => { const updateCultures = () => {
TIME && console.time("updateCulturesForBurgsAndStates"); 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) => { 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]}; 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) => { 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]}; 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}`; 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 { return {
generate, generate,
expandStates, expandStates,
normalizeStates, normalizeStates,
getPoles,
assignColors, assignColors,
drawBurgs,
specifyBurgs, specifyBurgs,
defineBurgFeatures, defineBurgFeatures,
getType, getType,
@ -1205,7 +871,6 @@ window.BurgsAndStates = (() => {
generateDiplomacy, generateDiplomacy,
defineStateForms, defineStateForms,
getFullName, getFullName,
generateProvinces,
updateCultures updateCultures
}; };
})(); })();

View file

@ -51,9 +51,8 @@ export function resolveVersionConflicts(mapVersion) {
BurgsAndStates.generateCampaigns(); BurgsAndStates.generateCampaigns();
BurgsAndStates.generateDiplomacy(); BurgsAndStates.generateDiplomacy();
BurgsAndStates.defineStateForms(); BurgsAndStates.defineStateForms();
drawStates(); Provinces.generate();
BurgsAndStates.generateProvinces(); Provinces.getPoles();
drawBorders();
if (!layerIsOn("toggleBorders")) $("#borders").fadeOut(); if (!layerIsOn("toggleBorders")) $("#borders").fadeOut();
if (!layerIsOn("toggleStates")) regions.attr("display", "none").selectAll("path").remove(); if (!layerIsOn("toggleStates")) regions.attr("display", "none").selectAll("path").remove();
@ -202,7 +201,9 @@ export function resolveVersionConflicts(mapVersion) {
defs.select("#water").selectAll("path").remove(); defs.select("#water").selectAll("path").remove();
coastline.selectAll("path").remove(); coastline.selectAll("path").remove();
lakes.selectAll("path").remove(); lakes.selectAll("path").remove();
drawCoastline();
Features.markupPack();
createDefaultRuler();
} }
if (isOlderThan("1.11.0")) { if (isOlderThan("1.11.0")) {
@ -940,4 +941,24 @@ export function resolveVersionConflicts(mapVersion) {
zones.style("display", null).selectAll("*").remove(); zones.style("display", null).selectAll("*").remove();
if (layerIsOn("toggleZones")) drawZones(); 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("#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");
}
if (isOlderThan("1.105.0")) {
// v1.104.0 introduced some bugs
viewbox.select("#regions").style("display", null);
viewbox.select("#armies").style("display", null);
}
} }

View file

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

View file

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

View file

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

View file

@ -580,4 +580,6 @@ MisterPete
Johanna Martin Johanna Martin
Marmalade_MacGuffin Marmalade_MacGuffin
James Benware James Benware
FortunesFaded`; FortunesFaded
breadsticks
Murderbits`;

271
modules/features.js Normal file
View file

@ -0,0 +1,271 @@
"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 [startCell, featureVertices] = getCellsData(type, 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,
cells: totalCells,
firstCell: startCell,
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 getCellsData(featureType, firstCell) {
if (featureType === "ocean") return [firstCell, []];
const getType = cellId => featureIds[cellId];
const type = getType(firstCell);
const ofSameType = cellId => getType(cellId) === type;
const ofDifferentType = cellId => getType(cellId) !== type;
const startCell = findOnBorderCell(firstCell);
const featureVertices = getFeatureVertices(startCell);
return [startCell, featureVertices];
function findOnBorderCell(firstCell) {
const isOnBorder = cellId => borderCells[cellId] || neighbors[cellId].some(ofDifferentType);
if (isOnBorder(firstCell)) return firstCell;
const startCell = cells.i.filter(ofSameType).find(isOnBorder);
if (startCell === undefined)
throw new Error(`Markup: firstCell ${firstCell} is not on the feature or map border`);
return startCell;
}
function getFeatureVertices(startCell) {
const startingVertex = cells.v[startCell].find(v => vertices.c[v].some(ofDifferentType));
if (startingVertex === undefined)
throw new Error(`Markup: startingVertex for cell ${startCell} 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, noWater = false,
noScaleBar = false, noScaleBar = false,
noIce = false, noIce = false,
noVignette = false,
fullMap = false fullMap = false
} = options || {}; } = options || {};
@ -199,6 +200,7 @@ async function getMapURL(type, options) {
clone.select("#oceanPattern").attr("opacity", 0); clone.select("#oceanPattern").attr("opacity", 0);
} }
if (noIce) clone.select("#ice")?.remove(); if (noIce) clone.select("#ice")?.remove();
if (noVignette) clone.select("#vignette")?.remove();
if (fullMap) { if (fullMap) {
// reset transform to show the whole map // reset transform to show the whole map
clone.attr("width", graphWidth).attr("height", graphHeight); clone.attr("width", graphWidth).attr("height", graphHeight);

View file

@ -1,4 +1,5 @@
"use strict"; "use strict";
// Functions to load and parse .map/.gz files // Functions to load and parse .map/.gz files
async function quickLoad() { async function quickLoad() {
const blob = await ldb.get("lastMap"); const blob = await ldb.get("lastMap");
@ -109,19 +110,23 @@ function uploadMap(file, callback) {
fileReader.onloadend = async function (fileLoadedEvent) { fileReader.onloadend = async function (fileLoadedEvent) {
if (callback) callback(); if (callback) callback();
byId("coas").innerHTML = ""; // remove auto-generated emblems byId("coas").innerHTML = ""; // remove auto-generated emblems
const result = fileLoadedEvent.target.result; const result = fileLoadedEvent.target.result;
const [mapData, mapVersion] = await parseLoadedResult(result); const {mapData, mapVersion} = await parseLoadedResult(result);
const isInvalid = !mapData || !isValidVersion(mapVersion) || mapData.length < 26 || !mapData[5];
const isUpdated = compareVersions(mapVersion, VERSION).isEqual;
const isAncient = compareVersions(mapVersion, "0.7.0").isOlder;
const isNewer = compareVersions(mapVersion, VERSION).isNewer;
const isOutdated = compareVersions(mapVersion, VERSION).isOlder;
const isInvalid = !mapData || !isValidVersion(mapVersion) || mapData.length < 10 || !mapData[5];
if (isInvalid) return showUploadMessage("invalid", mapData, mapVersion); if (isInvalid) return showUploadMessage("invalid", mapData, mapVersion);
const isUpdated = compareVersions(mapVersion, VERSION).isEqual;
if (isUpdated) return showUploadMessage("updated", mapData, mapVersion); if (isUpdated) return showUploadMessage("updated", mapData, mapVersion);
const isAncient = compareVersions(mapVersion, "0.70.0").isOlder;
if (isAncient) return showUploadMessage("ancient", mapData, mapVersion); if (isAncient) return showUploadMessage("ancient", mapData, mapVersion);
const isNewer = compareVersions(mapVersion, VERSION).isNewer;
if (isNewer) return showUploadMessage("newer", mapData, mapVersion); if (isNewer) return showUploadMessage("newer", mapData, mapVersion);
const isOutdated = compareVersions(mapVersion, VERSION).isOlder;
if (isOutdated) return showUploadMessage("outdated", mapData, mapVersion); if (isOutdated) return showUploadMessage("outdated", mapData, mapVersion);
}; };
@ -151,16 +156,16 @@ async function parseLoadedResult(result) {
const isDelimited = resultAsString.substring(0, 10).includes("|"); const isDelimited = resultAsString.substring(0, 10).includes("|");
const decoded = isDelimited ? resultAsString : decodeURIComponent(atob(resultAsString)); const decoded = isDelimited ? resultAsString : decodeURIComponent(atob(resultAsString));
const mapData = decoded.split("\r\n"); const mapData = decoded.split("\r\n"); // split by CRLF
const mapVersionString = mapData[0].split("|")[0] || mapData[0] || ""; const mapVersion = parseMapVersion(mapData[0].split("|")[0] || mapData[0] || "");
return [mapData, mapVersionString];
return {mapData, mapVersion};
} catch (error) { } catch (error) {
// map file can be compressed with gzip const uncompressedData = await uncompress(result); // file can be gzip compressed
const uncompressedData = await uncompress(result);
if (uncompressedData) return parseLoadedResult(uncompressedData); if (uncompressedData) return parseLoadedResult(uncompressedData);
ERROR && console.error(error); ERROR && console.error(error);
return [null, null]; return {mapData: null, mapVersion: null};
} }
} }
@ -204,21 +209,19 @@ async function parseLoadedData(data, mapVersion) {
customization = 0; customization = 0;
if (customizationMenu.offsetParent) styleTab.click(); if (customizationMenu.offsetParent) styleTab.click();
const params = data[0].split("|"); {
void (function parseParameters() { const params = data[0].split("|");
if (params[3]) { if (params[3]) {
seed = params[3]; seed = params[3];
optionsSeed.value = seed; optionsSeed.value = seed;
} INFO && console.group("Loaded Map " + seed);
} else INFO && console.group("Loaded Map");
if (params[4]) graphWidth = +params[4]; if (params[4]) graphWidth = +params[4];
if (params[5]) graphHeight = +params[5]; if (params[5]) graphHeight = +params[5];
mapId = params[6] ? +params[6] : Date.now(); mapId = params[6] ? +params[6] : Date.now();
})(); }
INFO && console.group("Loaded Map " + seed); {
// TODO: move all to options object
void (function parseSettings() {
const settings = data[1].split("|"); const settings = data[1].split("|");
if (settings[0]) applyOption(distanceUnitInput, settings[0]); if (settings[0]) applyOption(distanceUnitInput, settings[0]);
if (settings[1]) distanceScale = distanceScaleInput.value = settings[1]; if (settings[1]) distanceScale = distanceScaleInput.value = settings[1];
@ -242,16 +245,16 @@ async function parseLoadedData(data, mapVersion) {
if (settings[23]) rescaleLabels.checked = +settings[23]; if (settings[23]) rescaleLabels.checked = +settings[23];
if (settings[24]) urbanDensity = urbanDensityInput.value = +settings[24]; if (settings[24]) urbanDensity = urbanDensityInput.value = +settings[24];
if (settings[25]) longitudeInput.value = longitudeOutput.value = minmax(settings[25] || 50, 0, 100); if (settings[25]) longitudeInput.value = longitudeOutput.value = minmax(settings[25] || 50, 0, 100);
})(); }
void (function applyOptionsToUI() { {
stateLabelsModeInput.value = options.stateLabelsMode; stateLabelsModeInput.value = options.stateLabelsMode;
yearInput.value = options.year; yearInput.value = options.year;
eraInput.value = options.era; eraInput.value = options.era;
shapeRendering.value = viewbox.attr("shape-rendering") || "geometricPrecision"; shapeRendering.value = viewbox.attr("shape-rendering") || "geometricPrecision";
})(); }
void (function parseConfiguration() { {
if (data[2]) mapCoordinates = JSON.parse(data[2]); if (data[2]) mapCoordinates = JSON.parse(data[2]);
if (data[4]) notes = JSON.parse(data[4]); if (data[4]) notes = JSON.parse(data[4]);
if (data[33]) rulers.fromString(data[33]); if (data[33]) rulers.fromString(data[33]);
@ -267,13 +270,14 @@ async function parseLoadedData(data, mapVersion) {
declareFont(usedFont); declareFont(usedFont);
}); });
} }
}
{
const biomes = data[3].split("|"); const biomes = data[3].split("|");
biomesData = Biomes.getDefault(); biomesData = Biomes.getDefault();
biomesData.color = biomes[0].split(","); biomesData.color = biomes[0].split(",");
biomesData.habitability = biomes[1].split(",").map(h => +h); biomesData.habitability = biomes[1].split(",").map(h => +h);
biomesData.name = biomes[2].split(","); biomesData.name = biomes[2].split(",");
// push custom biomes if any // push custom biomes if any
for (let i = biomesData.i.length; i < biomesData.name.length; i++) { for (let i = biomesData.i.length; i < biomesData.name.length; i++) {
biomesData.i.push(biomesData.i.length); biomesData.i.push(biomesData.i.length);
@ -281,14 +285,14 @@ async function parseLoadedData(data, mapVersion) {
biomesData.icons.push([]); biomesData.icons.push([]);
biomesData.cost.push(50); biomesData.cost.push(50);
} }
})(); }
void (function replaceSVG() { {
svg.remove(); svg.remove();
document.body.insertAdjacentHTML("afterbegin", data[5]); document.body.insertAdjacentHTML("afterbegin", data[5]);
})(); }
void (function redefineElements() { {
svg = d3.select("#map"); svg = d3.select("#map");
defs = svg.select("#deftemp"); defs = svg.select("#deftemp");
viewbox = svg.select("#viewbox"); viewbox = svg.select("#viewbox");
@ -338,38 +342,33 @@ async function parseLoadedData(data, mapVersion) {
fogging = viewbox.select("#fogging"); fogging = viewbox.select("#fogging");
debug = viewbox.select("#debug"); debug = viewbox.select("#debug");
burgLabels = labels.select("#burgLabels"); burgLabels = labels.select("#burgLabels");
})();
void (function addMissingElements() {
if (!texture.size()) { if (!texture.size()) {
texture = viewbox texture = viewbox
.insert("g", "#landmass") .insert("g", "#landmass")
.attr("id", "texture") .attr("id", "texture")
.attr("data-href", "./images/textures/plaster.jpg"); .attr("data-href", "./images/textures/plaster.jpg");
} }
if (!emblems.size()) { if (!emblems.size()) {
emblems = viewbox.insert("g", "#labels").attr("id", "emblems").style("display", "none"); emblems = viewbox.insert("g", "#labels").attr("id", "emblems").style("display", "none");
} }
})(); }
void (function parseGridData() { {
grid = JSON.parse(data[6]); grid = JSON.parse(data[6]);
const {cells, vertices} = calculateVoronoi(grid.points, grid.boundary); const {cells, vertices} = calculateVoronoi(grid.points, grid.boundary);
grid.cells = cells; grid.cells = cells;
grid.vertices = vertices; grid.vertices = vertices;
grid.cells.h = Uint8Array.from(data[7].split(",")); grid.cells.h = Uint8Array.from(data[7].split(","));
grid.cells.prec = Uint8Array.from(data[8].split(",")); grid.cells.prec = Uint8Array.from(data[8].split(","));
grid.cells.f = Uint16Array.from(data[9].split(",")); grid.cells.f = Uint16Array.from(data[9].split(","));
grid.cells.t = Int8Array.from(data[10].split(",")); grid.cells.t = Int8Array.from(data[10].split(","));
grid.cells.temp = Int8Array.from(data[11].split(",")); grid.cells.temp = Int8Array.from(data[11].split(","));
})(); }
void (function parsePackData() { {
reGraph(); reGraph();
reMarkFeatures(); Features.markupPack();
pack.features = JSON.parse(data[12]); pack.features = JSON.parse(data[12]);
pack.cultures = JSON.parse(data[13]); pack.cultures = JSON.parse(data[13]);
pack.states = JSON.parse(data[14]); pack.states = JSON.parse(data[14]);
@ -380,22 +379,20 @@ async function parseLoadedData(data, mapVersion) {
pack.markers = data[35] ? JSON.parse(data[35]) : []; pack.markers = data[35] ? JSON.parse(data[35]) : [];
pack.routes = data[37] ? JSON.parse(data[37]) : []; pack.routes = data[37] ? JSON.parse(data[37]) : [];
pack.zones = data[38] ? JSON.parse(data[38]) : []; pack.zones = data[38] ? JSON.parse(data[38]) : [];
pack.cells.biome = Uint8Array.from(data[16].split(","));
const cells = pack.cells; pack.cells.burg = Uint16Array.from(data[17].split(","));
cells.biome = Uint8Array.from(data[16].split(",")); pack.cells.conf = Uint8Array.from(data[18].split(","));
cells.burg = Uint16Array.from(data[17].split(",")); pack.cells.culture = Uint16Array.from(data[19].split(","));
cells.conf = Uint8Array.from(data[18].split(",")); pack.cells.fl = Uint16Array.from(data[20].split(","));
cells.culture = Uint16Array.from(data[19].split(",")); pack.cells.pop = Float32Array.from(data[21].split(","));
cells.fl = Uint16Array.from(data[20].split(",")); pack.cells.r = Uint16Array.from(data[22].split(","));
cells.pop = Float32Array.from(data[21].split(",")); // data[23] had deprecated cells.road
cells.r = Uint16Array.from(data[22].split(",")); pack.cells.s = Uint16Array.from(data[24].split(","));
// data[23] for deprecated cells.road pack.cells.state = Uint16Array.from(data[25].split(","));
cells.s = Uint16Array.from(data[24].split(",")); pack.cells.religion = data[26] ? Uint16Array.from(data[26].split(",")) : new Uint16Array(pack.cells.i.length);
cells.state = Uint16Array.from(data[25].split(",")); pack.cells.province = data[27] ? Uint16Array.from(data[27].split(",")) : new Uint16Array(pack.cells.i.length);
cells.religion = data[26] ? Uint16Array.from(data[26].split(",")) : new Uint16Array(cells.i.length); // data[28] had deprecated cells.crossroad
cells.province = data[27] ? Uint16Array.from(data[27].split(",")) : new Uint16Array(cells.i.length); pack.cells.routes = data[36] ? JSON.parse(data[36]) : {};
// data[28] for deprecated cells.crossroad
cells.routes = data[36] ? JSON.parse(data[36]) : {};
if (data[31]) { if (data[31]) {
const namesDL = data[31].split("/"); const namesDL = data[31].split("/");
@ -406,9 +403,9 @@ async function parseLoadedData(data, mapVersion) {
nameBases[i] = {name: e[0], min: e[1], max: e[2], d: e[3], m: e[4], b}; nameBases[i] = {name: e[0], min: e[1], max: e[2], d: e[3], m: e[4], b};
}); });
} }
})(); }
void (function restoreLayersState() { {
const isVisible = selection => selection.node() && selection.style("display") !== "none"; const isVisible = selection => selection.node() && selection.style("display") !== "none";
const isVisibleNode = node => node && node.style.display !== "none"; const isVisibleNode = node => node && node.style.display !== "none";
const hasChildren = selection => selection.node()?.hasChildNodes(); const hasChildren = selection => selection.node()?.hasChildNodes();
@ -422,7 +419,7 @@ async function parseLoadedData(data, mapVersion) {
// turn on active layers // turn on active layers
if (hasChild(texture, "image")) turnOn("toggleTexture"); 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(biomes)) turnOn("toggleBiomes");
if (hasChildren(cells)) turnOn("toggleCells"); if (hasChildren(cells)) turnOn("toggleCells");
if (hasChildren(gridOverlay)) turnOn("toggleGrid"); if (hasChildren(gridOverlay)) turnOn("toggleGrid");
@ -437,13 +434,13 @@ async function parseLoadedData(data, mapVersion) {
if (hasChildren(zones) && isVisible(zones)) turnOn("toggleZones"); if (hasChildren(zones) && isVisible(zones)) turnOn("toggleZones");
if (isVisible(borders) && hasChild(borders, "path")) turnOn("toggleBorders"); if (isVisible(borders) && hasChild(borders, "path")) turnOn("toggleBorders");
if (isVisible(routes) && hasChild(routes, "path")) turnOn("toggleRoutes"); 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 (hasChild(population, "line")) turnOn("togglePopulation");
if (hasChildren(ice)) turnOn("toggleIce"); 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(emblems) && hasChild(emblems, "use")) turnOn("toggleEmblems");
if (isVisible(labels)) turnOn("toggleLabels"); if (isVisible(labels)) turnOn("toggleLabels");
if (isVisible(icons)) turnOn("toggleIcons"); if (isVisible(icons)) turnOn("toggleBurgIcons");
if (hasChildren(armies) && isVisible(armies)) turnOn("toggleMilitary"); if (hasChildren(armies) && isVisible(armies)) turnOn("toggleMilitary");
if (hasChildren(markers)) turnOn("toggleMarkers"); if (hasChildren(markers)) turnOn("toggleMarkers");
if (isVisible(ruler)) turnOn("toggleRulers"); if (isVisible(ruler)) turnOn("toggleRulers");
@ -451,18 +448,18 @@ async function parseLoadedData(data, mapVersion) {
if (isVisibleNode(byId("vignette"))) turnOn("toggleVignette"); if (isVisibleNode(byId("vignette"))) turnOn("toggleVignette");
getCurrentPreset(); getCurrentPreset();
})(); }
void (function restoreEvents() { {
scaleBar.on("mousemove", () => tip("Click to open Units Editor")).on("click", () => editUnits()); scaleBar.on("mousemove", () => tip("Click to open Units Editor")).on("click", () => editUnits());
legend legend
.on("mousemove", () => tip("Drag to change the position. Click to hide the legend")) .on("mousemove", () => tip("Drag to change the position. Click to hide the legend"))
.on("click", () => clearLegend()); .on("click", () => clearLegend());
})(); }
{ {
// dynamically import and run auto-update script // 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.12");
resolveVersionConflicts(mapVersion); resolveVersionConflicts(mapVersion);
} }
@ -480,11 +477,11 @@ async function parseLoadedData(data, mapVersion) {
if (textureHref) updateTextureSelectValue(textureHref); if (textureHref) updateTextureSelectValue(textureHref);
} }
void (function checkDataIntegrity() { {
const cells = pack.cells; const cells = pack.cells;
if (pack.cells.i.length !== pack.cells.state.length) { if (pack.cells.i.length !== pack.cells.state.length) {
const message = "Data integrity check. Striping issue detected. To fix edit the heightmap in ERASE mode"; const message = "[Data integrity] Striping issue detected. To fix edit the heightmap in ERASE mode";
ERROR && console.error(message); ERROR && console.error(message);
} }
@ -492,7 +489,7 @@ async function parseLoadedData(data, mapVersion) {
invalidStates.forEach(s => { invalidStates.forEach(s => {
const invalidCells = cells.i.filter(i => cells.state[i] === s); const invalidCells = cells.i.filter(i => cells.state[i] === s);
invalidCells.forEach(i => (cells.state[i] = 0)); invalidCells.forEach(i => (cells.state[i] = 0));
ERROR && console.error("Data integrity check. Invalid state", s, "is assigned to cells", invalidCells); ERROR && console.error("[Data integrity] Invalid state", s, "is assigned to cells", invalidCells);
}); });
const invalidProvinces = [...new Set(cells.province)].filter( const invalidProvinces = [...new Set(cells.province)].filter(
@ -501,14 +498,14 @@ async function parseLoadedData(data, mapVersion) {
invalidProvinces.forEach(p => { invalidProvinces.forEach(p => {
const invalidCells = cells.i.filter(i => cells.province[i] === p); const invalidCells = cells.i.filter(i => cells.province[i] === p);
invalidCells.forEach(i => (cells.province[i] = 0)); invalidCells.forEach(i => (cells.province[i] = 0));
ERROR && console.error("Data integrity check. Invalid province", p, "is assigned to cells", invalidCells); ERROR && console.error("[Data integrity] Invalid province", p, "is assigned to cells", invalidCells);
}); });
const invalidCultures = [...new Set(cells.culture)].filter(c => !pack.cultures[c] || pack.cultures[c].removed); const invalidCultures = [...new Set(cells.culture)].filter(c => !pack.cultures[c] || pack.cultures[c].removed);
invalidCultures.forEach(c => { invalidCultures.forEach(c => {
const invalidCells = cells.i.filter(i => cells.culture[i] === c); const invalidCells = cells.i.filter(i => cells.culture[i] === c);
invalidCells.forEach(i => (cells.province[i] = 0)); invalidCells.forEach(i => (cells.province[i] = 0));
ERROR && console.error("Data integrity check. Invalid culture", c, "is assigned to cells", invalidCells); ERROR && console.error("[Data integrity] Invalid culture", c, "is assigned to cells", invalidCells);
}); });
const invalidReligions = [...new Set(cells.religion)].filter( const invalidReligions = [...new Set(cells.religion)].filter(
@ -517,14 +514,14 @@ async function parseLoadedData(data, mapVersion) {
invalidReligions.forEach(r => { invalidReligions.forEach(r => {
const invalidCells = cells.i.filter(i => cells.religion[i] === r); const invalidCells = cells.i.filter(i => cells.religion[i] === r);
invalidCells.forEach(i => (cells.religion[i] = 0)); invalidCells.forEach(i => (cells.religion[i] = 0));
ERROR && console.error("Data integrity check. Invalid religion", r, "is assigned to cells", invalidCells); ERROR && console.error("[Data integrity] Invalid religion", r, "is assigned to cells", invalidCells);
}); });
const invalidFeatures = [...new Set(cells.f)].filter(f => f && !pack.features[f]); const invalidFeatures = [...new Set(cells.f)].filter(f => f && !pack.features[f]);
invalidFeatures.forEach(f => { invalidFeatures.forEach(f => {
const invalidCells = cells.i.filter(i => cells.f[i] === f); const invalidCells = cells.i.filter(i => cells.f[i] === f);
// No fix as for now // No fix as for now
ERROR && console.error("Data integrity check. Invalid feature", f, "is assigned to cells", invalidCells); ERROR && console.error("[Data integrity] Invalid feature", f, "is assigned to cells", invalidCells);
}); });
const invalidBurgs = [...new Set(cells.burg)].filter( const invalidBurgs = [...new Set(cells.burg)].filter(
@ -533,7 +530,7 @@ async function parseLoadedData(data, mapVersion) {
invalidBurgs.forEach(burgId => { invalidBurgs.forEach(burgId => {
const invalidCells = cells.i.filter(i => cells.burg[i] === burgId); const invalidCells = cells.i.filter(i => cells.burg[i] === burgId);
invalidCells.forEach(i => (cells.burg[i] = 0)); invalidCells.forEach(i => (cells.burg[i] = 0));
ERROR && console.error("Data integrity check. Invalid burg", burgId, "is assigned to cells", invalidCells); ERROR && console.error("[Data integrity] Invalid burg", burgId, "is assigned to cells", invalidCells);
}); });
const invalidRivers = [...new Set(cells.r)].filter(r => r && !pack.rivers.find(river => river.i === r)); const invalidRivers = [...new Set(cells.r)].filter(r => r && !pack.rivers.find(river => river.i === r));
@ -541,21 +538,20 @@ async function parseLoadedData(data, mapVersion) {
const invalidCells = cells.i.filter(i => cells.r[i] === r); const invalidCells = cells.i.filter(i => cells.r[i] === r);
invalidCells.forEach(i => (cells.r[i] = 0)); invalidCells.forEach(i => (cells.r[i] = 0));
rivers.select("river" + r).remove(); rivers.select("river" + r).remove();
ERROR && console.error("Data integrity check. Invalid river", r, "is assigned to cells", invalidCells); ERROR && console.error("[Data integrity] Invalid river", r, "is assigned to cells", invalidCells);
}); });
pack.burgs.forEach(burg => { pack.burgs.forEach(burg => {
if (typeof burg.capital === "boolean") burg.capital = Number(burg.capital); if (typeof burg.capital === "boolean") burg.capital = Number(burg.capital);
if (!burg.i && burg.lock) { if (!burg.i && burg.lock) {
ERROR && console.error(`Data integrity check. Burg 0 is marked as locked, removing the status`); ERROR && console.error(`[Data integrity] Burg 0 is marked as locked, removing the status`);
delete burg.lock; delete burg.lock;
return; return;
} }
if (burg.removed && burg.lock) { if (burg.removed && burg.lock) {
ERROR && ERROR && console.error(`[Data integrity] Removed burg ${burg.i} is marked as locked. Unlocking the burg`);
console.error(`Data integrity check. Removed burg ${burg.i} is marked as locked. Unlocking the burg`);
delete burg.lock; delete burg.lock;
return; return;
} }
@ -564,36 +560,34 @@ async function parseLoadedData(data, mapVersion) {
if (burg.cell === undefined || burg.x === undefined || burg.y === undefined) { if (burg.cell === undefined || burg.x === undefined || burg.y === undefined) {
ERROR && ERROR &&
console.error( console.error(`[Data integrity] Burg ${burg.i} is missing cell info or coordinates. Removing the burg`);
`Data integrity check. Burg ${burg.i} is missing cell info or coordinates. Removing the burg`
);
burg.removed = true; burg.removed = true;
} }
if (burg.port < 0) { if (burg.port < 0) {
ERROR && console.error("Data integrity check. Burg", burg.i, "has invalid port value", burg.port); ERROR && console.error("[Data integrity] Burg", burg.i, "has invalid port value", burg.port);
burg.port = 0; burg.port = 0;
} }
if (burg.cell >= cells.i.length) { if (burg.cell >= cells.i.length) {
ERROR && console.error("Data integrity check. Burg", burg.i, "is linked to invalid cell", burg.cell); ERROR && console.error("[Data integrity] Burg", burg.i, "is linked to invalid cell", burg.cell);
burg.cell = findCell(burg.x, burg.y); burg.cell = findCell(burg.x, burg.y);
cells.i.filter(i => cells.burg[i] === burg.i).forEach(i => (cells.burg[i] = 0)); cells.i.filter(i => cells.burg[i] === burg.i).forEach(i => (cells.burg[i] = 0));
cells.burg[burg.cell] = burg.i; cells.burg[burg.cell] = burg.i;
} }
if (burg.state && !pack.states[burg.state]) { if (burg.state && !pack.states[burg.state]) {
ERROR && console.error("Data integrity check. Burg", burg.i, "is linked to invalid state", burg.state); ERROR && console.error("[Data integrity] Burg", burg.i, "is linked to invalid state", burg.state);
burg.state = 0; burg.state = 0;
} }
if (burg.state && pack.states[burg.state].removed) { if (burg.state && pack.states[burg.state].removed) {
ERROR && console.error("Data integrity check. Burg", burg.i, "is linked to removed state", burg.state); ERROR && console.error("[Data integrity] Burg", burg.i, "is linked to removed state", burg.state);
burg.state = 0; burg.state = 0;
} }
if (burg.state === undefined) { if (burg.state === undefined) {
ERROR && console.error("Data integrity check. Burg", burg.i, "has no state data"); ERROR && console.error("[Data integrity] Burg", burg.i, "has no state data");
burg.state = 0; burg.state = 0;
} }
}); });
@ -607,7 +601,7 @@ async function parseLoadedData(data, mapVersion) {
if (!state.i && capitalBurgs.length) { if (!state.i && capitalBurgs.length) {
ERROR && ERROR &&
console.error( console.error(
`Data integrity check. Neutral burgs (${capitalBurgs `[Data integrity] Neutral burgs (${capitalBurgs
.map(b => b.i) .map(b => b.i)
.join(", ")}) marked as capitals. Moving them to towns` .join(", ")}) marked as capitals. Moving them to towns`
); );
@ -621,7 +615,7 @@ async function parseLoadedData(data, mapVersion) {
} }
if (capitalBurgs.length > 1) { if (capitalBurgs.length > 1) {
const message = `Data integrity check. State ${state.i} has multiple capitals (${capitalBurgs const message = `[Data integrity] State ${state.i} has multiple capitals (${capitalBurgs
.map(b => b.i) .map(b => b.i)
.join(", ")}) assigned. Keeping the first as capital and moving others to towns`; .join(", ")}) assigned. Keeping the first as capital and moving others to towns`;
ERROR && console.error(message); ERROR && console.error(message);
@ -637,7 +631,7 @@ async function parseLoadedData(data, mapVersion) {
if (state.i && stateBurgs.length && !capitalBurgs.length) { if (state.i && stateBurgs.length && !capitalBurgs.length) {
ERROR && ERROR &&
console.error(`Data integrity check. State ${state.i} has no capital. Assigning the first burg as capital`); console.error(`[Data integrity] State ${state.i} has no capital. Assigning the first burg as capital`);
stateBurgs[0].capital = 1; stateBurgs[0].capital = 1;
moveBurgToGroup(stateBurgs[0].i, "cities"); moveBurgToGroup(stateBurgs[0].i, "cities");
} }
@ -646,28 +640,48 @@ async function parseLoadedData(data, mapVersion) {
pack.provinces.forEach(p => { pack.provinces.forEach(p => {
if (!p.i || p.removed) return; if (!p.i || p.removed) return;
if (pack.states[p.state] && !pack.states[p.state].removed) return; if (pack.states[p.state] && !pack.states[p.state].removed) return;
ERROR && console.error("Data integrity check. Province", p.i, "is linked to removed state", p.state); ERROR &&
p.removed = true; // remove incorrect province console.error(
`[Data integrity] Province ${p.i} is linked to removed state ${p.state}. Removing the province`
);
p.removed = true;
}); });
pack.routes.forEach(({i, points}) => { pack.routes.forEach(route => {
if (!points || points.length < 2) { if (!route.points || route.points.length < 2) {
ERROR && ERROR && console.error(`[Data integrity] Route ${route.i} has less than 2 points. Removing the route`);
console.error( Routes.remove(route);
"Data integrity check. Route",
i,
"has less than 2 points. Route will be ignored on layer rendering"
);
} }
}); });
for (const from in pack.cells.routes) {
const value = pack.cells.routes[from];
if (!value) continue;
if (Object.keys(value).length === 0) {
// remove empty object
delete pack.cells.routes[from];
continue;
}
for (const to in value) {
const routeId = value[to];
const route = pack.routes.find(r => r.i === routeId);
if (!route) {
ERROR &&
console.error(`[Data integrity] Route ${routeId} from ${from} to ${to} is missing. Removing the route`);
delete pack.cells.routes[from][to];
}
}
}
{ {
const markerIds = []; const markerIds = [];
let nextId = last(pack.markers)?.i + 1 || 0; let nextId = last(pack.markers)?.i + 1 || 0;
pack.markers.forEach(marker => { pack.markers.forEach(marker => {
if (markerIds[marker.i]) { if (markerIds[marker.i]) {
ERROR && console.error("Data integrity check. Marker", marker.i, "has non-unique id. Changing to", nextId); ERROR && console.error("[Data integrity] Marker", marker.i, "has non-unique id. Changing to", nextId);
const domElements = document.querySelectorAll("#marker" + marker.i); const domElements = document.querySelectorAll("#marker" + marker.i);
if (domElements[1]) domElements[1].id = "marker" + nextId; // rename 2nd dom element if (domElements[1]) domElements[1].id = "marker" + nextId; // rename 2nd dom element
@ -685,20 +699,25 @@ async function parseLoadedData(data, mapVersion) {
// sort markers by index // sort markers by index
pack.markers.sort((a, b) => a.i - b.i); pack.markers.sort((a, b) => a.i - b.i);
} }
})(); }
fitMapToScreen(); {
// remove href from emblems, to trigger rendering on load
emblems.selectAll("use").attr("href", null);
}
// remove href from emblems, to trigger rendering on load {
emblems.selectAll("use").attr("href", null); // draw data layers (not kept in svg)
if (rulers && layerIsOn("toggleRulers")) rulers.draw();
if (layerIsOn("toggleGrid")) drawGrid();
}
// draw data layers (no kept in svg) {
if (rulers && layerIsOn("toggleRulers")) rulers.draw(); if (window.restoreDefaultEvents) restoreDefaultEvents();
if (layerIsOn("toggleGrid")) drawGrid(); focusOn(); // based on searchParams focus on point, cell or burg
invokeActiveZooming();
if (window.restoreDefaultEvents) restoreDefaultEvents(); fitMapToScreen();
focusOn(); // based on searchParams focus on point, cell or burg }
invokeActiveZooming();
WARN && console.warn(`TOTAL: ${rn((performance.now() - uploadMap.timeStart) / 1000, 2)}s`); WARN && console.warn(`TOTAL: ${rn((performance.now() - uploadMap.timeStart) / 1000, 2)}s`);
showStatistics(); showStatistics();

View file

@ -1,98 +1,87 @@
"use strict"; "use strict";
window.Lakes = (function () { window.Lakes = (function () {
const setClimateData = function (h) { const LAKE_ELEVATION_DELTA = 0.1;
const cells = pack.cells;
const lakeOutCells = new Uint16Array(cells.i.length);
pack.features.forEach(f => { // check if lake can be potentially open (not in deep depression)
if (f.type !== "lake") return; const detectCloseLakes = h => {
const {cells} = pack;
const ELEVATION_LIMIT = +byId("lakeElevationLimitOutput").value;
// default flux: sum of precipitation around lake pack.features.forEach(feature => {
f.flux = f.shoreline.reduce((acc, c) => acc + grid.cells.prec[cells.g[c]], 0); if (feature.type !== "lake") return;
delete feature.closed;
// temperature and evaporation to detect closed lakes const MAX_ELEVATION = feature.height + ELEVATION_LIMIT;
f.temp = if (MAX_ELEVATION > 99) {
f.cells < 6 feature.closed = false;
? 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;
return; return;
} }
let deep = true; let isDeep = true;
const threshold = f.height + ELEVATION_LIMIT; const lowestShorelineCell = feature.shoreline.sort((a, b) => h[a] - h[b])[0];
const queue = [min]; const queue = [lowestShorelineCell];
const checked = []; const checked = [];
checked[min] = true; checked[lowestShorelineCell] = true;
// check if elevated lake can potentially pour to another water body while (queue.length && isDeep) {
while (deep && queue.length) { const cellId = queue.pop();
const q = queue.pop();
for (const n of cells.c[q]) { for (const neibCellId of cells.c[cellId]) {
if (checked[n]) continue; if (checked[neibCellId]) continue;
if (h[n] >= threshold) continue; if (h[neibCellId] >= MAX_ELEVATION) continue;
if (h[n] < 20) { if (h[neibCellId] < 20) {
const nFeature = pack.features[cells.f[n]]; const nFeature = pack.features[cells.f[neibCellId]];
if (nFeature.type === "ocean" || f.height > nFeature.height) { if (nFeature.type === "ocean" || feature.height > nFeature.height) isDeep = false;
deep = false;
break;
}
} }
checked[n] = true; checked[neibCellId] = true;
queue.push(n); 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 () { const cleanupLakeData = function () {
for (const feature of pack.features) { for (const feature of pack.features) {
if (feature.type !== "lake") continue; if (feature.type !== "lake") continue;
@ -111,23 +100,10 @@ window.Lakes = (function () {
} }
}; };
const defineGroup = function () { const getHeight = function (feature) {
for (const feature of pack.features) { const heights = pack.cells.h;
if (feature.type !== "lake") continue; const minShoreHeight = d3.min(feature.shoreline.map(cellId => heights[cellId])) || 20;
const lakeEl = lakes.select(`[data-f="${feature.i}"]`).node(); return rn(minShoreHeight - LAKE_ELEVATION_DELTA, 2);
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 getName = function (feature) { const getName = function (feature) {
@ -136,19 +112,5 @@ window.Lakes = (function () {
return Names.getCulture(culture); return Names.getCulture(culture);
}; };
function getGroup(feature) { return {defineClimateData, cleanupLakeData, detectCloseLakes, getHeight, getName};
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};
})(); })();

View file

@ -117,6 +117,7 @@ window.Markers = (function () {
while (quantity && candidates.length) { while (quantity && candidates.length) {
const [cell] = extractAnyElement(candidates); const [cell] = extractAnyElement(candidates);
const marker = addMarker({icon, type, dx, dy, px}, {cell}); const marker = addMarker({icon, type, dx, dy, px}, {cell});
if (!marker) continue;
add("marker" + marker.i, cell); add("marker" + marker.i, cell);
quantity--; quantity--;
} }
@ -150,6 +151,7 @@ window.Markers = (function () {
} }
function addMarker(base, marker) { function addMarker(base, marker) {
if (marker.cell === undefined) return;
const i = last(pack.markers)?.i + 1 || 0; const i = last(pack.markers)?.i + 1 || 0;
const [x, y] = getMarkerCoordinates(marker.cell); const [x, y] = getMarkerCoordinates(marker.cell);
marker = {...base, x, y, ...marker, i}; marker = {...base, x, y, ...marker, i};

View file

@ -2,7 +2,7 @@
window.Military = (function () { window.Military = (function () {
const generate = function () { const generate = function () {
TIME && console.time("generateMilitaryForces"); TIME && console.time("generateMilitary");
const {cells, states} = pack; const {cells, states} = pack;
const {p} = cells; const {p} = cells;
const valid = states.filter(s => s.i && !s.removed); // valid states 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 delete s.temp; // do not store temp data
}); });
redraw();
function createRegiments(nodes, s) { function createRegiments(nodes, s) {
if (!nodes.length) return []; if (!nodes.length) return [];
@ -312,19 +310,9 @@ window.Military = (function () {
return regiments; 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 () { const getDefaultOptions = function () {
return [ return [
{icon: "⚔️", name: "infantry", rural: 0.25, urban: 0.2, crew: 1, power: 1, type: "melee", separate: 0}, {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 // 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); const getTotal = reg => (reg.a > (reg.n ? 999 : 99999) ? si(reg.a) : reg.a);
@ -513,13 +385,9 @@ window.Military = (function () {
return { return {
generate, generate,
redraw,
getDefaultOptions, getDefaultOptions,
getName, getName,
generateNote, generateNote,
drawRegiments,
drawRegiment,
moveRegiment,
getTotal, getTotal,
getEmblem 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(Military.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 // list - an optional array of stateIds to regenerate
function drawStateLabels(list) { function drawStateLabels(list) {
console.time("drawStateLabels"); TIME && console.time("drawStateLabels");
// temporary make the labels visible // temporary make the labels visible
const layerDisplay = labels.style("display"); const layerDisplay = labels.style("display");
@ -289,5 +289,5 @@ function drawStateLabels(list) {
return false; 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 riversData = {}; // rivers data
const riverParents = {}; const riverParents = {};
const addCellToRiver = function (cell, river) { const addCellToRiver = function (cell, river) {
if (!riversData[river]) riversData[river] = [cell]; if (!riversData[river]) riversData[river] = [cell];
else riversData[river].push(cell); else riversData[river].push(cell);
@ -19,7 +20,7 @@ window.Rivers = (function () {
let riverNext = 1; // first river id is 1 let riverNext = 1; // first river id is 1
const h = alterHeights(); const h = alterHeights();
Lakes.prepareLakeData(h); Lakes.detectCloseLakes(h);
resolveDepressions(h); resolveDepressions(h);
drainWater(); drainWater();
defineRivers(); defineRivers();
@ -39,9 +40,8 @@ window.Rivers = (function () {
const cellsNumberModifier = (pointsInput.dataset.cells / 10000) ** 0.25; const cellsNumberModifier = (pointsInput.dataset.cells / 10000) ** 0.25;
const prec = grid.cells.prec; 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 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) { land.forEach(function (i) {
cells.fl[i] += prec[cells.g[i]] / cellsNumberModifier; // add flux from precipitation cells.fl[i] += prec[cells.g[i]] / cellsNumberModifier; // add flux from precipitation

View file

@ -496,7 +496,7 @@ window.Routes = (function () {
// utility functions // utility functions
function isConnected(cellId) { function isConnected(cellId) {
const {routes} = pack.cells; const routes = pack.cells.routes;
return routes[cellId] && Object.keys(routes[cellId]).length > 0; return routes[cellId] && Object.keys(routes[cellId]).length > 0;
} }
@ -507,22 +507,34 @@ window.Routes = (function () {
function getRoute(from, to) { function getRoute(from, to) {
const routeId = pack.cells.routes[from]?.[to]; const routeId = pack.cells.routes[from]?.[to];
return routeId === undefined ? null : pack.routes[routeId]; if (routeId === undefined) return null;
const route = pack.routes.find(route => route.i === routeId);
if (!route) return null;
return route;
} }
function hasRoad(cellId) { function hasRoad(cellId) {
const connections = pack.cells.routes[cellId]; const connections = pack.cells.routes[cellId];
if (!connections) return false; if (!connections) return false;
return Object.values(connections).some(routeId => pack.routes[routeId].group === "roads");
return Object.values(connections).some(routeId => {
const route = pack.routes.find(route => route.i === routeId);
if (!route) return false;
return route.group === "roads";
});
} }
function isCrossroad(cellId) { function isCrossroad(cellId) {
const connections = pack.cells.routes[cellId]; const connections = pack.cells.routes[cellId];
if (!connections) return false; if (!connections) return false;
return ( if (Object.keys(connections).length > 3) return true;
Object.keys(connections).length > 3 || const roadConnections = Object.values(connections).filter(routeId => {
Object.values(connections).filter(routeId => pack.routes[routeId]?.group === "roads").length > 2 const route = pack.routes.find(route => route.i === routeId);
); return route?.group === "roads";
});
return roadConnections.length > 2;
} }
// name generator data // name generator data
@ -715,6 +727,7 @@ window.Routes = (function () {
for (const point of route.points) { for (const point of route.points) {
const from = point[2]; const from = point[2];
if (!routes[from]) continue;
for (const [to, routeId] of Object.entries(routes[from])) { for (const [to, routeId] of Object.entries(routes[from])) {
if (routeId === route.i) { if (routeId === route.i) {
@ -725,10 +738,7 @@ window.Routes = (function () {
} }
pack.routes = pack.routes.filter(r => r.i !== route.i); pack.routes = pack.routes.filter(r => r.i !== route.i);
viewbox viewbox.select("#route" + route.i).remove();
.select("#routes")
.select("#route" + route.i)
.remove();
} }
return { return {

View file

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

View file

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

View file

@ -277,7 +277,7 @@ class Battle {
const shift = side === "attackers" ? attackers.length * -8 : (defenders.length - 1) * 8; const shift = side === "attackers" ? attackers.length * -8 : (defenders.length - 1) * 8;
regiment.px = regiment.x; regiment.px = regiment.x;
regiment.py = regiment.y; 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() { cancelResults() {
// move regiments back to initial positions // 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"); $("#battleScreen").dialog("close");
this.cleanData(); this.cleanData();
} }

View file

@ -2,7 +2,7 @@
function editBurg(id) { function editBurg(id) {
if (customization) return; if (customization) return;
closeDialogs(".stable"); closeDialogs(".stable");
if (!layerIsOn("toggleIcons")) toggleIcons(); if (!layerIsOn("toggleBurgIcons")) toggleBurgIcons();
if (!layerIsOn("toggleLabels")) toggleLabels(); if (!layerIsOn("toggleLabels")) toggleLabels();
const burg = id || d3.event.target.dataset.id; const burg = id || d3.event.target.dataset.id;
@ -47,6 +47,7 @@ function editBurg(id) {
byId("burgEmblem").addEventListener("click", openEmblemEdit); byId("burgEmblem").addEventListener("click", openEmblemEdit);
byId("burgTogglePreview").addEventListener("click", toggleBurgPreview); byId("burgTogglePreview").addEventListener("click", toggleBurgPreview);
byId("burgEditEmblem").addEventListener("click", openEmblemEdit); byId("burgEditEmblem").addEventListener("click", openEmblemEdit);
byId("burgLocate").addEventListener("click", zoomIntoBurg);
byId("burgRelocate").addEventListener("click", toggleRelocateBurg); byId("burgRelocate").addEventListener("click", toggleRelocateBurg);
byId("burglLegend").addEventListener("click", editBurgLegend); byId("burglLegend").addEventListener("click", editBurgLegend);
byId("burgLock").addEventListener("click", toggleBurgLockButton); byId("burgLock").addEventListener("click", toggleBurgLockButton);
@ -397,6 +398,14 @@ function editBurg(id) {
byId("burgTogglePreview").className = options.showBurgPreview ? "icon-map" : "icon-map-o"; byId("burgTogglePreview").className = options.showBurgPreview ? "icon-map" : "icon-map-o";
} }
function zoomIntoBurg() {
const id = +elSelected.attr("data-id");
const burg = pack.burgs[id];
const x = burg.x;
const y = burg.y;
zoomTo(x, y, 8, 2000);
}
function toggleRelocateBurg() { function toggleRelocateBurg() {
const toggler = byId("toggleCells"); const toggler = byId("toggleCells");
byId("burgRelocate").classList.toggle("pressed"); byId("burgRelocate").classList.toggle("pressed");

View file

@ -2,7 +2,7 @@
function overviewBurgs(settings = {stateId: null, cultureId: null}) { function overviewBurgs(settings = {stateId: null, cultureId: null}) {
if (customization) return; if (customization) return;
closeDialogs("#burgsOverview, .stable"); closeDialogs("#burgsOverview, .stable");
if (!layerIsOn("toggleIcons")) toggleIcons(); if (!layerIsOn("toggleBurgIcons")) toggleBurgIcons();
if (!layerIsOn("toggleLabels")) toggleLabels(); if (!layerIsOn("toggleLabels")) toggleLabels();
const body = byId("burgsBody"); const body = byId("burgsBody");
@ -154,9 +154,9 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) {
} }
function burgHighlightOn(event) { function burgHighlightOn(event) {
if (!layerIsOn("toggleLabels")) toggleLabels();
const burg = +event.target.dataset.id; 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() { function burgHighlightOff() {

View file

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

View file

@ -10,31 +10,27 @@ function restoreDefaultEvents() {
legend.call(d3.drag().on("start", dragLegendBox)); legend.call(d3.drag().on("start", dragLegendBox));
} }
// on viewbox click event - run function based on target // handle viewbox click
function clicked() { function clicked() {
const el = d3.event.target; const el = d3.event.target;
if (!el || !el.parentElement || !el.parentElement.parentElement) return; const parent = el?.parentElement;
const parent = el.parentElement; const grand = parent?.parentElement;
const grand = parent.parentElement; const great = grand?.parentElement;
const great = grand.parentElement; const ancestor = great?.parentElement;
const p = d3.mouse(this); if (!ancestor) return;
const i = findCell(p[0], p[1]);
if (grand.id === "emblems") editEmblem(); if (grand.id === "emblems") editEmblem();
else if (parent.id === "rivers") editRiver(el.id); else if (parent.id === "rivers") editRiver(el.id);
else if (grand.id === "routes") editRoute(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 === "burgLabels") editBurg();
else if (grand.id === "burgIcons") editBurg(); else if (grand.id === "burgIcons") editBurg();
else if (parent.id === "ice") editIce(); else if (parent.id === "ice") editIce();
else if (parent.id === "terrain") editReliefIcon(); else if (parent.id === "terrain") editReliefIcon();
else if (grand.id === "markers" || great.id === "markers") editMarker(); else if (grand.id === "markers" || great.id === "markers") editMarker();
else if (grand.id === "coastline") editCoastline(); else if (grand.id === "coastline") editCoastline();
else if (grand.id === "lakes") editLake();
else if (great.id === "armies") editRegiment(); 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 // clear elSelected variable
@ -397,12 +393,12 @@ function createVillageGeneratorLink(burg) {
else if (cells.r[cell]) tags.push("river"); else if (cells.r[cell]) tags.push("river");
else if (pop < 200 && each(4)(cell)) tags.push("pond"); else if (pop < 200 && each(4)(cell)) tags.push("pond");
const connections = pack.cells.routes[cell] || {}; const roadsNumber = Object.values(pack.cells.routes[cell] || {}).filter(routeId => {
const roads = Object.values(connections).filter(routeId => { const route = pack.routes.find(route => route.i === routeId);
const route = pack.routes[routeId]; if (!route) return false;
return route.group === "roads" || route.group === "trails"; return route.group === "roads" || route.group === "trails";
}).length; }).length;
tags.push(roads > 1 ? "highway" : roads === 1 ? "dead end" : "isolated"); tags.push(roadsNumber > 1 ? "highway" : roadsNumber === 1 ? "dead end" : "isolated");
const biome = cells.biome[cell]; const biome = cells.biome[cell];
const arableBiomes = cells.r[cell] ? [1, 2, 3, 4, 5, 6, 7, 8] : [5, 6, 7, 8]; const arableBiomes = cells.r[cell] ? [1, 2, 3, 4, 5, 6, 7, 8] : [5, 6, 7, 8];
@ -1252,18 +1248,18 @@ function refreshAllEditors() {
// dynamically loaded editors // dynamically loaded editors
async function editStates() { async function editStates() {
if (customization) return; 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(); Editor.open();
} }
async function editCultures() { async function editCultures() {
if (customization) return; 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(); Editor.open();
} }
async function editReligions() { async function editReligions() {
if (customization) return; 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(); Editor.open();
} }

View file

@ -153,20 +153,24 @@ function showMapTooltip(point, e, i, g) {
if (group === "routes") { if (group === "routes") {
const routeId = +e.target.id.slice(5); const routeId = +e.target.id.slice(5);
const name = pack.routes[routeId]?.name; const route = pack.routes.find(route => route.i === routeId);
if (name) return tip(`${name}. Click to edit the Route`); if (route) {
return tip("Click to edit the Route"); if (route.name) return tip(`${route.name}. Click to edit the Route`);
return tip("Click to edit the Route");
}
} }
if (group === "terrain") return tip("Click to edit the Relief Icon"); if (group === "terrain") return tip("Click to edit the Relief Icon");
if (subgroup === "burgLabels" || subgroup === "burgIcons") { if (subgroup === "burgLabels" || subgroup === "burgIcons") {
const burg = +path[path.length - 10].dataset.id; const burgId = +path[path.length - 10].dataset.id;
const b = pack.burgs[burg]; if (burgId) {
const population = si(b.population * populationRate * urbanization); const burg = pack.burgs[burgId];
tip(`${b.name}. Population: ${population}. Click to edit`); const population = si(burg.population * populationRate * urbanization);
if (burgsOverview?.offsetParent) highlightEditorLine(burgsOverview, burg, 5000); tip(`${burg.name}. Population: ${population}. Click to edit`);
return; if (burgsOverview?.offsetParent) highlightEditorLine(burgsOverview, burgId, 5000);
return;
}
} }
if (group === "labels") return tip("Click to edit the Label"); if (group === "labels") return tip("Click to edit the Label");
@ -211,9 +215,9 @@ function showMapTooltip(point, e, i, g) {
if (group === "ice") return tip("Click to edit the Ice"); if (group === "ice") return tip("Click to edit the Ice");
// covering elements // 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("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]) { else if (layerIsOn("toggleBiomes") && pack.cells.biome[i]) {
const biome = pack.cells.biome[i]; const biome = pack.cells.biome[i];
tip("Biome: " + biomesData.name[biome]); tip("Biome: " + biomesData.name[biome]);
@ -259,10 +263,11 @@ function updateCellInfo(point, i, g) {
const f = cells.f[i]; const f = cells.f[i];
infoLat.innerHTML = toDMS(getLatitude(y, 4), "lat"); infoLat.innerHTML = toDMS(getLatitude(y, 4), "lat");
infoLon.innerHTML = toDMS(getLongitude(x, 4), "lon"); infoLon.innerHTML = toDMS(getLongitude(x, 4), "lon");
infoGeozone.innerHTML = getGeozone(getLatitude(y, 4));
infoCell.innerHTML = i; infoCell.innerHTML = i;
infoArea.innerHTML = cells.area[i] ? si(getArea(cells.area[i])) + " " + getAreaUnit() : "n/a"; infoArea.innerHTML = cells.area[i] ? si(getArea(cells.area[i])) + " " + getAreaUnit() : "n/a";
infoEvelation.innerHTML = getElevation(pack.features[f], pack.cells.h[i]); infoElevation.innerHTML = getElevation(pack.features[f], pack.cells.h[i]);
infoDepth.innerHTML = getDepth(pack.features[f], point); infoDepth.innerHTML = getDepth(pack.features[f], point);
infoTemp.innerHTML = convertTemperature(grid.cells.temp[g]); infoTemp.innerHTML = convertTemperature(grid.cells.temp[g]);
infoPrec.innerHTML = cells.h[i] >= 20 ? getFriendlyPrecipitation(i) : "n/a"; infoPrec.innerHTML = cells.h[i] >= 20 ? getFriendlyPrecipitation(i) : "n/a";
@ -286,6 +291,18 @@ function updateCellInfo(point, i, g) {
infoBiome.innerHTML = biomesData.name[cells.biome[i]]; infoBiome.innerHTML = biomesData.name[cells.biome[i]];
} }
function getGeozone(latitude) {
if (latitude > 66.5) return "Arctic";
if (latitude > 35) return "Temperate North";
if (latitude > 23.5) return "Subtropical North";
if (latitude > 1) return "Tropical North";
if (latitude > -1) return "Equatorial";
if (latitude > -23.5) return "Tropical South";
if (latitude > -35) return "Subtropical South";
if (latitude > -66.5) return "Temperate South";
return "Antarctic";
}
// convert coordinate to DMS format // convert coordinate to DMS format
function toDMS(coord, c) { function toDMS(coord, c) {
const degrees = Math.floor(Math.abs(coord)); const degrees = Math.floor(Math.abs(coord));
@ -429,17 +446,17 @@ function highlightEmblemElement(type, el) {
// assign lock behavior // assign lock behavior
document.querySelectorAll("[data-locked]").forEach(function (e) { document.querySelectorAll("[data-locked]").forEach(function (e) {
e.addEventListener("mouseover", function (event) { e.addEventListener("mouseover", function (e) {
e.stopPropagation();
if (this.className === "icon-lock") if (this.className === "icon-lock")
tip("Click to unlock the option and allow it to be randomized on new map generation"); tip("Click to unlock the option and allow it to be randomized on new map generation");
else tip("Click to lock the option and always use the current value on new map generation"); else tip("Click to lock the option and always use the current value on new map generation");
event.stopPropagation();
}); });
e.addEventListener("click", function () { e.addEventListener("click", function () {
const id = this.id.slice(5); const ids = this.dataset.ids ? this.dataset.ids.split(",") : [this.id.slice(5)];
if (this.className === "icon-lock") unlock(id); const fn = this.className === "icon-lock" ? unlock : lock;
else lock(id); ids.forEach(fn);
}); });
}); });

View file

@ -75,7 +75,8 @@ function editHeightmap(options) {
changeOnlyLand.checked = true; changeOnlyLand.checked = true;
} else if (mode === "risk") { } else if (mode === "risk") {
defs.selectAll("#land, #water").selectAll("path").remove(); defs.selectAll("#land, #water").selectAll("path").remove();
viewbox.selectAll("#coastline path, #lakes path, #oceanLayers path").remove(); defs.select("#featurePaths").selectAll("path").remove();
viewbox.selectAll("#coastline use, #lakes path, #oceanLayers path").remove();
changeOnlyLand.checked = false; changeOnlyLand.checked = false;
} }
@ -157,11 +158,7 @@ function editHeightmap(options) {
// Exit customization mode // Exit customization mode
function finalizeHeightmap() { function finalizeHeightmap() {
if (viewbox.select("#heights").selectAll("*").size() < 200) if (viewbox.select("#heights").selectAll("*").size() < 200)
return tip( return tip("Insufficient land area. There should be at least 200 land cells!", null, "error");
"Insufficient land area! There should be at least 200 land cells to finalize the heightmap",
null,
"error"
);
if (byId("imageConverter").offsetParent) return tip("Please exit the Image Conversion mode first", null, "error"); if (byId("imageConverter").offsetParent) return tip("Please exit the Image Conversion mode first", null, "error");
delete window.edits; // remove global variable delete window.edits; // remove global variable
@ -173,6 +170,7 @@ function editHeightmap(options) {
if (byId("options").querySelector(".tab > button.active").id === "toolsTab") toolsContent.style.display = "block"; if (byId("options").querySelector(".tab > button.active").id === "toolsTab") toolsContent.style.display = "block";
layersPreset.disabled = false; layersPreset.disabled = false;
exitCustomization.style.display = "none"; // hide finalize button exitCustomization.style.display = "none"; // hide finalize button
restoreDefaultEvents(); restoreDefaultEvents();
clearMainTip(); clearMainTip();
closeDialogs(); closeDialogs();
@ -187,6 +185,7 @@ function editHeightmap(options) {
else if (mode === "risk") restoreRiskedData(); else if (mode === "risk") restoreRiskedData();
// restore initial layers // restore initial layers
drawFeatures();
byId("heights").remove(); byId("heights").remove();
turnButtonOff("toggleHeight"); turnButtonOff("toggleHeight");
document document
@ -215,8 +214,7 @@ function editHeightmap(options) {
pack.religions = []; pack.religions = [];
const erosionAllowed = allowErosion.checked; const erosionAllowed = allowErosion.checked;
markFeatures(); Features.markupGrid();
markupGridOcean();
if (erosionAllowed) { if (erosionAllowed) {
addLakesInDeepDepressions(); addLakesInDeepDepressions();
openNearSeaLakes(); openNearSeaLakes();
@ -225,7 +223,7 @@ function editHeightmap(options) {
calculateTemperatures(); calculateTemperatures();
generatePrecipitation(); generatePrecipitation();
reGraph(); reGraph();
drawCoastline(); Features.markupPack();
Rivers.generate(erosionAllowed); Rivers.generate(erosionAllowed);
@ -237,8 +235,6 @@ function editHeightmap(options) {
} }
} }
drawRivers();
Lakes.defineGroup();
Biomes.define(); Biomes.define();
rankCells(); rankCells();
@ -249,15 +245,12 @@ function editHeightmap(options) {
Routes.generate(); Routes.generate();
Religions.generate(); Religions.generate();
BurgsAndStates.defineStateForms(); BurgsAndStates.defineStateForms();
BurgsAndStates.generateProvinces(); Provinces.generate();
Provinces.getPoles();
BurgsAndStates.defineBurgFeatures(); BurgsAndStates.defineBurgFeatures();
drawStates();
drawBorders();
drawStateLabels();
Rivers.specify(); Rivers.specify();
Lakes.generateName(); Features.specify();
Military.generate(); Military.generate();
Markers.generate(); Markers.generate();
@ -338,14 +331,13 @@ function editHeightmap(options) {
zone.selectAll("*").remove(); zone.selectAll("*").remove();
}); });
markFeatures(); Features.markupGrid();
markupGridOcean();
if (erosionAllowed) addLakesInDeepDepressions(); if (erosionAllowed) addLakesInDeepDepressions();
OceanLayers(); OceanLayers();
calculateTemperatures(); calculateTemperatures();
generatePrecipitation(); generatePrecipitation();
reGraph(); reGraph();
drawCoastline(); Features.markupPack();
if (erosionAllowed) Rivers.generate(true); if (erosionAllowed) Rivers.generate(true);
@ -439,13 +431,9 @@ function editHeightmap(options) {
c.center = findCell(c.x, c.y); c.center = findCell(c.x, c.y);
} }
drawStateLabels();
drawStates();
drawBorders();
if (erosionAllowed) { if (erosionAllowed) {
Rivers.specify(); Rivers.specify();
Lakes.generateName(); Features.specify();
} }
// restore zones from grid // restore zones from grid
@ -489,10 +477,14 @@ function editHeightmap(options) {
updateHistory(); updateHistory();
} }
function getColor(value, scheme = getColorScheme()) {
return scheme(1 - (value < 20 ? value - 5 : value) / 100);
}
// draw or update heightmap // draw or update heightmap
function mockHeightmap() { function mockHeightmap() {
const data = renderOcean.checked ? grid.cells.i : grid.cells.i.filter(i => grid.cells.h[i] >= 20); const data = renderOcean.checked ? grid.cells.i : grid.cells.i.filter(i => grid.cells.h[i] >= 20);
const scheme = getColorScheme();
viewbox viewbox
.select("#heights") .select("#heights")
.selectAll("polygon") .selectAll("polygon")
@ -500,13 +492,12 @@ function editHeightmap(options) {
.join("polygon") .join("polygon")
.attr("points", d => getGridPolygon(d)) .attr("points", d => getGridPolygon(d))
.attr("id", d => "cell" + 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 // draw or update heightmap for a selection of cells
function mockHeightmapSelection(selection) { function mockHeightmapSelection(selection) {
const ocean = renderOcean.checked; const ocean = renderOcean.checked;
const scheme = getColorScheme();
selection.forEach(function (i) { selection.forEach(function (i) {
let cell = viewbox.select("#heights").select("#cell" + i); let cell = viewbox.select("#heights").select("#cell" + i);
@ -518,7 +509,7 @@ function editHeightmap(options) {
.append("polygon") .append("polygon")
.attr("points", getGridPolygon(i)) .attr("points", getGridPolygon(i))
.attr("id", "cell" + i); .attr("id", "cell" + i);
cell.attr("fill", getColor(grid.cells.h[i], scheme)); cell.attr("fill", getColor(grid.cells.h[i]));
}); });
} }
@ -1349,7 +1340,7 @@ function editHeightmap(options) {
return lum | 0; // land 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 hues = scheme.map(rgb => d3.hsl(rgb).h | 0);
const getHeightByScheme = function (color) { const getHeightByScheme = function (color) {
let height = scheme.indexOf(color); let height = scheme.indexOf(color);

View file

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

View file

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

@ -251,8 +251,7 @@ const voiceInterval = setInterval(function () {
select.options.add(new Option(voice.name, i, false)); select.options.add(new Option(voice.name, i, false));
}); });
if (stored("speakerVoice")) select.value = stored("speakerVoice"); if (stored("speakerVoice")) select.value = stored("speakerVoice");
// se voice to store else select.value = voices.findIndex(voice => voice.lang === "en-US");
else select.value = voices.findIndex(voice => voice.lang === "en-US"); // or to first found English-US
}, 1000); }, 1000);
function testSpeaker() { function testSpeaker() {
@ -704,12 +703,6 @@ async function openTemplateSelectionDialog() {
HeightmapSelectionDialog.open(); HeightmapSelectionDialog.open();
} }
// remove all saved data from LocalStorage and reload the page
function restoreDefaultOptions() {
localStorage.clear();
location.reload();
}
// Sticked menu Options listeners // Sticked menu Options listeners
byId("sticked").addEventListener("click", function (event) { byId("sticked").addEventListener("click", function (event) {
const id = event.target.id; const id = event.target.id;

View file

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

View file

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

View file

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

View file

@ -18,7 +18,7 @@ function editRouteGroups() {
// add listeners // add listeners
byId("routeGroupsEditorAdd").addEventListener("click", addGroup); byId("routeGroupsEditorAdd").addEventListener("click", addGroup);
byId("routeGroupsEditorBody").on("click", ev => { byId("routeGroupsEditorBody").on("click", ev => {
const group = ev.target.parentNode.dataset.id; const group = ev.target.closest(".states")?.dataset.id;
if (ev.target.classList.contains("editStyle")) editStyle("routes", group); if (ev.target.classList.contains("editStyle")) editStyle("routes", group);
else if (ev.target.classList.contains("removeGroup")) removeGroup(group); else if (ev.target.classList.contains("removeGroup")) removeGroup(group);
}); });
@ -72,12 +72,11 @@ function editRouteGroups() {
confirmationDialog({ confirmationDialog({
title: "Remove route group", title: "Remove route group",
message: message:
"Are you sure you want to remove the entire route group? All routes in this group will be removed. This action can't be reverted.", "Are you sure you want to remove the entire route group? All routes in this group will be removed.<br>This action can't be reverted",
confirm: "Remove", confirm: "Remove",
onConfirm: () => { onConfirm: () => {
const routes = pack.routes.filter(r => r.group === group); pack.routes.filter(r => r.group === group).forEach(Routes.remove);
routes.forEach(r => Routes.remove(r)); if (!DEFAULT_GROUPS.includes(group)) routes.select(`#${group}`).remove();
if (DEFAULT_GROUPS.includes(group)) routes.select(`#${group}`).remove();
addLines(); addLines();
} }
}); });

View file

@ -174,9 +174,10 @@ function editRoute(id) {
function handleControlPointClick() { function handleControlPointClick() {
const controlPoint = d3.select(this); const controlPoint = d3.select(this);
const point = controlPoint.datum(); const point = controlPoint.datum();
const route = getRoute(); const route = getRoute();
if (route.points.length < 3) return; // can't remove or split point if only 2 points in route
const index = route.points.indexOf(point); const index = route.points.indexOf(point);
const isSplitMode = byId("routeSplit").classList.contains("pressed"); const isSplitMode = byId("routeSplit").classList.contains("pressed");

View file

@ -32,6 +32,7 @@ function overviewRoutes() {
let lines = ""; let lines = "";
for (const route of pack.routes) { for (const route of pack.routes) {
if (!route.points || route.points.length < 2) continue;
route.name = route.name || Routes.generateName(route); route.name = route.name || Routes.generateName(route);
route.length = route.length || Routes.getLength(route.i); route.length = route.length || Routes.getLength(route.i);
const length = rn(route.length * distanceScale) + " " + distanceUnitInput.value; const length = rn(route.length * distanceScale) + " " + distanceUnitInput.value;
@ -92,8 +93,8 @@ function overviewRoutes() {
} }
function zoomToRoute() { function zoomToRoute() {
const r = +this.parentNode.dataset.id; const routeId = +this.parentNode.dataset.id;
const route = routes.select("#route" + r).node(); const route = routes.select("#route" + routeId).node();
highlightElement(route, 3); highlightElement(route, 3);
} }
@ -111,15 +112,16 @@ function overviewRoutes() {
} }
function openRouteEditor() { function openRouteEditor() {
const id = "route" + this.parentNode.dataset.id; const routeId = "route" + this.parentNode.dataset.id;
editRoute(id); editRoute(routeId);
} }
function toggleLockStatus() { function toggleLockStatus() {
const routeId = +this.parentNode.dataset.id; const routeId = +this.parentNode.dataset.id;
const route = pack.routes[routeId]; const route = pack.routes.find(route => route.i === routeId);
route.lock = !route.lock; if (!route) return;
route.lock = !route.lock;
if (this.classList.contains("icon-lock")) { if (this.classList.contains("icon-lock")) {
this.classList.remove("icon-lock"); this.classList.remove("icon-lock");
this.classList.add("icon-lock-open"); this.classList.add("icon-lock-open");

View file

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

View file

@ -28,6 +28,7 @@ window.UISubmap = (function () {
$("#submapOptionsDialog").dialog({ $("#submapOptionsDialog").dialog({
title: "Create a submap", title: "Create a submap",
resizable: false, resizable: false,
width: "32em",
position: {my: "center", at: "center", of: "svg"}, position: {my: "center", at: "center", of: "svg"},
buttons: { buttons: {
Submap: function () { Submap: function () {
@ -142,6 +143,7 @@ window.UISubmap = (function () {
fullMap: true, fullMap: true,
noLabels: true, noLabels: true,
noScaleBar: true, noScaleBar: true,
noVignette: true,
noIce: true noIce: true
}); });
@ -282,7 +284,7 @@ window.UISubmap = (function () {
oldstate = null; // destroy old state to free memory oldstate = null; // destroy old state to free memory
restoreLayers(); drawLayers();
if (ThreeD.options.isOn) ThreeD.redraw(); if (ThreeD.options.isOn) ThreeD.redraw();
if ($("#worldConfigurator").is(":visible")) editWorld(); if ($("#worldConfigurator").is(":visible")) editWorld();
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -50,14 +50,14 @@ window.Zones = (function () {
const startCell = ra(borderCells); const startCell = ra(borderCells);
if (startCell === undefined) return; if (startCell === undefined) return;
const invationCells = []; const invasionCells = [];
const queue = [startCell]; const queue = [startCell];
const maxCells = rand(5, 30); const maxCells = rand(5, 30);
while (queue.length) { while (queue.length) {
const cellId = P(0.4) ? queue.shift() : queue.pop(); const cellId = P(0.4) ? queue.shift() : queue.pop();
invationCells.push(cellId); invasionCells.push(cellId);
if (invationCells.length >= maxCells) break; if (invasionCells.length >= maxCells) break;
cells.c[cellId].forEach(neibCellId => { cells.c[cellId].forEach(neibCellId => {
if (usedCells[neibCellId]) return; if (usedCells[neibCellId]) return;
@ -73,15 +73,19 @@ window.Zones = (function () {
Conquest: 3, Conquest: 3,
Incursion: 2, Incursion: 2,
Intervention: 2, Intervention: 2,
Subjugation: 1, Assault: 1,
Foray: 1, Foray: 1,
Skirmishes: 1, Intrusion: 1,
Irruption: 1,
Offensive: 1,
Pillaging: 1, Pillaging: 1,
Raid: 1 Plunder: 1,
Raid: 1,
Skirmishes: 1
}); });
const name = getAdjective(states[attacker].name) + " " + subtype; const name = getAdjective(states[attacker].name) + " " + subtype;
pack.zones.push({i: pack.zones.length, name, type: "Invasion", cells: invationCells, color: "url(#hatch1)"}); pack.zones.push({i: pack.zones.length, name, type: "Invasion", cells: invasionCells, color: "url(#hatch1)"});
} }
function addRebels(usedCells) { function addRebels(usedCells) {
@ -120,10 +124,13 @@ window.Zones = (function () {
Insurrection: 2, Insurrection: 2,
Mutineers: 1, Mutineers: 1,
Insurgents: 1, Insurgents: 1,
Rebellion: 1,
Renegades: 1,
Revolters: 1,
Revolutionaries: 1,
Rioters: 1, Rioters: 1,
Separatists: 1, Separatists: 1,
Secessionists: 1, Secessionists: 1,
Rebellion: 1,
Conspiracy: 1 Conspiracy: 1
}); });
@ -226,7 +233,7 @@ window.Zones = (function () {
const name = `${(() => { const name = `${(() => {
const model = rw({color: 2, animal: 1, adjective: 1}); const model = rw({color: 2, animal: 1, adjective: 1});
if (model === "color") return ra(["Amber", "Azure", "Black", "Blue", "Brown", "Crimson", "Emerald", "Golden", "Green", "Grey", "Orange", "Pink", "Purple", "Red", "Ruby", "Scarlet", "Silver", "Violet", "White", "Yellow"]); if (model === "color") return ra(["Amber", "Azure", "Black", "Blue", "Brown", "Crimson", "Emerald", "Golden", "Green", "Grey", "Orange", "Pink", "Purple", "Red", "Ruby", "Scarlet", "Silver", "Violet", "White", "Yellow"]);
if (model === "animal") return ra(["Ape", "Bear", "Bird", "Boar", "Cat", "Cow", "Dog", "Fox", "Horse", "Lion", "Pig", "Rat", "Raven", "Sheep", "Spider", "Tiger", "Viper", "Wolf", "Worm", "Wyrm"]); if (model === "animal") return ra(["Ape", "Bear", "Bird", "Boar", "Cat", "Cow", "Deer", "Dog", "Fox", "Goat", "Horse", "Lion", "Pig", "Rat", "Raven", "Sheep", "Spider", "Tiger", "Viper", "Wolf", "Worm", "Wyrm"]);
if (model === "adjective") return ra(["Blind", "Bloody", "Brutal", "Burning", "Deadly", "Fatal", "Furious", "Great", "Grim", "Horrible", "Invisible", "Lethal", "Loud", "Mortal", "Savage", "Severe", "Silent", "Unknown", "Venomous", "Vicious"]); if (model === "adjective") return ra(["Blind", "Bloody", "Brutal", "Burning", "Deadly", "Fatal", "Furious", "Great", "Grim", "Horrible", "Invisible", "Lethal", "Loud", "Mortal", "Savage", "Severe", "Silent", "Unknown", "Venomous", "Vicious"]);
})()} ${rw({Fever: 5, Plague: 3, Cough: 3, Flu: 2, Pox: 2, Cholera: 2, Typhoid: 2, Leprosy: 1, Smallpox: 1, Pestilence: 1, Consumption: 1, Malaria: 1, Dropsy: 1})}`; })()} ${rw({Fever: 5, Plague: 3, Cough: 3, Flu: 2, Pox: 2, Cholera: 2, Typhoid: 2, Leprosy: 1, Smallpox: 1, Pestilence: 1, Consumption: 1, Malaria: 1, Dropsy: 1})}`;

9
sw.js
View file

@ -1,7 +1,7 @@
importScripts("https://storage.googleapis.com/workbox-cdn/releases/6.2.0/workbox-sw.js"); importScripts("https://storage.googleapis.com/workbox-cdn/releases/6.2.0/workbox-sw.js");
const {Route, registerRoute} = workbox.routing; const {Route, registerRoute} = workbox.routing;
const {CacheFirst, NetworkFirst} = workbox.strategies; const {CacheFirst, NetworkFirst, StaleWhileRevalidate} = workbox.strategies;
const {CacheableResponsePlugin} = workbox.cacheableResponse; const {CacheableResponsePlugin} = workbox.cacheableResponse;
const {ExpirationPlugin} = workbox.expiration; const {ExpirationPlugin} = workbox.expiration;
@ -18,8 +18,11 @@ registerRoute(
registerRoute( registerRoute(
({request, url}) => ({request, url}) =>
request.destination === "script" && !url.pathname.endsWith("min.js") && !url.pathname.includes("versioning.js"), request.destination === "script" &&
new CacheFirst({ !url.pathname.endsWith("min.js") &&
!url.pathname.includes("versioning.js") &&
!url.pathname.includes("google"),
new StaleWhileRevalidate({
cacheName: "fmg-scripts", cacheName: "fmg-scripts",
plugins: [ plugins: [
new CacheableResponsePlugin({statuses: [0, 200]}), new CacheableResponsePlugin({statuses: [0, 200]}),

View file

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

View file

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

View file

@ -7,12 +7,13 @@
* *
* Update the version MANUALLY on each merge to main: * Update the version MANUALLY on each merge to main:
* 1. MAJOR version: Incompatible changes that break existing maps * 1. MAJOR version: Incompatible changes that break existing maps
* 2. MINOR version: Backwards-compatible changes requiring old .map files to be updated * 2. MINOR version: Additions or changes that are backward-compatible but may require old .map files to be updated
* 3. PATCH version: Backwards-compatible bug fixes not affecting .map file format * 3. PATCH version: Backward-compatible bug fixes and small features that do not affect the .map file format
* *
* Example: 1.102.0 -> Major version 1, Minor version 102, Patch version 0 * Example: 1.102.2 -> Major version 1, Minor version 102, Patch version 2
*/ */
const VERSION = "1.102.00"; const VERSION = "1.105.0";
if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format or parsing function");
{ {
document.title += " v" + VERSION; document.title += " v" + VERSION;
@ -20,7 +21,9 @@ const VERSION = "1.102.00";
if (loadingScreenVersion) loadingScreenVersion.innerText = `v${VERSION}`; if (loadingScreenVersion) loadingScreenVersion.innerText = `v${VERSION}`;
const storedVersion = localStorage.getItem("version"); const storedVersion = localStorage.getItem("version");
if (compareVersions(storedVersion, VERSION, {patch: false}).isOlder) setTimeout(showUpdateWindow, 6000); if (compareVersions(storedVersion, VERSION, {major: true, minor: true, patch: false}).isOlder) {
setTimeout(showUpdateWindow, 6000);
}
function showUpdateWindow() { function showUpdateWindow() {
const changelog = "https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog"; const changelog = "https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog";
@ -33,7 +36,7 @@ const VERSION = "1.102.00";
<ul> <ul>
<strong>Latest changes:</strong> <strong>Latest changes:</strong>
<li>Style: ability to set letter spacing</li> <li>Labels: ability to set letter spacing</li>
<li>Zones update</li> <li>Zones update</li>
<li>Notes Editor: on-demand AI text generation</li> <li>Notes Editor: on-demand AI text generation</li>
<li>New style preset: Dark Seas</li> <li>New style preset: Dark Seas</li>
@ -43,41 +46,55 @@ const VERSION = "1.102.00";
<li>Preview villages map</li> <li>Preview villages map</li>
<li>Ability to render ocean heightmap</li> <li>Ability to render ocean heightmap</li>
<li>Scale bar styling features</li> <li>Scale bar styling features</li>
<li>Vignette visual layer and vignette styling options</li>
</ul> </ul>
<p>Join our <a href="${discord}" target="_blank">Discord server</a> and <a href="${reddit}" target="_blank">Reddit community</a> to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.</p> <p>Join our <a href="${discord}" target="_blank">Discord server</a> and <a href="${reddit}" target="_blank">Reddit community</a> to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.</p>
<span><i>Thanks for all supporters on <a href="${patreon}" target="_blank">Patreon</a>!</i></span>`; <span><i>Thanks for all supporters on <a href="${patreon}" target="_blank">Patreon</a>!</i></span>`;
const buttons = {
Ok: function () {
$(this).dialog("close");
localStorage.setItem("version", VERSION);
}
};
if (storedVersion) {
buttons.Cleanup = () => {
clearCache();
localStorage.clear();
localStorage.setItem("version", VERSION);
location.reload();
};
}
$("#alert").dialog({ $("#alert").dialog({
resizable: false, resizable: false,
title: "Fantasy Map Generator update", title: "Fantasy Map Generator update",
width: "28em", width: "28em",
position: {my: "center center-4em", at: "center", of: "svg"}, position: {my: "center center-4em", at: "center", of: "svg"},
buttons buttons: {
"Cleanup data": () => cleanupData(),
"Don't show again": function () {
$(this).dialog("close");
localStorage.setItem("version", VERSION);
}
}
}); });
} }
}
async function clearCache() { async function cleanupData() {
const cacheNames = await caches.keys(); await clearCache();
return Promise.all(cacheNames.map(cacheName => caches.delete(cacheName))); localStorage.clear();
localStorage.setItem("version", VERSION);
localStorage.setItem("disable_click_arrow_tooltip", "true");
location.reload();
}
async function clearCache() {
const cacheNames = await caches.keys();
return Promise.all(cacheNames.map(cacheName => caches.delete(cacheName)));
}
function parseMapVersion(version) {
let [major, minor, patch] = version.split(".");
if (patch === undefined) {
// e.g. 1.732
minor = minor.slice(0, 2);
patch = minor.slice(2);
} }
// e.g. 0.7b
major = parseInt(major) || 0;
minor = parseInt(minor) || 0;
patch = parseInt(patch) || 0;
return `${major}.${minor}.${patch}`;
} }
function isValidVersion(versionString) { function isValidVersion(versionString) {