mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 09:41:24 +01:00
Merge branch 'master' of https://github.com/Azgaar/Fantasy-Map-Generator into dev-economics
This commit is contained in:
commit
7dc71a5616
33 changed files with 5797 additions and 2941 deletions
12
index.css
12
index.css
|
|
@ -239,7 +239,6 @@ i.icon-lock {
|
||||||
#labels {
|
#labels {
|
||||||
text-anchor: start;
|
text-anchor: start;
|
||||||
dominant-baseline: central;
|
dominant-baseline: central;
|
||||||
text-shadow: 0 0 4px white;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -285,6 +284,12 @@ i.icon-lock {
|
||||||
animation: dash 80s linear backwards;
|
animation: dash 80s linear backwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
marker-end: url(#end-arrow-small);
|
||||||
|
stroke: #555;
|
||||||
|
stroke-width: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes dash {
|
@keyframes dash {
|
||||||
to {
|
to {
|
||||||
stroke-dashoffset: 0;
|
stroke-dashoffset: 0;
|
||||||
|
|
@ -1554,6 +1559,11 @@ div.states > div.resourceBonus > span.icon-male {
|
||||||
width: 5.5em;
|
width: 5.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#saveTilesScreen div.label {
|
||||||
|
display: inline-block;
|
||||||
|
width: 5em;
|
||||||
|
}
|
||||||
|
|
||||||
#regimentBody div {
|
#regimentBody div {
|
||||||
margin: 0.1em 0;
|
margin: 0.1em 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
100
index.html
100
index.html
|
|
@ -235,7 +235,7 @@
|
||||||
<div id="loading">
|
<div id="loading">
|
||||||
<div id="titleName"><t data-t="titleName">Azgaar's</t></div>
|
<div id="titleName"><t data-t="titleName">Azgaar's</t></div>
|
||||||
<div id="title"><t data-t="title">Fantasy Map Generator</t></div>
|
<div id="title"><t data-t="title">Fantasy Map Generator</t></div>
|
||||||
<div id="version"><t data-t="version">v. </t>1.61</div>
|
<div id="version"><t data-t="version">v. </t>1.63</div>
|
||||||
<p id="loading-text"><t data-t="loading">LOADING</t><span>.</span><span>.</span><span>.</span></p>
|
<p id="loading-text"><t data-t="loading">LOADING</t><span>.</span><span>.</span><span>.</span></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -673,6 +673,15 @@
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
||||||
|
<tbody id="styleShadow">
|
||||||
|
<tr data-tip="Set text shadow">
|
||||||
|
<td>Text shadow</td>
|
||||||
|
<td>
|
||||||
|
<input id="styleShadowInput" type="text" value="0 0 4px white"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
|
||||||
<tbody id="styleFont">
|
<tbody id="styleFont">
|
||||||
<tr data-tip="Select font">
|
<tr data-tip="Select font">
|
||||||
<td>Font</td>
|
<td>Font</td>
|
||||||
|
|
@ -756,7 +765,7 @@
|
||||||
<tr data-tip="Select color scheme for the element">
|
<tr data-tip="Select color scheme for the element">
|
||||||
<td>Color scheme</td>
|
<td>Color scheme</td>
|
||||||
<td>
|
<td>
|
||||||
<select id="styleHeightmapScheme" onchange="drawHeightmap()">
|
<select id="styleHeightmapScheme">
|
||||||
<option value="bright" selected>Bright</option>
|
<option value="bright" selected>Bright</option>
|
||||||
<option value="light">Light</option>
|
<option value="light">Light</option>
|
||||||
<option value="green">Green</option>
|
<option value="green">Green</option>
|
||||||
|
|
@ -768,14 +777,14 @@
|
||||||
<tr data-tip="Terracing rate. Set to 0 (toggle off) to improve performance">
|
<tr data-tip="Terracing rate. Set to 0 (toggle off) to improve performance">
|
||||||
<td>Terracing</td>
|
<td>Terracing</td>
|
||||||
<td>
|
<td>
|
||||||
<input id="styleHeightmapTerracing" type="range" min=0 max=20 step=1>
|
<input id="styleHeightmapTerracingInput" type="range" min=0 max=20 step=1>
|
||||||
<output id="styleHeightmapTerracingOutput">0</output>
|
<output id="styleHeightmapTerracingOutput">0</output>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr data-tip="Layers reduction rate. Increase to improve performance">
|
<tr data-tip="Layers reduction rate. Increase to improve performance">
|
||||||
<td>Reduce layers</td>
|
<td>Reduce layers</td>
|
||||||
<td>
|
<td>
|
||||||
<input id="styleHeightmapSkip" type="range" min=0 max=10 step=1 value=5 oninput="styleHeightmapSkipOutput.value = this.value; drawHeightmap()">
|
<input id="styleHeightmapSkipInput" type="range" min=0 max=10 step=1 value=5 >
|
||||||
<output id="styleHeightmapSkipOutput">5</output>
|
<output id="styleHeightmapSkipOutput">5</output>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -783,7 +792,7 @@
|
||||||
<tr data-tip="Line simplification rate. Increase to slightly improve performance">
|
<tr data-tip="Line simplification rate. Increase to slightly improve performance">
|
||||||
<td>Simplify line</td>
|
<td>Simplify line</td>
|
||||||
<td>
|
<td>
|
||||||
<input id="styleHeightmapSimplification" type="range" min=0 max=10 step=1 value=0 oninput="styleHeightmapSimplificationOutput.value = this.value; drawHeightmap()">
|
<input id="styleHeightmapSimplificationInput" type="range" min=0 max=10 step=1 value=0 >
|
||||||
<output id="styleHeightmapSimplificationOutput">0</output>
|
<output id="styleHeightmapSimplificationOutput">0</output>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -791,7 +800,7 @@
|
||||||
<tr data-tip="Select line interpolation type">
|
<tr data-tip="Select line interpolation type">
|
||||||
<td>Line style</td>
|
<td>Line style</td>
|
||||||
<td>
|
<td>
|
||||||
<select id="styleHeightmapCurve" onchange="drawHeightmap()">
|
<select id="styleHeightmapCurve">
|
||||||
<option value=0 selected>Curved</option>
|
<option value=0 selected>Curved</option>
|
||||||
<option value=1>Linear</option>
|
<option value=1>Linear</option>
|
||||||
<option value=2>Rectangular</option>
|
<option value=2>Rectangular</option>
|
||||||
|
|
@ -957,7 +966,7 @@
|
||||||
<input id="pointsInput" type="range" min=1 max=13 value=4 data-cells=10000 >
|
<input id="pointsInput" type="range" min=1 max=13 value=4 data-cells=10000 >
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<output id="pointsOutput" style="color: #053305">10K</output>
|
<output id="pointsOutput_formatted" style="color: #053305">10K</output>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
|
@ -1155,7 +1164,7 @@
|
||||||
<input id="tooltipSizeInput" data-stored="tooltipSize" type="range" min=4 max=32 value=14>
|
<input id="tooltipSizeInput" data-stored="tooltipSize" type="range" min=4 max=32 value=14>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input id="tooltipSizeOutput" data-stored="tooltipSize" type="number" min=4 max=32 value=14>
|
<input id="tooltipSizeOutput" data-stored="tooltipSize" type="number" min=0 max=256 value=14>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
|
@ -1364,9 +1373,20 @@
|
||||||
<input id="renderOcean" class="checkbox" type="checkbox">
|
<input id="renderOcean" class="checkbox" type="checkbox">
|
||||||
<label for="renderOcean" class="checkbox-label">Render ocean cells</label>
|
<label for="renderOcean" class="checkbox-label">Render ocean cells</label>
|
||||||
</div>
|
</div>
|
||||||
<div id="changeHeightsBox" data-tip="Regenerate rivers and allow water flow to slightly change heights">
|
<div id="allowErosionBox" data-tip="Regenerate rivers and allow water flow to change heights and form new lakes. Better to keep checked">
|
||||||
<input id="changeHeights" class="checkbox" type="checkbox" checked>
|
<input id="allowErosion" class="checkbox" type="checkbox" checked>
|
||||||
<label for="changeHeights" class="checkbox-label">Allow water erosion</label>
|
<label for="allowErosion" class="checkbox-label">Allow water erosion</label>
|
||||||
|
</div>
|
||||||
|
<div data-tip="Maximum number of iterations taken to resolve depressions. Increase if you have rivers ending nowhere">
|
||||||
|
Depressions filling max iterations:
|
||||||
|
<input id="resolveDepressionsStepsInput" data-stored="resolveDepressionsSteps" type="range" min=0 max=500 value=250>
|
||||||
|
<input id="resolveDepressionsStepsOutput" data-stored="resolveDepressionsSteps" type="number" min=0 max=1000 value=250>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-tip="Depression depth to form a new lake. Increase to reduce number of lakes added by system">
|
||||||
|
Depression depth threshold:
|
||||||
|
<input id="lakeElevationLimitInput" data-stored="lakeElevationLimit" type="range" min=0 max=80 value=20>
|
||||||
|
<input id="lakeElevationLimitOutput" data-stored="lakeElevationLimit" type="number" min=0 max=80 value=20>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -2328,8 +2348,8 @@
|
||||||
<div style="width:4em">Hill</div>
|
<div style="width:4em">Hill</div>
|
||||||
<i class="icon-trash-empty pointer" data-tip="Remove the step"></i>
|
<i class="icon-trash-empty pointer" data-tip="Remove the step"></i>
|
||||||
<i class="icon-resize-vertical" data-tip="Drag to reorder"></i>
|
<i class="icon-resize-vertical" data-tip="Drag to reorder"></i>
|
||||||
<span>y:<input class="templateY" data-tip="Placement range percentage along Y axis (minY-maxY)" value="47-53"></span>
|
<span>y:<input class="templateY" data-tip="Y axis position in percentage (minY-maxY or Y)" value="47-53"></span>
|
||||||
<span>x:<input class="templateX" data-tip="Placement range percentage along X axis (minX-maxX)" value="65-75"></span>
|
<span>x:<input class="templateX" data-tip="X axis position in percentage (minX-maxX or X)" value="65-75"></span>
|
||||||
<span>h:<input class="templateHeight" data-tip="Blob maximum height, use hyphen to get a random number in range" value="90-100"></span>
|
<span>h:<input class="templateHeight" data-tip="Blob maximum height, use hyphen to get a random number in range" value="90-100"></span>
|
||||||
<span>n:<input class="templateCount" data-tip="Blobs to add, use hyphen to get a random number in range" value="1"></span>
|
<span>n:<input class="templateCount" data-tip="Blobs to add, use hyphen to get a random number in range" value="1"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2844,6 +2864,7 @@
|
||||||
<button id="notesPin" data-tip="Toggle notes box dispay: hide or do not hide the box on mouse move" class="icon-pin"></button>
|
<button id="notesPin" data-tip="Toggle notes box dispay: hide or do not hide the box on mouse move" class="icon-pin"></button>
|
||||||
<button id="notesDownload" data-tip="Download notes to PC" class="icon-download"></button>
|
<button id="notesDownload" data-tip="Download notes to PC" class="icon-download"></button>
|
||||||
<button id="notesUpload" data-tip="Upload notes from PC" class="icon-upload"></button>
|
<button id="notesUpload" data-tip="Upload notes from PC" class="icon-upload"></button>
|
||||||
|
<button id="notesClearStyle" data-tip="Remove all styling, get plain text only" class="icon-eraser"></button>
|
||||||
<button id="notesRemove" data-tip="Remove this note" class="icon-trash fastDelete"></button>
|
<button id="notesRemove" data-tip="Remove this note" class="icon-trash fastDelete"></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -3136,8 +3157,8 @@
|
||||||
|
|
||||||
<div data-tip="Set scale bar size">
|
<div data-tip="Set scale bar size">
|
||||||
<div>Bar size:</div>
|
<div>Bar size:</div>
|
||||||
<input id="barSizeOutput" type="range" min=.5 max=5 value=2 step=.1>
|
<input id="barSizeOutput" data-stored="barSize" type="range" min=.5 max=5 value=2 step=.1>
|
||||||
<input id="barSize" data-stored="barSize" type="number" min=.5 max=5 value=2 step=.1>
|
<input id="barSizeInput" data-stored="barSize" type="number" min=.5 max=5 value=2 step=.1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div data-tip="Type scale bar label, leave blank to hide label">
|
<div data-tip="Type scale bar label, leave blank to hide label">
|
||||||
|
|
@ -3164,14 +3185,14 @@
|
||||||
|
|
||||||
<div data-tip="Set how many people are in one population point">
|
<div data-tip="Set how many people are in one population point">
|
||||||
<div>1 population point =</div>
|
<div>1 population point =</div>
|
||||||
<input id="populationRateOutput" type="range" min=10 max=9990 step=10 value=1000 style="width:6em">
|
<input id="populationRateOutput" data-stored="populationRate" type="range" min=10 max=9990 step=10 value=1000 style="width:6em">
|
||||||
<input id="populationRate" data-stored="populationRate" type="number" min=10 max=9990 step=10 value=1000 data-value=1000 style="width:4.5em">
|
<input id="populationRateInput" data-stored="populationRate" type="number" min=10 max=9990 step=10 value=1000 style="width:4.5em">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div data-tip="Set urbanization rate: burgs population relative to all population">
|
<div data-tip="Set urbanization rate: burgs population relative to all population">
|
||||||
<div>Urbanization rate:</div>
|
<div>Urbanization rate:</div>
|
||||||
<input id="urbanizationOutput" type="range" min=.01 max=5 step=.01 value=1>
|
<input id="urbanizationOutput" data-stored="urbanization" type="range" min=.01 max=5 step=.01 value=1 >
|
||||||
<input id="urbanization" data-stored="urbanization" type="number" min=.01 max=5 step=.01 value=1 data-value=1>
|
<input id="urbanizationInput" data-stored="urbanization" type="number" min=.01 max=5 step=.01 value=1 >
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -3450,6 +3471,7 @@
|
||||||
<button onclick="saveSVG()" data-tip="Download the map as vector image (open in browser or Inkscape)">.svg</button>
|
<button onclick="saveSVG()" data-tip="Download the map as vector image (open in browser or Inkscape)">.svg</button>
|
||||||
<button onclick="savePNG()" data-tip="Download visible part of the map as .png (lossless compressed)">.png</button>
|
<button onclick="savePNG()" data-tip="Download visible part of the map as .png (lossless compressed)">.png</button>
|
||||||
<button onclick="saveJPEG()" data-tip="Download visible part of the map as .jpeg (lossy compressed) image">.jpeg</button>
|
<button onclick="saveJPEG()" data-tip="Download visible part of the map as .jpeg (lossy compressed) image">.jpeg</button>
|
||||||
|
<button onclick="openSaveTiles()" data-tip="Split map into smaller png tiles and download as zip archive">tiles</button>
|
||||||
<button onclick="saveGeoJSON()" data-tip="Download map data in GeoJSON format">.json</button>
|
<button onclick="saveGeoJSON()" data-tip="Download map data in GeoJSON format">.json</button>
|
||||||
<button onclick="quickSave()" data-tip="Save the project to browser storage (unreliable). Shortcut: F6">storage</button>
|
<button onclick="quickSave()" data-tip="Save the project to browser storage (unreliable). Shortcut: F6">storage</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -3457,8 +3479,12 @@
|
||||||
<p style="font-style: italic">Generator uses pop-up window to download files. Please ensure your browser does not block popups.</p>
|
<p style="font-style: italic">Generator uses pop-up window to download files. Please ensure your browser does not block popups.</p>
|
||||||
<div data-tip="Define scale of a saved png/jpeg image (e.g. 5x). Saving big images is slow and may cause a browser crash!" style="margin-bottom: .3em">
|
<div data-tip="Define scale of a saved png/jpeg image (e.g. 5x). Saving big images is slow and may cause a browser crash!" style="margin-bottom: .3em">
|
||||||
PNG / JPEG scale:
|
PNG / JPEG scale:
|
||||||
<input id="pngResolutionInput" data-stored="pngResolution" type="range" min=1 max=8 value=1 style="width: 10.8em" oninput="pngResolutionOutput.value = this.value">
|
<input id="pngResolutionInput" data-stored="pngResolution" type="range" min=1 max=8 value=1 style="width: 14em">
|
||||||
<input id="pngResolutionOutput" data-stored="pngResolution" type="number" min=1 max=8 value=1 oninput="pngResolutionInput.value = this.value">
|
<input id="pngResolutionOutput" data-stored="pngResolution" type="number" min=1 max=8 value=1>
|
||||||
|
</div>
|
||||||
|
<div data-tip="Check to not allow system to automatically hide labels">
|
||||||
|
<input id="showLabels" class="checkbox" type="checkbox" onchange="hideLabels.checked = !this.checked; invokeActiveZooming()" checked="">
|
||||||
|
<label for="showLabels" class="checkbox-label">Show all labels</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -3471,6 +3497,31 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="saveTilesScreen" style="display: none" class="dialog">
|
||||||
|
<p style="font-style: italic">Map will be split into tiles and downloaded as a single zip file. Avoid saving to big images</p>
|
||||||
|
<div data-tip="Number of columns" style="margin-bottom: .3em">
|
||||||
|
<div class="label">Columns:</div>
|
||||||
|
<input id="tileColsInput" data-stored="tileCols" type="range" min=2 max=20 value=8 style="width: 11em">
|
||||||
|
<input id="tileColsOutput" data-stored="tileCols" type="number" min=2 value=8 >
|
||||||
|
</div>
|
||||||
|
<div data-tip="Number of rows" style="margin-bottom: .3em">
|
||||||
|
<div class="label">Rows:</div>
|
||||||
|
<input id="tileRowsInput" data-stored="tileRows" type="range" min=2 max=20 value=8 style="width: 11em">
|
||||||
|
<input id="tileRowsOutput" data-stored="tileRows" type="number" min=2 value=8 >
|
||||||
|
</div>
|
||||||
|
<div data-tip="Image scale relative to image size (e.g. 5x)" style="margin-bottom: .3em">
|
||||||
|
<div class="label">Scale:</div>
|
||||||
|
<input id="tileScaleInput" data-stored="tileScale" type="range" min=1 max=4 value=1 style="width: 11em">
|
||||||
|
<input id="tileScaleOutput" data-stored="tileScale" type="number" min=1 value=1
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div data-tip="Calculated size of image if combined" style="margin-bottom: .3em">
|
||||||
|
<div class="label">Total size:</div>
|
||||||
|
<div id="tileSize" style="display: inline-block">1000 x 1000 px</div>
|
||||||
|
</div>
|
||||||
|
<div id="tileStatus" style="background-color: #33333310; font-style: italic"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="alert" style="display: none" class="dialog">
|
<div id="alert" style="display: none" class="dialog">
|
||||||
<p id="alertMessage">Warning!</p>
|
<p id="alertMessage">Warning!</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -3512,6 +3563,9 @@
|
||||||
<marker id="end-arrow" viewBox="0 -5 10 10" refX="6" markerWidth="7" markerHeight="7" orient="auto">
|
<marker id="end-arrow" viewBox="0 -5 10 10" refX="6" markerWidth="7" markerHeight="7" orient="auto">
|
||||||
<path d="M0,-5L10,0L0,5" fill="#000"></path>
|
<path d="M0,-5L10,0L0,5" fill="#000"></path>
|
||||||
</marker>
|
</marker>
|
||||||
|
<marker id="end-arrow-small" viewBox="0 -5 10 10" refX="6" markerWidth="2" markerHeight="2" orient="auto">
|
||||||
|
<path d="M0,-5L10,0L0,5" fill="#555"></path>
|
||||||
|
</marker>
|
||||||
|
|
||||||
<symbol id="icon-store" viewBox="0 0 616 512">
|
<symbol id="icon-store" viewBox="0 0 616 512">
|
||||||
<path d="M602 118.6L537.1 15C531.3 5.7 521 0 510 0H106C95 0 84.7 5.7 78.9 15L14 118.6c-33.5 53.5-3.8 127.9 58.8 136.4 4.5.6 9.1.9 13.7.9 29.6 0 55.8-13 73.8-33.1 18 20.1 44.3 33.1 73.8 33.1 29.6 0 55.8-13 73.8-33.1 18 20.1 44.3 33.1 73.8 33.1 29.6 0 55.8-13 73.8-33.1 18.1 20.1 44.3 33.1 73.8 33.1 4.7 0 9.2-.3 13.7-.9 62.8-8.4 92.6-82.8 59-136.4zM529.5 288c-10 0-19.9-1.5-29.5-3.8V384H116v-99.8c-9.6 2.2-19.5 3.8-29.5 3.8-6 0-12.1-.4-18-1.2-5.6-.8-11.1-2.1-16.4-3.6V480c0 17.7 14.3 32 32 32h448c17.7 0 32-14.3 32-32V283.2c-5.4 1.6-10.8 2.9-16.4 3.6-6.1.8-12.1 1.2-18.2 1.2z"></path>
|
<path d="M602 118.6L537.1 15C531.3 5.7 521 0 510 0H106C95 0 84.7 5.7 78.9 15L14 118.6c-33.5 53.5-3.8 127.9 58.8 136.4 4.5.6 9.1.9 13.7.9 29.6 0 55.8-13 73.8-33.1 18 20.1 44.3 33.1 73.8 33.1 29.6 0 55.8-13 73.8-33.1 18 20.1 44.3 33.1 73.8 33.1 29.6 0 55.8-13 73.8-33.1 18.1 20.1 44.3 33.1 73.8 33.1 4.7 0 9.2-.3 13.7-.9 62.8-8.4 92.6-82.8 59-136.4zM529.5 288c-10 0-19.9-1.5-29.5-3.8V384H116v-99.8c-9.6 2.2-19.5 3.8-29.5 3.8-6 0-12.1-.4-18-1.2-5.6-.8-11.1-2.1-16.4-3.6V480c0 17.7 14.3 32 32 32h448c17.7 0 32-14.3 32-32V283.2c-5.4 1.6-10.8 2.9-16.4 3.6-6.1.8-12.1 1.2-18.2 1.2z"></path>
|
||||||
|
|
@ -4391,7 +4445,8 @@
|
||||||
<script defer src="modules/ui/general.js"></script>
|
<script defer src="modules/ui/general.js"></script>
|
||||||
<script defer src="modules/ui/options.js"></script>
|
<script defer src="modules/ui/options.js"></script>
|
||||||
<script defer src="modules/ui/style.js"></script>
|
<script defer src="modules/ui/style.js"></script>
|
||||||
<script defer src="modules/save-and-load.js"></script>
|
<script defer src="modules/save.js"></script>
|
||||||
|
<script defer src="modules/load.js"></script>
|
||||||
<script defer src="main.js"></script>
|
<script defer src="main.js"></script>
|
||||||
<script defer src="modules/relief-icons.js"></script>
|
<script defer src="modules/relief-icons.js"></script>
|
||||||
<script defer src="modules/ui/tools.js"></script>
|
<script defer src="modules/ui/tools.js"></script>
|
||||||
|
|
@ -4431,5 +4486,6 @@
|
||||||
<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="libs/pell.min.js"></script>
|
<script defer src="libs/pell.min.js"></script>
|
||||||
|
<script defer src="libs/jszip.min.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
13
libs/jszip.min.js
vendored
Normal file
13
libs/jszip.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1,49 +1,72 @@
|
||||||
(function (global, factory) {
|
(function (global, factory) {
|
||||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
typeof exports === "object" && typeof module !== "undefined" ? (module.exports = factory()) : typeof define === "function" && define.amd ? define(factory) : (global.HeightmapGenerator = factory());
|
||||||
typeof define === 'function' && define.amd ? define(factory) :
|
})(this, function () {
|
||||||
(global.HeightmapGenerator = factory());
|
"use strict";
|
||||||
}(this, (function () { 'use strict';
|
|
||||||
|
|
||||||
let cells, p;
|
let cells, p;
|
||||||
|
|
||||||
const generate = function () {
|
const generate = function () {
|
||||||
TIME && console.time('generateHeightmap');
|
TIME && console.time("generateHeightmap");
|
||||||
cells = grid.cells, p = grid.points;
|
cells = grid.cells;
|
||||||
|
p = grid.points;
|
||||||
cells.h = new Uint8Array(grid.points.length);
|
cells.h = new Uint8Array(grid.points.length);
|
||||||
|
|
||||||
switch (document.getElementById("templateInput").value) {
|
const template = document.getElementById("templateInput").value;
|
||||||
case "Volcano": templateVolcano(); break;
|
switch (template) {
|
||||||
case "High Island": templateHighIsland(); break;
|
case "Volcano":
|
||||||
case "Low Island": templateLowIsland(); break;
|
templateVolcano();
|
||||||
case "Continents": templateContinents(); break;
|
break;
|
||||||
case "Archipelago": templateArchipelago(); break;
|
case "High Island":
|
||||||
case "Atoll": templateAtoll(); break;
|
templateHighIsland();
|
||||||
case "Mediterranean": templateMediterranean(); break;
|
break;
|
||||||
case "Peninsula": templatePeninsula(); break;
|
case "Low Island":
|
||||||
case "Pangea": templatePangea(); break;
|
templateLowIsland();
|
||||||
case "Isthmus": templateIsthmus(); break;
|
break;
|
||||||
case "Shattered": templateShattered(); break;
|
case "Continents":
|
||||||
|
templateContinents();
|
||||||
|
break;
|
||||||
|
case "Archipelago":
|
||||||
|
templateArchipelago();
|
||||||
|
break;
|
||||||
|
case "Atoll":
|
||||||
|
templateAtoll();
|
||||||
|
break;
|
||||||
|
case "Mediterranean":
|
||||||
|
templateMediterranean();
|
||||||
|
break;
|
||||||
|
case "Peninsula":
|
||||||
|
templatePeninsula();
|
||||||
|
break;
|
||||||
|
case "Pangea":
|
||||||
|
templatePangea();
|
||||||
|
break;
|
||||||
|
case "Isthmus":
|
||||||
|
templateIsthmus();
|
||||||
|
break;
|
||||||
|
case "Shattered":
|
||||||
|
templateShattered();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
TIME && console.timeEnd('generateHeightmap');
|
TIME && console.timeEnd("generateHeightmap");
|
||||||
}
|
};
|
||||||
|
|
||||||
// parse template step
|
// parse template step
|
||||||
function addStep(a1, a2, a3, a4, a5) {
|
function addStep(a1, a2, a3, a4, a5) {
|
||||||
if (a1 === "Hill") addHill(a2, a3, a4, a5); else
|
if (a1 === "Hill") return addHill(a2, a3, a4, a5);
|
||||||
if (a1 === "Pit") addPit(a2, a3, a4, a5); else
|
if (a1 === "Pit") return addPit(a2, a3, a4, a5);
|
||||||
if (a1 === "Range") addRange(a2, a3, a4, a5); else
|
if (a1 === "Range") return addRange(a2, a3, a4, a5);
|
||||||
if (a1 === "Trough") addTrough(a2, a3, a4, a5); else
|
if (a1 === "Trough") return addTrough(a2, a3, a4, a5);
|
||||||
if (a1 === "Strait") addStrait(a2, a3); else
|
if (a1 === "Strait") return addStrait(a2, a3);
|
||||||
if (a1 === "Add") modify(a3, a2, 1); else
|
if (a1 === "Add") return modify(a3, a2, 1);
|
||||||
if (a1 === "Multiply") modify(a3, 0, a2); else
|
if (a1 === "Multiply") return modify(a3, 0, a2);
|
||||||
if (a1 === "Smooth") smooth(a2);
|
if (a1 === "Smooth") return smooth(a2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Heighmap Template: Volcano
|
// Heighmap Template: Volcano
|
||||||
function templateVolcano() {
|
function templateVolcano() {
|
||||||
addStep("Hill", "1", "90-100", "44-56", "40-60");
|
addStep("Hill", "1", "90-100", "44-56", "40-60");
|
||||||
addStep("Multiply", .8, "50-100");
|
addStep("Multiply", 0.8, "50-100");
|
||||||
addStep("Range", "1.5", "30-55", "45-55", "40-60");
|
addStep("Range", "1.5", "30-55", "45-55", "40-60");
|
||||||
addStep("Smooth", 2);
|
addStep("Smooth", 2);
|
||||||
addStep("Hill", "1.5", "25-35", "25-30", "20-75");
|
addStep("Hill", "1.5", "25-35", "25-30", "20-75");
|
||||||
|
|
@ -62,7 +85,7 @@
|
||||||
addStep("Trough", "2-3", "20-30", "60-80", "70-80");
|
addStep("Trough", "2-3", "20-30", "60-80", "70-80");
|
||||||
addStep("Hill", "1", "10-15", "60-60", "50-50");
|
addStep("Hill", "1", "10-15", "60-60", "50-50");
|
||||||
addStep("Hill", "1.5", "13-16", "15-20", "20-75");
|
addStep("Hill", "1.5", "13-16", "15-20", "20-75");
|
||||||
addStep("Multiply", .8, "20-100");
|
addStep("Multiply", 0.8, "20-100");
|
||||||
addStep("Range", "1.5", "30-40", "15-85", "30-40");
|
addStep("Range", "1.5", "30-40", "15-85", "30-40");
|
||||||
addStep("Range", "1.5", "30-40", "15-85", "60-70");
|
addStep("Range", "1.5", "30-40", "15-85", "60-70");
|
||||||
addStep("Pit", "2-3", "10-15", "15-85", "20-80");
|
addStep("Pit", "2-3", "10-15", "15-85", "20-80");
|
||||||
|
|
@ -79,14 +102,14 @@
|
||||||
addStep("Hill", "1.5", "10-15", "5-15", "20-80");
|
addStep("Hill", "1.5", "10-15", "5-15", "20-80");
|
||||||
addStep("Hill", "1", "10-15", "85-95", "70-80");
|
addStep("Hill", "1", "10-15", "85-95", "70-80");
|
||||||
addStep("Pit", "3-5", "10-15", "15-85", "20-80");
|
addStep("Pit", "3-5", "10-15", "15-85", "20-80");
|
||||||
addStep("Multiply", .4, "20-100");
|
addStep("Multiply", 0.4, "20-100");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Heighmap Template: Continents
|
// Heighmap Template: Continents
|
||||||
function templateContinents() {
|
function templateContinents() {
|
||||||
addStep("Hill", "1", "80-85", "75-80", "40-60");
|
addStep("Hill", "1", "80-85", "75-80", "40-60");
|
||||||
addStep("Hill", "1", "80-85", "20-25", "40-60");
|
addStep("Hill", "1", "80-85", "20-25", "40-60");
|
||||||
addStep("Multiply", .22, "20-100");
|
addStep("Multiply", 0.22, "20-100");
|
||||||
addStep("Hill", "5-6", "15-20", "25-75", "20-82");
|
addStep("Hill", "5-6", "15-20", "25-75", "20-82");
|
||||||
addStep("Range", ".8", "30-60", "5-15", "20-45");
|
addStep("Range", ".8", "30-60", "5-15", "20-45");
|
||||||
addStep("Range", ".8", "30-60", "5-15", "55-80");
|
addStep("Range", ".8", "30-60", "5-15", "55-80");
|
||||||
|
|
@ -118,7 +141,7 @@
|
||||||
addStep("Hill", "1.5", "30-50", "25-75", "30-70");
|
addStep("Hill", "1.5", "30-50", "25-75", "30-70");
|
||||||
addStep("Hill", ".5", "30-50", "25-35", "30-70");
|
addStep("Hill", ".5", "30-50", "25-35", "30-70");
|
||||||
addStep("Smooth", 1);
|
addStep("Smooth", 1);
|
||||||
addStep("Multiply", .2, "25-100");
|
addStep("Multiply", 0.2, "25-100");
|
||||||
addStep("Hill", ".5", "10-20", "50-55", "48-52");
|
addStep("Hill", ".5", "10-20", "50-55", "48-52");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -131,7 +154,7 @@
|
||||||
addStep("Smooth", 1);
|
addStep("Smooth", 1);
|
||||||
addStep("Hill", "2-3", "30-70", "0-5", "20-80");
|
addStep("Hill", "2-3", "30-70", "0-5", "20-80");
|
||||||
addStep("Hill", "2-3", "30-70", "95-100", "20-80");
|
addStep("Hill", "2-3", "30-70", "95-100", "20-80");
|
||||||
addStep("Multiply", .8, "land");
|
addStep("Multiply", 0.8, "land");
|
||||||
addStep("Trough", "3-5", "40-50", "0-100", "0-10");
|
addStep("Trough", "3-5", "40-50", "0-100", "0-10");
|
||||||
addStep("Trough", "3-5", "40-50", "0-100", "90-100");
|
addStep("Trough", "3-5", "40-50", "0-100", "90-100");
|
||||||
}
|
}
|
||||||
|
|
@ -156,7 +179,7 @@
|
||||||
addStep("Hill", "1-2", "5-40", "15-50", "90-100");
|
addStep("Hill", "1-2", "5-40", "15-50", "90-100");
|
||||||
addStep("Hill", "8-12", "20-40", "20-80", "48-52");
|
addStep("Hill", "8-12", "20-40", "20-80", "48-52");
|
||||||
addStep("Smooth", 2);
|
addStep("Smooth", 2);
|
||||||
addStep("Multiply", .7, "land");
|
addStep("Multiply", 0.7, "land");
|
||||||
addStep("Trough", "3-4", "25-35", "5-95", "10-20");
|
addStep("Trough", "3-4", "25-35", "5-95", "10-20");
|
||||||
addStep("Trough", "3-4", "25-35", "5-95", "80-90");
|
addStep("Trough", "3-4", "25-35", "5-95", "80-90");
|
||||||
addStep("Range", "5-6", "30-40", "10-90", "35-65");
|
addStep("Range", "5-6", "30-40", "10-90", "35-65");
|
||||||
|
|
@ -187,48 +210,78 @@
|
||||||
|
|
||||||
function getBlobPower() {
|
function getBlobPower() {
|
||||||
switch (+pointsInput.dataset.cells) {
|
switch (+pointsInput.dataset.cells) {
|
||||||
case 1000: return .93;
|
case 1000:
|
||||||
case 2000: return .95;
|
return 0.93;
|
||||||
case 5000: return .96;
|
case 2000:
|
||||||
case 10000: return .98;
|
return 0.95;
|
||||||
case 20000: return .985;
|
case 5000:
|
||||||
case 30000: return .987;
|
return 0.96;
|
||||||
case 40000: return .9892;
|
case 10000:
|
||||||
case 50000: return .9911;
|
return 0.98;
|
||||||
case 60000: return .9921;
|
case 20000:
|
||||||
case 70000: return .9934;
|
return 0.985;
|
||||||
case 80000: return .9942;
|
case 30000:
|
||||||
case 90000: return .9946;
|
return 0.987;
|
||||||
case 100000: return .995;
|
case 40000:
|
||||||
|
return 0.9892;
|
||||||
|
case 50000:
|
||||||
|
return 0.9911;
|
||||||
|
case 60000:
|
||||||
|
return 0.9921;
|
||||||
|
case 70000:
|
||||||
|
return 0.9934;
|
||||||
|
case 80000:
|
||||||
|
return 0.9942;
|
||||||
|
case 90000:
|
||||||
|
return 0.9946;
|
||||||
|
case 100000:
|
||||||
|
return 0.995;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLinePower() {
|
function getLinePower() {
|
||||||
switch (+pointsInput.dataset.cells) {
|
switch (+pointsInput.dataset.cells) {
|
||||||
case 1000: return .74;
|
case 1000:
|
||||||
case 2000: return .75;
|
return 0.74;
|
||||||
case 5000: return .78;
|
case 2000:
|
||||||
case 10000: return .81;
|
return 0.75;
|
||||||
case 20000: return .82;
|
case 5000:
|
||||||
case 30000: return .83;
|
return 0.78;
|
||||||
case 40000: return .84;
|
case 10000:
|
||||||
case 50000: return .855;
|
return 0.81;
|
||||||
case 60000: return .87;
|
case 20000:
|
||||||
case 70000: return .885;
|
return 0.82;
|
||||||
case 80000: return .91;
|
case 30000:
|
||||||
case 90000: return .92;
|
return 0.83;
|
||||||
case 100000: return .93;
|
case 40000:
|
||||||
|
return 0.84;
|
||||||
|
case 50000:
|
||||||
|
return 0.855;
|
||||||
|
case 60000:
|
||||||
|
return 0.87;
|
||||||
|
case 70000:
|
||||||
|
return 0.885;
|
||||||
|
case 80000:
|
||||||
|
return 0.91;
|
||||||
|
case 90000:
|
||||||
|
return 0.92;
|
||||||
|
case 100000:
|
||||||
|
return 0.93;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const addHill = function (count, height, rangeX, rangeY) {
|
const addHill = function (count, height, rangeX, rangeY) {
|
||||||
count = getNumberInRange(count);
|
count = getNumberInRange(count);
|
||||||
const power = getBlobPower();
|
const power = getBlobPower();
|
||||||
while (count > 0) {addOneHill(); count--;}
|
while (count > 0) {
|
||||||
|
addOneHill();
|
||||||
|
count--;
|
||||||
|
}
|
||||||
|
|
||||||
function addOneHill() {
|
function addOneHill() {
|
||||||
const change = new Uint8Array(cells.h.length);
|
const change = new Uint8Array(cells.h.length);
|
||||||
let limit = 0, start;
|
let limit = 0,
|
||||||
|
start;
|
||||||
let h = lim(getNumberInRange(height));
|
let h = lim(getNumberInRange(height));
|
||||||
|
|
||||||
do {
|
do {
|
||||||
|
|
@ -236,7 +289,7 @@
|
||||||
const y = getPointInRange(rangeY, graphHeight);
|
const y = getPointInRange(rangeY, graphHeight);
|
||||||
start = findGridCell(x, y);
|
start = findGridCell(x, y);
|
||||||
limit++;
|
limit++;
|
||||||
} while (cells.h[start] + h > 90 && limit < 50)
|
} while (cells.h[start] + h > 90 && limit < 50);
|
||||||
|
|
||||||
change[start] = h;
|
change[start] = h;
|
||||||
const queue = [start];
|
const queue = [start];
|
||||||
|
|
@ -245,23 +298,26 @@
|
||||||
|
|
||||||
for (const c of cells.c[q]) {
|
for (const c of cells.c[q]) {
|
||||||
if (change[c]) continue;
|
if (change[c]) continue;
|
||||||
change[c] = change[q] ** power * (Math.random() * .2 + .9);
|
change[c] = change[q] ** power * (Math.random() * 0.2 + 0.9);
|
||||||
if (change[c] > 1) queue.push(c);
|
if (change[c] > 1) queue.push(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cells.h = cells.h.map((h, i) => lim(h + change[i]));
|
cells.h = cells.h.map((h, i) => lim(h + change[i]));
|
||||||
}
|
}
|
||||||
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const addPit = function (count, height, rangeX, rangeY) {
|
const addPit = function (count, height, rangeX, rangeY) {
|
||||||
count = getNumberInRange(count);
|
count = getNumberInRange(count);
|
||||||
while (count > 0) {addOnePit(); count--;}
|
while (count > 0) {
|
||||||
|
addOnePit();
|
||||||
|
count--;
|
||||||
|
}
|
||||||
|
|
||||||
function addOnePit() {
|
function addOnePit() {
|
||||||
const used = new Uint8Array(cells.h.length);
|
const used = new Uint8Array(cells.h.length);
|
||||||
let limit = 0, start;
|
let limit = 0,
|
||||||
|
start;
|
||||||
let h = lim(getNumberInRange(height));
|
let h = lim(getNumberInRange(height));
|
||||||
|
|
||||||
do {
|
do {
|
||||||
|
|
@ -269,28 +325,31 @@
|
||||||
const y = getPointInRange(rangeY, graphHeight);
|
const y = getPointInRange(rangeY, graphHeight);
|
||||||
start = findGridCell(x, y);
|
start = findGridCell(x, y);
|
||||||
limit++;
|
limit++;
|
||||||
} while (cells.h[start] < 20 && limit < 50)
|
} while (cells.h[start] < 20 && limit < 50);
|
||||||
|
|
||||||
const queue = [start];
|
const queue = [start];
|
||||||
while (queue.length) {
|
while (queue.length) {
|
||||||
const q = queue.shift();
|
const q = queue.shift();
|
||||||
h = h ** getBlobPower() * (Math.random() * .2 + .9);
|
h = h ** getBlobPower() * (Math.random() * 0.2 + 0.9);
|
||||||
if (h < 1) return;
|
if (h < 1) return;
|
||||||
|
|
||||||
cells.c[q].forEach(function (c, i) {
|
cells.c[q].forEach(function (c, i) {
|
||||||
if (used[c]) return;
|
if (used[c]) return;
|
||||||
cells.h[c] = lim(cells.h[c] - h * (Math.random() * .2 + .9));
|
cells.h[c] = lim(cells.h[c] - h * (Math.random() * 0.2 + 0.9));
|
||||||
used[c] = 1;
|
used[c] = 1;
|
||||||
queue.push(c);
|
queue.push(c);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const addRange = function (count, height, rangeX, rangeY) {
|
const addRange = function (count, height, rangeX, rangeY) {
|
||||||
count = getNumberInRange(count);
|
count = getNumberInRange(count);
|
||||||
const power = getLinePower();
|
const power = getLinePower();
|
||||||
while (count > 0) {addOneRange(); count--;}
|
while (count > 0) {
|
||||||
|
addOneRange();
|
||||||
|
count--;
|
||||||
|
}
|
||||||
|
|
||||||
function addOneRange() {
|
function addOneRange() {
|
||||||
const used = new Uint8Array(cells.h.length);
|
const used = new Uint8Array(cells.h.length);
|
||||||
|
|
@ -300,13 +359,16 @@
|
||||||
const startX = getPointInRange(rangeX, graphWidth);
|
const startX = getPointInRange(rangeX, graphWidth);
|
||||||
const startY = getPointInRange(rangeY, graphHeight);
|
const startY = getPointInRange(rangeY, graphHeight);
|
||||||
|
|
||||||
let dist = 0, limit = 0, endX, endY;
|
let dist = 0,
|
||||||
|
limit = 0,
|
||||||
|
endX,
|
||||||
|
endY;
|
||||||
do {
|
do {
|
||||||
endX = Math.random() * graphWidth * .8 + graphWidth * .1;
|
endX = Math.random() * graphWidth * 0.8 + graphWidth * 0.1;
|
||||||
endY = Math.random() * graphHeight * .7 + graphHeight * .15;
|
endY = Math.random() * graphHeight * 0.7 + graphHeight * 0.15;
|
||||||
dist = Math.abs(endY - startY) + Math.abs(endX - startX);
|
dist = Math.abs(endY - startY) + Math.abs(endX - startX);
|
||||||
limit++;
|
limit++;
|
||||||
} while ((dist < graphWidth / 8 || dist > graphWidth / 3) && limit < 50)
|
} while ((dist < graphWidth / 8 || dist > graphWidth / 3) && limit < 50);
|
||||||
|
|
||||||
let range = getRange(findGridCell(startX, startY), findGridCell(endX, endY));
|
let range = getRange(findGridCell(startX, startY), findGridCell(endX, endY));
|
||||||
|
|
||||||
|
|
@ -320,8 +382,11 @@
|
||||||
cells.c[cur].forEach(function (e) {
|
cells.c[cur].forEach(function (e) {
|
||||||
if (used[e]) return;
|
if (used[e]) return;
|
||||||
let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
|
let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
|
||||||
if (Math.random() > .85) diff = diff / 2;
|
if (Math.random() > 0.85) diff = diff / 2;
|
||||||
if (diff < min) {min = diff; cur = e;}
|
if (diff < min) {
|
||||||
|
min = diff;
|
||||||
|
cur = e;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
if (min === Infinity) return range;
|
if (min === Infinity) return range;
|
||||||
range.push(cur);
|
range.push(cur);
|
||||||
|
|
@ -332,18 +397,22 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// add height to ridge and cells around
|
// add height to ridge and cells around
|
||||||
let queue = range.slice(), i = 0;
|
let queue = range.slice(),
|
||||||
|
i = 0;
|
||||||
while (queue.length) {
|
while (queue.length) {
|
||||||
const frontier = queue.slice();
|
const frontier = queue.slice();
|
||||||
queue = [], i++;
|
(queue = []), i++;
|
||||||
frontier.forEach(i => {
|
frontier.forEach(i => {
|
||||||
cells.h[i] = lim(cells.h[i] + h * (Math.random() * .3 + .85));
|
cells.h[i] = lim(cells.h[i] + h * (Math.random() * 0.3 + 0.85));
|
||||||
});
|
});
|
||||||
h = h ** power - 1;
|
h = h ** power - 1;
|
||||||
if (h < 2) break;
|
if (h < 2) break;
|
||||||
frontier.forEach(f => {
|
frontier.forEach(f => {
|
||||||
cells.c[f].forEach(i => {
|
cells.c[f].forEach(i => {
|
||||||
if (!used[i]) {queue.push(i); used[i] = 1;}
|
if (!used[i]) {
|
||||||
|
queue.push(i);
|
||||||
|
used[i] = 1;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -358,35 +427,43 @@
|
||||||
cur = min;
|
cur = min;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const addTrough = function (count, height, rangeX, rangeY) {
|
const addTrough = function (count, height, rangeX, rangeY) {
|
||||||
count = getNumberInRange(count);
|
count = getNumberInRange(count);
|
||||||
const power = getLinePower();
|
const power = getLinePower();
|
||||||
while (count > 0) {addOneTrough(); count--;}
|
while (count > 0) {
|
||||||
|
addOneTrough();
|
||||||
|
count--;
|
||||||
|
}
|
||||||
|
|
||||||
function addOneTrough() {
|
function addOneTrough() {
|
||||||
const used = new Uint8Array(cells.h.length);
|
const used = new Uint8Array(cells.h.length);
|
||||||
let h = lim(getNumberInRange(height));
|
let h = lim(getNumberInRange(height));
|
||||||
|
|
||||||
// find start and end points
|
// find start and end points
|
||||||
let limit = 0, startX, startY, start, dist = 0, endX, endY;
|
let limit = 0,
|
||||||
|
startX,
|
||||||
|
startY,
|
||||||
|
start,
|
||||||
|
dist = 0,
|
||||||
|
endX,
|
||||||
|
endY;
|
||||||
do {
|
do {
|
||||||
startX = getPointInRange(rangeX, graphWidth);
|
startX = getPointInRange(rangeX, graphWidth);
|
||||||
startY = getPointInRange(rangeY, graphHeight);
|
startY = getPointInRange(rangeY, graphHeight);
|
||||||
start = findGridCell(startX, startY);
|
start = findGridCell(startX, startY);
|
||||||
limit++;
|
limit++;
|
||||||
} while (cells.h[start] < 20 && limit < 50)
|
} while (cells.h[start] < 20 && limit < 50);
|
||||||
|
|
||||||
limit = 0;
|
limit = 0;
|
||||||
do {
|
do {
|
||||||
endX = Math.random() * graphWidth * .8 + graphWidth * .1;
|
endX = Math.random() * graphWidth * 0.8 + graphWidth * 0.1;
|
||||||
endY = Math.random() * graphHeight * .7 + graphHeight * .15;
|
endY = Math.random() * graphHeight * 0.7 + graphHeight * 0.15;
|
||||||
dist = Math.abs(endY - startY) + Math.abs(endX - startX);
|
dist = Math.abs(endY - startY) + Math.abs(endX - startX);
|
||||||
limit++;
|
limit++;
|
||||||
} while ((dist < graphWidth / 8 || dist > graphWidth / 2) && limit < 50)
|
} while ((dist < graphWidth / 8 || dist > graphWidth / 2) && limit < 50);
|
||||||
|
|
||||||
let range = getRange(start, findGridCell(endX, endY));
|
let range = getRange(start, findGridCell(endX, endY));
|
||||||
|
|
||||||
|
|
@ -400,8 +477,11 @@
|
||||||
cells.c[cur].forEach(function (e) {
|
cells.c[cur].forEach(function (e) {
|
||||||
if (used[e]) return;
|
if (used[e]) return;
|
||||||
let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
|
let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
|
||||||
if (Math.random() > .8) diff = diff / 2;
|
if (Math.random() > 0.8) diff = diff / 2;
|
||||||
if (diff < min) {min = diff; cur = e;}
|
if (diff < min) {
|
||||||
|
min = diff;
|
||||||
|
cur = e;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
if (min === Infinity) return range;
|
if (min === Infinity) return range;
|
||||||
range.push(cur);
|
range.push(cur);
|
||||||
|
|
@ -412,18 +492,22 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// add height to ridge and cells around
|
// add height to ridge and cells around
|
||||||
let queue = range.slice(), i = 0;
|
let queue = range.slice(),
|
||||||
|
i = 0;
|
||||||
while (queue.length) {
|
while (queue.length) {
|
||||||
const frontier = queue.slice();
|
const frontier = queue.slice();
|
||||||
queue = [], i++;
|
(queue = []), i++;
|
||||||
frontier.forEach(i => {
|
frontier.forEach(i => {
|
||||||
cells.h[i] = lim(cells.h[i] - h * (Math.random() * .3 + .85));
|
cells.h[i] = lim(cells.h[i] - h * (Math.random() * 0.3 + 0.85));
|
||||||
});
|
});
|
||||||
h = h ** power - 1;
|
h = h ** power - 1;
|
||||||
if (h < 2) break;
|
if (h < 2) break;
|
||||||
frontier.forEach(f => {
|
frontier.forEach(f => {
|
||||||
cells.c[f].forEach(i => {
|
cells.c[f].forEach(i => {
|
||||||
if (!used[i]) {queue.push(i); used[i] = 1;}
|
if (!used[i]) {
|
||||||
|
queue.push(i);
|
||||||
|
used[i] = 1;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -438,21 +522,21 @@
|
||||||
cur = min;
|
cur = min;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const addStrait = function (width, direction = "vertical") {
|
const addStrait = function (width, direction = "vertical") {
|
||||||
width = Math.min(getNumberInRange(width), grid.cellsX / 3);
|
width = Math.min(getNumberInRange(width), grid.cellsX / 3);
|
||||||
if (width < 1 && P(width)) return;
|
if (width < 1 && P(width)) return;
|
||||||
const used = new Uint8Array(cells.h.length);
|
const used = new Uint8Array(cells.h.length);
|
||||||
const vert = direction === "vertical";
|
const vert = direction === "vertical";
|
||||||
const startX = vert ? Math.floor(Math.random() * graphWidth * .4 + graphWidth * .3) : 5;
|
const startX = vert ? Math.floor(Math.random() * graphWidth * 0.4 + graphWidth * 0.3) : 5;
|
||||||
const startY = vert ? 5 : Math.floor(Math.random() * graphHeight * .4 + graphHeight * .3);
|
const startY = vert ? 5 : Math.floor(Math.random() * graphHeight * 0.4 + graphHeight * 0.3);
|
||||||
const endX = vert ? Math.floor((graphWidth - startX) - (graphWidth * .1) + (Math.random() * graphWidth * .2)) : graphWidth - 5;
|
const endX = vert ? Math.floor(graphWidth - startX - graphWidth * 0.1 + Math.random() * graphWidth * 0.2) : graphWidth - 5;
|
||||||
const endY = vert ? graphHeight - 5 : Math.floor((graphHeight - startY) - (graphHeight * .1) + (Math.random() * graphHeight * .2));
|
const endY = vert ? graphHeight - 5 : Math.floor(graphHeight - startY - graphHeight * 0.1 + Math.random() * graphHeight * 0.2);
|
||||||
|
|
||||||
const start = findGridCell(startX, startY), end = findGridCell(endX, endY);
|
const start = findGridCell(startX, startY),
|
||||||
|
end = findGridCell(endX, endY);
|
||||||
let range = getRange(start, end);
|
let range = getRange(start, end);
|
||||||
const query = [];
|
const query = [];
|
||||||
|
|
||||||
|
|
@ -464,7 +548,10 @@
|
||||||
cells.c[cur].forEach(function (e) {
|
cells.c[cur].forEach(function (e) {
|
||||||
let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
|
let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
|
||||||
if (Math.random() > 0.8) diff = diff / 2;
|
if (Math.random() > 0.8) diff = diff / 2;
|
||||||
if (diff < min) {min = diff; cur = e;}
|
if (diff < min) {
|
||||||
|
min = diff;
|
||||||
|
cur = e;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
range.push(cur);
|
range.push(cur);
|
||||||
}
|
}
|
||||||
|
|
@ -472,10 +559,10 @@
|
||||||
return range;
|
return range;
|
||||||
}
|
}
|
||||||
|
|
||||||
const step = .1 / width;
|
const step = 0.1 / width;
|
||||||
|
|
||||||
while (width > 0) {
|
while (width > 0) {
|
||||||
const exp = .9 - step * width;
|
const exp = 0.9 - step * width;
|
||||||
range.forEach(function (r) {
|
range.forEach(function (r) {
|
||||||
cells.c[r].forEach(function (e) {
|
cells.c[r].forEach(function (e) {
|
||||||
if (used[e]) return;
|
if (used[e]) return;
|
||||||
|
|
@ -489,12 +576,12 @@
|
||||||
|
|
||||||
width--;
|
width--;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const modify = function (range, add, mult, power) {
|
const modify = function (range, add, mult, power) {
|
||||||
const min = range === "land" ? 20 : range === "all" ? 0 : +range.split("-")[0];
|
const min = range === "land" ? 20 : range === "all" ? 0 : +range.split("-")[0];
|
||||||
const max = range === "land" || range === "all" ? 100 : +range.split("-")[1];
|
const max = range === "land" || range === "all" ? 100 : +range.split("-")[1];
|
||||||
grid.cells.h = grid.cells.h.map(h => h >= min && h <= max ? mod(h) : h);
|
grid.cells.h = grid.cells.h.map(h => (h >= min && h <= max ? mod(h) : h));
|
||||||
|
|
||||||
function mod(v) {
|
function mod(v) {
|
||||||
if (add) v = min === 20 ? Math.max(v + add, 20) : v + add;
|
if (add) v = min === 20 ? Math.max(v + add, 20) : v + add;
|
||||||
|
|
@ -502,7 +589,7 @@
|
||||||
if (power) v = min === 20 ? (v - 20) ** power + 20 : v ** power;
|
if (power) v = min === 20 ? (v - 20) ** power + 20 : v ** power;
|
||||||
return lim(v);
|
return lim(v);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const smooth = function (fr = 2, add = 0) {
|
const smooth = function (fr = 2, add = 0) {
|
||||||
cells.h = cells.h.map((h, i) => {
|
cells.h = cells.h.map((h, i) => {
|
||||||
|
|
@ -510,15 +597,18 @@
|
||||||
cells.c[i].forEach(c => a.push(cells.h[c]));
|
cells.c[i].forEach(c => a.push(cells.h[c]));
|
||||||
return lim((h * (fr - 1) + d3.mean(a) + add) / fr);
|
return lim((h * (fr - 1) + d3.mean(a) + add) / fr);
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
function getPointInRange(range, length) {
|
function getPointInRange(range, length) {
|
||||||
if (typeof range !== "string") {ERROR && console.error("Range should be a string"); return;}
|
if (typeof range !== "string") {
|
||||||
|
ERROR && console.error("Range should be a string");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const min = range.split("-")[0] / 100 || 0;
|
const min = range.split("-")[0] / 100 || 0;
|
||||||
const max = range.split("-")[1]/100 || 100;
|
const max = range.split("-")[1] / 100 || min;
|
||||||
return rand(min * length, max * length);
|
return rand(min * length, max * length);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {generate, addHill, addRange, addTrough, addStrait, addPit, smooth, modify};
|
return {generate, addHill, addRange, addTrough, addStrait, addPit, smooth, modify};
|
||||||
|
});
|
||||||
})));
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
(function (global, factory) {
|
(function (global, factory) {
|
||||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
typeof exports === "object" && typeof module !== "undefined" ? (module.exports = factory()) : typeof define === "function" && define.amd ? define(factory) : (global.Lakes = factory());
|
||||||
typeof define === 'function' && define.amd ? define(factory) :
|
})(this, function () {
|
||||||
(global.Lakes = factory());
|
"use strict";
|
||||||
}(this, (function () {'use strict';
|
|
||||||
|
|
||||||
const setClimateData = function (h) {
|
const setClimateData = function (h) {
|
||||||
const cells = pack.cells;
|
const cells = pack.cells;
|
||||||
|
|
@ -17,17 +16,81 @@ const setClimateData = function(h) {
|
||||||
// temperature and evaporation to detect closed lakes
|
// temperature and evaporation to detect closed lakes
|
||||||
f.temp = f.cells < 6 ? grid.cells.temp[cells.g[f.firstCell]] : rn(d3.mean(f.shoreline.map(c => grid.cells.temp[cells.g[c]])), 1);
|
f.temp = f.cells < 6 ? grid.cells.temp[cells.g[f.firstCell]] : rn(d3.mean(f.shoreline.map(c => grid.cells.temp[cells.g[c]])), 1);
|
||||||
const height = (f.height - 18) ** heightExponentInput.value; // height in meters
|
const height = (f.height - 18) ** heightExponentInput.value; // height in meters
|
||||||
const evaporation = (700 * (f.temp + .006 * height) / 50 + 75) / (80 - f.temp); // based on Penman formula, [1-11]
|
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);
|
f.evaporation = rn(evaporation * f.cells);
|
||||||
|
|
||||||
|
// no outlet for lakes in depressed areas
|
||||||
|
if (f.closed) return;
|
||||||
|
|
||||||
// lake outlet cell
|
// lake outlet cell
|
||||||
f.outCell = f.shoreline[d3.scan(f.shoreline, (a, b) => h[a] - h[b])];
|
f.outCell = f.shoreline[d3.scan(f.shoreline, (a, b) => h[a] - h[b])];
|
||||||
lakeOutCells[f.outCell] = f.i;
|
lakeOutCells[f.outCell] = f.i;
|
||||||
});
|
});
|
||||||
|
|
||||||
return lakeOutCells;
|
return lakeOutCells;
|
||||||
|
};
|
||||||
|
|
||||||
|
// get array of land cells aroound lake
|
||||||
|
const getShoreline = function (lake) {
|
||||||
|
const uniqueCells = new Set();
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let deep = true;
|
||||||
|
const threshold = f.height + ELEVATION_LIMIT;
|
||||||
|
const queue = [min];
|
||||||
|
const checked = [];
|
||||||
|
checked[min] = true;
|
||||||
|
|
||||||
|
// check if elevated lake can potentially pour to another water body
|
||||||
|
while (deep && queue.length) {
|
||||||
|
const q = queue.pop();
|
||||||
|
|
||||||
|
for (const n of cells.c[q]) {
|
||||||
|
if (checked[n]) continue;
|
||||||
|
if (h[n] >= threshold) continue;
|
||||||
|
|
||||||
|
if (h[n] < 20) {
|
||||||
|
const nFeature = pack.features[cells.f[n]];
|
||||||
|
if (nFeature.type === "ocean" || f.height > nFeature.height) {
|
||||||
|
deep = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checked[n] = true;
|
||||||
|
queue.push(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f.closed = deep;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
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;
|
||||||
|
|
@ -35,7 +98,8 @@ const cleanupLakeData = function() {
|
||||||
delete feature.enteringFlux;
|
delete feature.enteringFlux;
|
||||||
delete feature.shoreline;
|
delete feature.shoreline;
|
||||||
delete feature.outCell;
|
delete feature.outCell;
|
||||||
feature.height = rn(feature.height);
|
delete feature.closed;
|
||||||
|
feature.height = rn(feature.height, 3);
|
||||||
|
|
||||||
const inlets = feature.inlets?.filter(r => pack.rivers.find(river => river.i === r));
|
const inlets = feature.inlets?.filter(r => pack.rivers.find(river => river.i === r));
|
||||||
if (!inlets || !inlets.length) delete feature.inlets;
|
if (!inlets || !inlets.length) delete feature.inlets;
|
||||||
|
|
@ -44,7 +108,7 @@ const cleanupLakeData = function() {
|
||||||
const outlet = feature.outlet && pack.rivers.find(river => river.i === feature.outlet);
|
const outlet = feature.outlet && pack.rivers.find(river => river.i === feature.outlet);
|
||||||
if (!outlet) delete feature.outlet;
|
if (!outlet) delete feature.outlet;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const defineGroup = function () {
|
const defineGroup = function () {
|
||||||
for (const feature of pack.features) {
|
for (const feature of pack.features) {
|
||||||
|
|
@ -55,7 +119,7 @@ const defineGroup = function() {
|
||||||
feature.group = getGroup(feature);
|
feature.group = getGroup(feature);
|
||||||
document.getElementById(feature.group).appendChild(lakeEl);
|
document.getElementById(feature.group).appendChild(lakeEl);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const generateName = function () {
|
const generateName = function () {
|
||||||
Math.random = aleaPRNG(seed);
|
Math.random = aleaPRNG(seed);
|
||||||
|
|
@ -63,21 +127,21 @@ const generateName = function() {
|
||||||
if (feature.type !== "lake") continue;
|
if (feature.type !== "lake") continue;
|
||||||
feature.name = getName(feature);
|
feature.name = getName(feature);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const getName = function (feature) {
|
const getName = function (feature) {
|
||||||
const landCell = pack.cells.c[feature.firstCell].find(c => pack.cells.h[c] >= 20);
|
const landCell = pack.cells.c[feature.firstCell].find(c => pack.cells.h[c] >= 20);
|
||||||
const culture = pack.cells.culture[landCell];
|
const culture = pack.cells.culture[landCell];
|
||||||
return Names.getCulture(culture);
|
return Names.getCulture(culture);
|
||||||
}
|
};
|
||||||
|
|
||||||
function getGroup(feature) {
|
function getGroup(feature) {
|
||||||
if (feature.temp < -3) return "frozen";
|
if (feature.temp < -3) return "frozen";
|
||||||
if (feature.height > 60 && feature.cells < 10 && feature.firstCell % 5 === 0) return "lava";
|
if (feature.height > 60 && feature.cells < 10 && feature.firstCell % 10 === 0) return "lava";
|
||||||
|
|
||||||
if (!feature.inlets && !feature.outlet) {
|
if (!feature.inlets && !feature.outlet) {
|
||||||
if (feature.evaporation / 2 > feature.flux) return "dry";
|
if (feature.evaporation / 2 > feature.flux) return "dry";
|
||||||
if (feature.cells < 3 && feature.firstCell % 5 === 0) return "sinkhole";
|
if (feature.cells < 3 && feature.firstCell % 10 === 0) return "sinkhole";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!feature.outlet && feature.evaporation > feature.flux) return "salt";
|
if (!feature.outlet && feature.evaporation > feature.flux) return "salt";
|
||||||
|
|
@ -85,6 +149,5 @@ function getGroup(feature) {
|
||||||
return "freshwater";
|
return "freshwater";
|
||||||
}
|
}
|
||||||
|
|
||||||
return {setClimateData, cleanupLakeData, defineGroup, generateName, getName};
|
return {setClimateData, cleanupLakeData, prepareLakeData, defineGroup, generateName, getName, getShoreline};
|
||||||
|
});
|
||||||
})));
|
|
||||||
|
|
|
||||||
831
modules/load.js
Normal file
831
modules/load.js
Normal file
|
|
@ -0,0 +1,831 @@
|
||||||
|
// Functions to save and load the map
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function quickLoad() {
|
||||||
|
ldb.get("lastMap", blob => {
|
||||||
|
if (blob) {
|
||||||
|
loadMapPrompt(blob);
|
||||||
|
} else {
|
||||||
|
tip("No map stored. Save map to storage first", true, "error", 2000);
|
||||||
|
ERROR && console.error("No map stored");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadMapPrompt(blob) {
|
||||||
|
const workingTime = (Date.now() - last(mapHistory).created) / 60000; // minutes
|
||||||
|
if (workingTime < 5) {
|
||||||
|
loadLastSavedMap();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
alertMessage.innerHTML = `Are you sure you want to load saved map?<br>
|
||||||
|
All unsaved changes made to the current map will be lost`;
|
||||||
|
$("#alert").dialog({
|
||||||
|
resizable: false,
|
||||||
|
title: "Load saved map",
|
||||||
|
buttons: {
|
||||||
|
Cancel: function () {
|
||||||
|
$(this).dialog("close");
|
||||||
|
},
|
||||||
|
Load: function () {
|
||||||
|
loadLastSavedMap();
|
||||||
|
$(this).dialog("close");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadLastSavedMap() {
|
||||||
|
WARN && console.warn("Load last saved map");
|
||||||
|
try {
|
||||||
|
uploadMap(blob);
|
||||||
|
} catch (error) {
|
||||||
|
ERROR && console.error(error);
|
||||||
|
tip("Cannot load last saved map", true, "error", 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadMap(file, callback) {
|
||||||
|
uploadMap.timeStart = performance.now();
|
||||||
|
|
||||||
|
const fileReader = new FileReader();
|
||||||
|
fileReader.onload = function (fileLoadedEvent) {
|
||||||
|
if (callback) callback();
|
||||||
|
document.getElementById("coas").innerHTML = ""; // remove auto-generated emblems
|
||||||
|
|
||||||
|
const dataLoaded = fileLoadedEvent.target.result;
|
||||||
|
const data = dataLoaded.split("\r\n");
|
||||||
|
|
||||||
|
const mapVersion = data[0].split("|")[0] || data[0];
|
||||||
|
if (mapVersion === version) {
|
||||||
|
parseLoadedData(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const archive = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "archived version");
|
||||||
|
const parsed = parseFloat(mapVersion);
|
||||||
|
let message = "",
|
||||||
|
load = false;
|
||||||
|
if (isNaN(parsed) || data.length < 26 || !data[5]) {
|
||||||
|
message = `The file you are trying to load is outdated or not a valid .map file.
|
||||||
|
<br>Please try to open it using an ${archive}`;
|
||||||
|
} else if (parsed < 0.7) {
|
||||||
|
message = `The map version you are trying to load (${mapVersion}) is too old and cannot be updated to the current version.
|
||||||
|
<br>Please keep using an ${archive}`;
|
||||||
|
} else {
|
||||||
|
load = true;
|
||||||
|
message = `The map version (${mapVersion}) does not match the Generator version (${version}).
|
||||||
|
<br>Click OK to get map <b>auto-updated</b>. In case of issues please keep using an ${archive} of the Generator`;
|
||||||
|
}
|
||||||
|
alertMessage.innerHTML = message;
|
||||||
|
$("#alert").dialog({
|
||||||
|
title: "Version conflict",
|
||||||
|
width: "38em",
|
||||||
|
buttons: {
|
||||||
|
OK: function () {
|
||||||
|
$(this).dialog("close");
|
||||||
|
if (load) parseLoadedData(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
fileReader.readAsText(file, "UTF-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLoadedData(data) {
|
||||||
|
try {
|
||||||
|
// exit customization
|
||||||
|
if (window.closeDialogs) closeDialogs();
|
||||||
|
customization = 0;
|
||||||
|
if (customizationMenu.offsetParent) styleTab.click();
|
||||||
|
|
||||||
|
const reliefIcons = document.getElementById("defs-relief").innerHTML; // save relief icons
|
||||||
|
const hatching = document.getElementById("hatching").cloneNode(true); // save hatching
|
||||||
|
|
||||||
|
void (function parseParameters() {
|
||||||
|
const params = data[0].split("|");
|
||||||
|
if (params[3]) {
|
||||||
|
seed = params[3];
|
||||||
|
optionsSeed.value = seed;
|
||||||
|
}
|
||||||
|
if (params[4]) graphWidth = +params[4];
|
||||||
|
if (params[5]) graphHeight = +params[5];
|
||||||
|
mapId = params[6] ? +params[6] : Date.now();
|
||||||
|
})();
|
||||||
|
|
||||||
|
INFO && console.group("Loaded Map " + seed);
|
||||||
|
|
||||||
|
void (function parseSettings() {
|
||||||
|
const settings = data[1].split("|");
|
||||||
|
if (settings[0]) applyOption(distanceUnitInput, settings[0]);
|
||||||
|
if (settings[1]) distanceScaleInput.value = distanceScaleOutput.value = settings[1];
|
||||||
|
if (settings[2]) areaUnit.value = settings[2];
|
||||||
|
if (settings[3]) applyOption(heightUnit, settings[3]);
|
||||||
|
if (settings[4]) heightExponentInput.value = heightExponentOutput.value = settings[4];
|
||||||
|
if (settings[5]) temperatureScale.value = settings[5];
|
||||||
|
if (settings[6]) barSizeInput.value = barSizeOutput.value = settings[6];
|
||||||
|
if (settings[7] !== undefined) barLabel.value = settings[7];
|
||||||
|
if (settings[8] !== undefined) barBackOpacity.value = settings[8];
|
||||||
|
if (settings[9]) barBackColor.value = settings[9];
|
||||||
|
if (settings[10]) barPosX.value = settings[10];
|
||||||
|
if (settings[11]) barPosY.value = settings[11];
|
||||||
|
if (settings[12]) populationRate = populationRateInput.value = populationRateOutput.value = settings[12];
|
||||||
|
if (settings[13]) urbanization = urbanizationInput.value = urbanizationOutput.value = settings[13];
|
||||||
|
if (settings[14]) mapSizeInput.value = mapSizeOutput.value = Math.max(Math.min(settings[14], 100), 1);
|
||||||
|
if (settings[15]) latitudeInput.value = latitudeOutput.value = Math.max(Math.min(settings[15], 100), 0);
|
||||||
|
if (settings[16]) temperatureEquatorInput.value = temperatureEquatorOutput.value = settings[16];
|
||||||
|
if (settings[17]) temperaturePoleInput.value = temperaturePoleOutput.value = settings[17];
|
||||||
|
if (settings[18]) precInput.value = precOutput.value = settings[18];
|
||||||
|
if (settings[19]) options = JSON.parse(settings[19]);
|
||||||
|
if (settings[20]) mapName.value = settings[20];
|
||||||
|
if (settings[21]) hideLabels.checked = +settings[21];
|
||||||
|
})();
|
||||||
|
|
||||||
|
void (function parseConfiguration() {
|
||||||
|
if (data[2]) mapCoordinates = JSON.parse(data[2]);
|
||||||
|
if (data[4]) notes = JSON.parse(data[4]);
|
||||||
|
if (data[33]) rulers.fromString(data[33]);
|
||||||
|
|
||||||
|
const biomes = data[3].split("|");
|
||||||
|
biomesData = applyDefaultBiomesSystem();
|
||||||
|
biomesData.color = biomes[0].split(",");
|
||||||
|
biomesData.habitability = biomes[1].split(",").map(h => +h);
|
||||||
|
biomesData.name = biomes[2].split(",");
|
||||||
|
|
||||||
|
// push custom biomes if any
|
||||||
|
for (let i = biomesData.i.length; i < biomesData.name.length; i++) {
|
||||||
|
biomesData.i.push(biomesData.i.length);
|
||||||
|
biomesData.iconsDensity.push(0);
|
||||||
|
biomesData.icons.push([]);
|
||||||
|
biomesData.cost.push(50);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
void (function replaceSVG() {
|
||||||
|
svg.remove();
|
||||||
|
document.body.insertAdjacentHTML("afterbegin", data[5]);
|
||||||
|
})();
|
||||||
|
|
||||||
|
void (function redefineElements() {
|
||||||
|
svg = d3.select("#map");
|
||||||
|
defs = svg.select("#deftemp");
|
||||||
|
viewbox = svg.select("#viewbox");
|
||||||
|
scaleBar = svg.select("#scaleBar");
|
||||||
|
legend = svg.select("#legend");
|
||||||
|
ocean = viewbox.select("#ocean");
|
||||||
|
oceanLayers = ocean.select("#oceanLayers");
|
||||||
|
oceanPattern = ocean.select("#oceanPattern");
|
||||||
|
lakes = viewbox.select("#lakes");
|
||||||
|
landmass = viewbox.select("#landmass");
|
||||||
|
texture = viewbox.select("#texture");
|
||||||
|
terrs = viewbox.select("#terrs");
|
||||||
|
biomes = viewbox.select("#biomes");
|
||||||
|
ice = viewbox.select("#ice");
|
||||||
|
cells = viewbox.select("#cells");
|
||||||
|
gridOverlay = viewbox.select("#gridOverlay");
|
||||||
|
coordinates = viewbox.select("#coordinates");
|
||||||
|
compass = viewbox.select("#compass");
|
||||||
|
rivers = viewbox.select("#rivers");
|
||||||
|
terrain = viewbox.select("#terrain");
|
||||||
|
relig = viewbox.select("#relig");
|
||||||
|
cults = viewbox.select("#cults");
|
||||||
|
regions = viewbox.select("#regions");
|
||||||
|
statesBody = regions.select("#statesBody");
|
||||||
|
statesHalo = regions.select("#statesHalo");
|
||||||
|
provs = viewbox.select("#provs");
|
||||||
|
zones = viewbox.select("#zones");
|
||||||
|
borders = viewbox.select("#borders");
|
||||||
|
stateBorders = borders.select("#stateBorders");
|
||||||
|
provinceBorders = borders.select("#provinceBorders");
|
||||||
|
routes = viewbox.select("#routes");
|
||||||
|
roads = routes.select("#roads");
|
||||||
|
trails = routes.select("#trails");
|
||||||
|
searoutes = routes.select("#searoutes");
|
||||||
|
temperature = viewbox.select("#temperature");
|
||||||
|
coastline = viewbox.select("#coastline");
|
||||||
|
prec = viewbox.select("#prec");
|
||||||
|
population = viewbox.select("#population");
|
||||||
|
emblems = viewbox.select("#emblems");
|
||||||
|
labels = viewbox.select("#labels");
|
||||||
|
icons = viewbox.select("#icons");
|
||||||
|
burgIcons = icons.select("#burgIcons");
|
||||||
|
anchors = icons.select("#anchors");
|
||||||
|
armies = viewbox.select("#armies");
|
||||||
|
markers = viewbox.select("#markers");
|
||||||
|
ruler = viewbox.select("#ruler");
|
||||||
|
fogging = viewbox.select("#fogging");
|
||||||
|
debug = viewbox.select("#debug");
|
||||||
|
burgLabels = labels.select("#burgLabels");
|
||||||
|
})();
|
||||||
|
|
||||||
|
void (function parseGridData() {
|
||||||
|
grid = JSON.parse(data[6]);
|
||||||
|
calculateVoronoi(grid, grid.points);
|
||||||
|
grid.cells.h = Uint8Array.from(data[7].split(","));
|
||||||
|
grid.cells.prec = Uint8Array.from(data[8].split(","));
|
||||||
|
grid.cells.f = Uint16Array.from(data[9].split(","));
|
||||||
|
grid.cells.t = Int8Array.from(data[10].split(","));
|
||||||
|
grid.cells.temp = Int8Array.from(data[11].split(","));
|
||||||
|
})();
|
||||||
|
|
||||||
|
void (function parsePackData() {
|
||||||
|
pack = {};
|
||||||
|
reGraph();
|
||||||
|
reMarkFeatures();
|
||||||
|
pack.features = JSON.parse(data[12]);
|
||||||
|
pack.cultures = JSON.parse(data[13]);
|
||||||
|
pack.states = JSON.parse(data[14]);
|
||||||
|
pack.burgs = JSON.parse(data[15]);
|
||||||
|
pack.religions = data[29] ? JSON.parse(data[29]) : [{i: 0, name: "No religion"}];
|
||||||
|
pack.provinces = data[30] ? JSON.parse(data[30]) : [0];
|
||||||
|
pack.rivers = data[32] ? JSON.parse(data[32]) : [];
|
||||||
|
|
||||||
|
const cells = pack.cells;
|
||||||
|
cells.biome = Uint8Array.from(data[16].split(","));
|
||||||
|
cells.burg = Uint16Array.from(data[17].split(","));
|
||||||
|
cells.conf = Uint8Array.from(data[18].split(","));
|
||||||
|
cells.culture = Uint16Array.from(data[19].split(","));
|
||||||
|
cells.fl = Uint16Array.from(data[20].split(","));
|
||||||
|
cells.pop = Float32Array.from(data[21].split(","));
|
||||||
|
cells.r = Uint16Array.from(data[22].split(","));
|
||||||
|
cells.road = Uint16Array.from(data[23].split(","));
|
||||||
|
cells.s = Uint16Array.from(data[24].split(","));
|
||||||
|
cells.state = Uint16Array.from(data[25].split(","));
|
||||||
|
cells.religion = data[26] ? Uint16Array.from(data[26].split(",")) : new Uint16Array(cells.i.length);
|
||||||
|
cells.province = data[27] ? Uint16Array.from(data[27].split(",")) : new Uint16Array(cells.i.length);
|
||||||
|
cells.crossroad = data[28] ? Uint16Array.from(data[28].split(",")) : new Uint16Array(cells.i.length);
|
||||||
|
|
||||||
|
if (data[31]) {
|
||||||
|
const namesDL = data[31].split("/");
|
||||||
|
namesDL.forEach((d, i) => {
|
||||||
|
const e = d.split("|");
|
||||||
|
if (!e.length) return;
|
||||||
|
const b = e[5].split(",").length > 2 || !nameBases[i] ? e[5] : nameBases[i].b;
|
||||||
|
nameBases[i] = {name: e[0], min: e[1], max: e[2], d: e[3], m: e[4], b};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
const notHidden = selection => selection.node() && selection.style("display") !== "none";
|
||||||
|
const hasChildren = selection => selection.node()?.hasChildNodes();
|
||||||
|
const hasChild = (selection, selector) => selection.node()?.querySelector(selector);
|
||||||
|
const turnOn = el => document.getElementById(el).classList.remove("buttonoff");
|
||||||
|
|
||||||
|
void (function restoreLayersState() {
|
||||||
|
// turn all layers off
|
||||||
|
document
|
||||||
|
.getElementById("mapLayers")
|
||||||
|
.querySelectorAll("li")
|
||||||
|
.forEach(el => el.classList.add("buttonoff"));
|
||||||
|
|
||||||
|
// turn on active layers
|
||||||
|
if (notHidden(texture) && hasChild(texture, "image")) turnOn("toggleTexture");
|
||||||
|
if (hasChildren(terrs)) turnOn("toggleHeight");
|
||||||
|
if (hasChildren(biomes)) turnOn("toggleBiomes");
|
||||||
|
if (hasChildren(cells)) turnOn("toggleCells");
|
||||||
|
if (hasChildren(gridOverlay)) turnOn("toggleGrid");
|
||||||
|
if (hasChildren(coordinates)) turnOn("toggleCoordinates");
|
||||||
|
if (notHidden(compass) && hasChild(compass, "use")) turnOn("toggleCompass");
|
||||||
|
if (notHidden(rivers)) turnOn("toggleRivers");
|
||||||
|
if (notHidden(terrain) && hasChildren(terrain)) turnOn("toggleRelief");
|
||||||
|
if (hasChildren(relig)) turnOn("toggleReligions");
|
||||||
|
if (hasChildren(cults)) turnOn("toggleCultures");
|
||||||
|
if (hasChildren(statesBody)) turnOn("toggleStates");
|
||||||
|
if (hasChildren(provs)) turnOn("toggleProvinces");
|
||||||
|
if (hasChildren(zones) && notHidden(zones)) turnOn("toggleZones");
|
||||||
|
if (notHidden(borders) && hasChild(compass, "use")) turnOn("toggleBorders");
|
||||||
|
if (notHidden(routes) && hasChild(routes, "path")) turnOn("toggleRoutes");
|
||||||
|
if (hasChildren(temperature)) turnOn("toggleTemp");
|
||||||
|
if (hasChild(population, "line")) turnOn("togglePopulation");
|
||||||
|
if (hasChildren(ice)) turnOn("toggleIce");
|
||||||
|
if (hasChild(prec, "circle")) turnOn("togglePrec");
|
||||||
|
if (notHidden(emblems) && hasChild(emblems, "use")) turnOn("toggleEmblems");
|
||||||
|
if (notHidden(labels)) turnOn("toggleLabels");
|
||||||
|
if (notHidden(icons)) turnOn("toggleIcons");
|
||||||
|
if (hasChildren(armies) && notHidden(armies)) turnOn("toggleMilitary");
|
||||||
|
if (hasChildren(markers) && notHidden(markers)) turnOn("toggleMarkers");
|
||||||
|
if (notHidden(ruler)) turnOn("toggleRulers");
|
||||||
|
if (notHidden(scaleBar)) turnOn("toggleScaleBar");
|
||||||
|
|
||||||
|
getCurrentPreset();
|
||||||
|
})();
|
||||||
|
|
||||||
|
void (function restoreEvents() {
|
||||||
|
scaleBar.on("mousemove", () => tip("Click to open Units Editor")).on("click", () => editUnits());
|
||||||
|
legend.on("mousemove", () => tip("Drag to change the position. Click to hide the legend")).on("click", () => clearLegend());
|
||||||
|
})();
|
||||||
|
|
||||||
|
void (function resolveVersionConflicts() {
|
||||||
|
const version = parseFloat(data[0].split("|")[0]);
|
||||||
|
if (version < 0.9) {
|
||||||
|
// 0.9 has additional relief icons to be included into older maps
|
||||||
|
document.getElementById("defs-relief").innerHTML = reliefIcons;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version < 1) {
|
||||||
|
// 1.0 adds a new religions layer
|
||||||
|
relig = viewbox.insert("g", "#terrain").attr("id", "relig");
|
||||||
|
Religions.generate();
|
||||||
|
|
||||||
|
// 1.0 adds a legend box
|
||||||
|
legend = svg.append("g").attr("id", "legend");
|
||||||
|
legend.attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC").attr("font-size", 13).attr("data-size", 13).attr("data-x", 99).attr("data-y", 93).attr("stroke-width", 2.5).attr("stroke", "#812929").attr("stroke-dasharray", "0 4 10 4").attr("stroke-linecap", "round");
|
||||||
|
|
||||||
|
// 1.0 separated drawBorders fron drawStates()
|
||||||
|
stateBorders = borders.append("g").attr("id", "stateBorders");
|
||||||
|
provinceBorders = borders.append("g").attr("id", "provinceBorders");
|
||||||
|
borders.attr("opacity", null).attr("stroke", null).attr("stroke-width", null).attr("stroke-dasharray", null).attr("stroke-linecap", null).attr("filter", null);
|
||||||
|
stateBorders.attr("opacity", 0.8).attr("stroke", "#56566d").attr("stroke-width", 1).attr("stroke-dasharray", "2").attr("stroke-linecap", "butt");
|
||||||
|
provinceBorders.attr("opacity", 0.8).attr("stroke", "#56566d").attr("stroke-width", 0.5).attr("stroke-dasharray", "1").attr("stroke-linecap", "butt");
|
||||||
|
|
||||||
|
// 1.0 adds state relations, provinces, forms and full names
|
||||||
|
provs = viewbox.insert("g", "#borders").attr("id", "provs").attr("opacity", 0.6);
|
||||||
|
BurgsAndStates.collectStatistics();
|
||||||
|
BurgsAndStates.generateCampaigns();
|
||||||
|
BurgsAndStates.generateDiplomacy();
|
||||||
|
BurgsAndStates.defineStateForms();
|
||||||
|
drawStates();
|
||||||
|
BurgsAndStates.generateProvinces();
|
||||||
|
drawBorders();
|
||||||
|
if (!layerIsOn("toggleBorders")) $("#borders").fadeOut();
|
||||||
|
if (!layerIsOn("toggleStates")) regions.attr("display", "none").selectAll("path").remove();
|
||||||
|
|
||||||
|
// 1.0 adds hatching
|
||||||
|
document.getElementsByTagName("defs")[0].appendChild(hatching);
|
||||||
|
|
||||||
|
// 1.0 adds zones layer
|
||||||
|
zones = viewbox.insert("g", "#borders").attr("id", "zones").attr("display", "none");
|
||||||
|
zones.attr("opacity", 0.6).attr("stroke", null).attr("stroke-width", 0).attr("stroke-dasharray", null).attr("stroke-linecap", "butt");
|
||||||
|
addZones();
|
||||||
|
if (!markers.selectAll("*").size()) {
|
||||||
|
addMarkers();
|
||||||
|
turnButtonOn("toggleMarkers");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1.0 add fogging layer (state focus)
|
||||||
|
fogging = viewbox.insert("g", "#ruler").attr("id", "fogging-cont").attr("mask", "url(#fog)").append("g").attr("id", "fogging").style("display", "none");
|
||||||
|
fogging.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%");
|
||||||
|
defs.append("mask").attr("id", "fog").append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%").attr("fill", "white");
|
||||||
|
|
||||||
|
// 1.0 changes states opacity bask to regions level
|
||||||
|
if (statesBody.attr("opacity")) {
|
||||||
|
regions.attr("opacity", statesBody.attr("opacity"));
|
||||||
|
statesBody.attr("opacity", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1.0 changed labels to multi-lined
|
||||||
|
labels.selectAll("textPath").each(function () {
|
||||||
|
const text = this.textContent;
|
||||||
|
const shift = this.getComputedTextLength() / -1.5;
|
||||||
|
this.innerHTML = `<tspan x="${shift}">${text}</tspan>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1.0 added new biome - Wetland
|
||||||
|
biomesData.name.push("Wetland");
|
||||||
|
biomesData.color.push("#0b9131");
|
||||||
|
biomesData.habitability.push(12);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version < 1.1) {
|
||||||
|
// v 1.0 initial code had a bug with religion layer id
|
||||||
|
if (!relig.size()) relig = viewbox.insert("g", "#terrain").attr("id", "relig");
|
||||||
|
|
||||||
|
// v 1.0 initially has Sympathy status then relaced with Friendly
|
||||||
|
for (const s of pack.states) {
|
||||||
|
if (!s.diplomacy) continue;
|
||||||
|
s.diplomacy = s.diplomacy.map(r => (r === "Sympathy" ? "Friendly" : r));
|
||||||
|
}
|
||||||
|
|
||||||
|
// labels should be toggled via style attribute, so remove display attribute
|
||||||
|
labels.attr("display", null);
|
||||||
|
|
||||||
|
// v 1.0 added religions heirarchy tree
|
||||||
|
if (pack.religions[1] && !pack.religions[1].code) {
|
||||||
|
pack.religions
|
||||||
|
.filter(r => r.i)
|
||||||
|
.forEach(r => {
|
||||||
|
r.origin = 0;
|
||||||
|
r.code = r.name.slice(0, 2);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!document.getElementById("freshwater")) {
|
||||||
|
lakes.append("g").attr("id", "freshwater");
|
||||||
|
lakes.select("#freshwater").attr("opacity", 0.5).attr("fill", "#a6c1fd").attr("stroke", "#5f799d").attr("stroke-width", 0.7).attr("filter", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!document.getElementById("salt")) {
|
||||||
|
lakes.append("g").attr("id", "salt");
|
||||||
|
lakes.select("#salt").attr("opacity", 0.5).attr("fill", "#409b8a").attr("stroke", "#388985").attr("stroke-width", 0.7).attr("filter", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// v 1.1 added new lake and coast groups
|
||||||
|
if (!document.getElementById("sinkhole")) {
|
||||||
|
lakes.append("g").attr("id", "sinkhole");
|
||||||
|
lakes.append("g").attr("id", "frozen");
|
||||||
|
lakes.append("g").attr("id", "lava");
|
||||||
|
lakes.select("#sinkhole").attr("opacity", 1).attr("fill", "#5bc9fd").attr("stroke", "#53a3b0").attr("stroke-width", 0.7).attr("filter", null);
|
||||||
|
lakes.select("#frozen").attr("opacity", 0.95).attr("fill", "#cdd4e7").attr("stroke", "#cfe0eb").attr("stroke-width", 0).attr("filter", null);
|
||||||
|
lakes.select("#lava").attr("opacity", 0.7).attr("fill", "#90270d").attr("stroke", "#f93e0c").attr("stroke-width", 2).attr("filter", "url(#crumpled)");
|
||||||
|
|
||||||
|
coastline.append("g").attr("id", "sea_island");
|
||||||
|
coastline.append("g").attr("id", "lake_island");
|
||||||
|
coastline.select("#sea_island").attr("opacity", 0.5).attr("stroke", "#1f3846").attr("stroke-width", 0.7).attr("filter", "url(#dropShadow)");
|
||||||
|
coastline.select("#lake_island").attr("opacity", 1).attr("stroke", "#7c8eaf").attr("stroke-width", 0.35).attr("filter", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// v 1.1 features stores more data
|
||||||
|
defs.select("#land").selectAll("path").remove();
|
||||||
|
defs.select("#water").selectAll("path").remove();
|
||||||
|
coastline.selectAll("path").remove();
|
||||||
|
lakes.selectAll("path").remove();
|
||||||
|
drawCoastline();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version < 1.11) {
|
||||||
|
// v 1.11 added new attributes
|
||||||
|
terrs.attr("scheme", "bright").attr("terracing", 0).attr("skip", 5).attr("relax", 0).attr("curve", 0);
|
||||||
|
svg.select("#oceanic > *").attr("id", "oceanicPattern");
|
||||||
|
oceanLayers.attr("layers", "-6,-3,-1");
|
||||||
|
gridOverlay.attr("type", "pointyHex").attr("size", 10);
|
||||||
|
|
||||||
|
// v 1.11 added cultures heirarchy tree
|
||||||
|
if (pack.cultures[1] && !pack.cultures[1].code) {
|
||||||
|
pack.cultures
|
||||||
|
.filter(c => c.i)
|
||||||
|
.forEach(c => {
|
||||||
|
c.origin = 0;
|
||||||
|
c.code = c.name.slice(0, 2);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// v 1.11 had an issue with fogging being displayed on load
|
||||||
|
unfog();
|
||||||
|
|
||||||
|
// v 1.2 added new terrain attributes
|
||||||
|
if (!terrain.attr("set")) terrain.attr("set", "simple");
|
||||||
|
if (!terrain.attr("size")) terrain.attr("size", 1);
|
||||||
|
if (!terrain.attr("density")) terrain.attr("density", 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version < 1.21) {
|
||||||
|
// v 1.11 replaced "display" attribute by "display" style
|
||||||
|
viewbox.selectAll("g").each(function () {
|
||||||
|
if (this.hasAttribute("display")) {
|
||||||
|
this.removeAttribute("display");
|
||||||
|
this.style.display = "none";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// v 1.21 added rivers data to pack
|
||||||
|
pack.rivers = []; // rivers data
|
||||||
|
rivers.selectAll("path").each(function () {
|
||||||
|
const i = +this.id.slice(5);
|
||||||
|
const length = this.getTotalLength() / 2;
|
||||||
|
const s = this.getPointAtLength(length),
|
||||||
|
e = this.getPointAtLength(0);
|
||||||
|
const source = findCell(s.x, s.y),
|
||||||
|
mouth = findCell(e.x, e.y);
|
||||||
|
const name = Rivers.getName(mouth);
|
||||||
|
const type = length < 25 ? rw({Creek: 9, River: 3, Brook: 3, Stream: 1}) : "River";
|
||||||
|
pack.rivers.push({i, parent: 0, length, source, mouth, basin: i, name, type});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version < 1.22) {
|
||||||
|
// v 1.22 changed state neighbors from Set object to array
|
||||||
|
BurgsAndStates.collectStatistics();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version < 1.3) {
|
||||||
|
// v 1.3 added global options object
|
||||||
|
const winds = options.slice(); // previostly wind was saved in settings[19]
|
||||||
|
const year = rand(100, 2000);
|
||||||
|
const era = Names.getBaseShort(P(0.7) ? 1 : rand(nameBases.length)) + " Era";
|
||||||
|
const eraShort = era[0] + "E";
|
||||||
|
const military = Military.getDefaultOptions();
|
||||||
|
options = {winds, year, era, eraShort, military};
|
||||||
|
|
||||||
|
// v 1.3 added campaings data for all states
|
||||||
|
BurgsAndStates.generateCampaigns();
|
||||||
|
|
||||||
|
// v 1.3 added militry layer
|
||||||
|
armies = viewbox.insert("g", "#icons").attr("id", "armies");
|
||||||
|
armies.attr("opacity", 1).attr("fill-opacity", 1).attr("font-size", 6).attr("box-size", 3).attr("stroke", "#000").attr("stroke-width", 0.3);
|
||||||
|
turnButtonOn("toggleMilitary");
|
||||||
|
Military.generate();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version < 1.4) {
|
||||||
|
// v 1.35 added dry lakes
|
||||||
|
if (!lakes.select("#dry").size()) {
|
||||||
|
lakes.append("g").attr("id", "dry");
|
||||||
|
lakes.select("#dry").attr("opacity", 1).attr("fill", "#c9bfa7").attr("stroke", "#8e816f").attr("stroke-width", 0.7).attr("filter", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// v 1.4 added ice layer
|
||||||
|
ice = viewbox.insert("g", "#coastline").attr("id", "ice").style("display", "none");
|
||||||
|
ice.attr("opacity", null).attr("fill", "#e8f0f6").attr("stroke", "#e8f0f6").attr("stroke-width", 1).attr("filter", "url(#dropShadow05)");
|
||||||
|
drawIce();
|
||||||
|
|
||||||
|
// v 1.4 added icon and power attributes for units
|
||||||
|
for (const unit of options.military) {
|
||||||
|
if (!unit.icon) unit.icon = getUnitIcon(unit.type);
|
||||||
|
if (!unit.power) unit.power = unit.crew;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUnitIcon(type) {
|
||||||
|
if (type === "naval") return "🌊";
|
||||||
|
if (type === "ranged") return "🏹";
|
||||||
|
if (type === "mounted") return "🐴";
|
||||||
|
if (type === "machinery") return "💣";
|
||||||
|
if (type === "armored") return "🐢";
|
||||||
|
if (type === "aviation") return "🦅";
|
||||||
|
if (type === "magical") return "🔮";
|
||||||
|
else return "⚔️";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1.4 added state reference for regiments
|
||||||
|
pack.states.filter(s => s.military).forEach(s => s.military.forEach(r => (r.state = s.i)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version < 1.5) {
|
||||||
|
// not need to store default styles from v 1.5
|
||||||
|
localStorage.removeItem("styleClean");
|
||||||
|
localStorage.removeItem("styleGloom");
|
||||||
|
localStorage.removeItem("styleAncient");
|
||||||
|
localStorage.removeItem("styleMonochrome");
|
||||||
|
|
||||||
|
// v 1.5 cultures has shield attribute
|
||||||
|
pack.cultures.forEach(culture => {
|
||||||
|
if (culture.removed) return;
|
||||||
|
culture.shield = Cultures.getRandomShield();
|
||||||
|
});
|
||||||
|
|
||||||
|
// v 1.5 added burg type value
|
||||||
|
pack.burgs.forEach(burg => {
|
||||||
|
if (!burg.i || burg.removed) return;
|
||||||
|
burg.type = BurgsAndStates.getType(burg.cell, burg.port);
|
||||||
|
});
|
||||||
|
|
||||||
|
// v 1.5 added emblems
|
||||||
|
defs.append("g").attr("id", "defs-emblems");
|
||||||
|
emblems = viewbox.insert("g", "#population").attr("id", "emblems").style("display", "none");
|
||||||
|
emblems.append("g").attr("id", "burgEmblems");
|
||||||
|
emblems.append("g").attr("id", "provinceEmblems");
|
||||||
|
emblems.append("g").attr("id", "stateEmblems");
|
||||||
|
regenerateEmblems();
|
||||||
|
toggleEmblems();
|
||||||
|
|
||||||
|
// v 1.5 changed releif icons data
|
||||||
|
terrain.selectAll("use").each(function () {
|
||||||
|
const type = this.getAttribute("data-type") || this.getAttribute("xlink:href");
|
||||||
|
this.removeAttribute("xlink:href");
|
||||||
|
this.removeAttribute("data-type");
|
||||||
|
this.removeAttribute("data-size");
|
||||||
|
this.setAttribute("href", type);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version < 1.6) {
|
||||||
|
// v 1.6 changed rivers data
|
||||||
|
for (const river of pack.rivers) {
|
||||||
|
const el = document.getElementById("river" + river.i);
|
||||||
|
if (el) {
|
||||||
|
river.widthFactor = +el.getAttribute("data-width");
|
||||||
|
el.removeAttribute("data-width");
|
||||||
|
el.removeAttribute("data-increment");
|
||||||
|
river.discharge = pack.cells.fl[river.mouth] || 1;
|
||||||
|
river.width = rn(river.length / 100, 2);
|
||||||
|
river.sourceWidth = 0.1;
|
||||||
|
} else {
|
||||||
|
Rivers.remove(river.i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// v 1.6 changed lakes data
|
||||||
|
for (const f of pack.features) {
|
||||||
|
if (f.type !== "lake") continue;
|
||||||
|
if (f.evaporation) continue;
|
||||||
|
|
||||||
|
f.flux = f.flux || f.cells * 3;
|
||||||
|
f.temp = grid.cells.temp[pack.cells.g[f.firstCell]];
|
||||||
|
f.height = f.height || d3.min(pack.cells.c[f.firstCell].map(c => pack.cells.h[c]).filter(h => h >= 20));
|
||||||
|
const height = (f.height - 18) ** heightExponentInput.value;
|
||||||
|
const evaporation = ((700 * (f.temp + 0.006 * height)) / 50 + 75) / (80 - f.temp);
|
||||||
|
f.evaporation = rn(evaporation * f.cells);
|
||||||
|
f.name = f.name || Lakes.getName(f);
|
||||||
|
delete f.river;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version < 1.61) {
|
||||||
|
// v 1.61 changed rulers data
|
||||||
|
ruler.style("display", null);
|
||||||
|
rulers = new Rulers();
|
||||||
|
|
||||||
|
ruler.selectAll(".ruler > .white").each(function () {
|
||||||
|
const x1 = +this.getAttribute("x1");
|
||||||
|
const y1 = +this.getAttribute("y1");
|
||||||
|
const x2 = +this.getAttribute("x2");
|
||||||
|
const y2 = +this.getAttribute("y2");
|
||||||
|
if (isNaN(x1) || isNaN(y1) || isNaN(x2) || isNaN(y2)) return;
|
||||||
|
const points = [
|
||||||
|
[x1, y1],
|
||||||
|
[x2, y2]
|
||||||
|
];
|
||||||
|
rulers.create(Ruler, points);
|
||||||
|
});
|
||||||
|
|
||||||
|
ruler.selectAll("g.opisometer").each(function () {
|
||||||
|
const pointsString = this.dataset.points;
|
||||||
|
if (!pointsString) return;
|
||||||
|
const points = JSON.parse(pointsString);
|
||||||
|
rulers.create(Opisometer, points);
|
||||||
|
});
|
||||||
|
|
||||||
|
ruler.selectAll("path.planimeter").each(function () {
|
||||||
|
const length = this.getTotalLength();
|
||||||
|
if (length < 30) return;
|
||||||
|
|
||||||
|
const step = length > 1000 ? 40 : length > 400 ? 20 : 10;
|
||||||
|
const increment = length / Math.ceil(length / step);
|
||||||
|
const points = [];
|
||||||
|
for (let i = 0; i <= length; i += increment) {
|
||||||
|
const point = this.getPointAtLength(i);
|
||||||
|
points.push([point.x | 0, point.y | 0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
rulers.create(Planimeter, points);
|
||||||
|
});
|
||||||
|
|
||||||
|
ruler.selectAll("*").remove();
|
||||||
|
|
||||||
|
if (rulers.data.length) {
|
||||||
|
turnButtonOn("toggleRulers");
|
||||||
|
rulers.draw();
|
||||||
|
} else turnButtonOff("toggleRulers");
|
||||||
|
|
||||||
|
// 1.61 changed oceanicPattern from rect to image
|
||||||
|
const pattern = document.getElementById("oceanic");
|
||||||
|
const filter = pattern.firstElementChild.getAttribute("filter");
|
||||||
|
const href = filter ? "./images/" + filter.replace("url(#", "").replace(")", "") + ".png" : "";
|
||||||
|
pattern.innerHTML = `<image id="oceanicPattern" href=${href} width="100" height="100" opacity="0.2"></image>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version < 1.62) {
|
||||||
|
// v 1.62 changed grid data
|
||||||
|
gridOverlay.attr("size", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version < 1.63) {
|
||||||
|
// v.1.63 change ocean pattern opacity element
|
||||||
|
const oceanPattern = document.getElementById("oceanPattern");
|
||||||
|
if (oceanPattern) oceanPattern.removeAttribute("opacity");
|
||||||
|
const oceanicPattern = document.getElementById("oceanicPattern");
|
||||||
|
if (!oceanicPattern.getAttribute("opacity")) oceanicPattern.setAttribute("opacity", 0.2);
|
||||||
|
|
||||||
|
// v 1.63 moved label text-shadow from css to editable inline style
|
||||||
|
burgLabels.select("#cities").style("text-shadow", "white 0 0 4px");
|
||||||
|
burgLabels.select("#towns").style("text-shadow", "white 0 0 4px");
|
||||||
|
labels.select("#states").style("text-shadow", "white 0 0 4px");
|
||||||
|
labels.select("#addedLabels").style("text-shadow", "white 0 0 4px");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
void (function checkDataIntegrity() {
|
||||||
|
const cells = pack.cells;
|
||||||
|
|
||||||
|
if (pack.cells.i.length !== pack.cells.state.length) {
|
||||||
|
ERROR && console.error("Striping issue. Map data is corrupted. The only solution is to edit the heightmap in erase mode");
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalidStates = [...new Set(cells.state)].filter(s => !pack.states[s] || pack.states[s].removed);
|
||||||
|
invalidStates.forEach(s => {
|
||||||
|
const invalidCells = cells.i.filter(i => cells.state[i] === s);
|
||||||
|
invalidCells.forEach(i => (cells.state[i] = 0));
|
||||||
|
ERROR && console.error("Data Integrity Check. Invalid state", s, "is assigned to cells", invalidCells);
|
||||||
|
});
|
||||||
|
|
||||||
|
const invalidProvinces = [...new Set(cells.province)].filter(p => p && (!pack.provinces[p] || pack.provinces[p].removed));
|
||||||
|
invalidProvinces.forEach(p => {
|
||||||
|
const invalidCells = cells.i.filter(i => cells.province[i] === p);
|
||||||
|
invalidCells.forEach(i => (cells.province[i] = 0));
|
||||||
|
ERROR && console.error("Data Integrity Check. Invalid province", p, "is assigned to cells", invalidCells);
|
||||||
|
});
|
||||||
|
|
||||||
|
const invalidCultures = [...new Set(cells.culture)].filter(c => !pack.cultures[c] || pack.cultures[c].removed);
|
||||||
|
invalidCultures.forEach(c => {
|
||||||
|
const invalidCells = cells.i.filter(i => cells.culture[i] === c);
|
||||||
|
invalidCells.forEach(i => (cells.province[i] = 0));
|
||||||
|
ERROR && console.error("Data Integrity Check. Invalid culture", c, "is assigned to cells", invalidCells);
|
||||||
|
});
|
||||||
|
|
||||||
|
const invalidReligions = [...new Set(cells.religion)].filter(r => !pack.religions[r] || pack.religions[r].removed);
|
||||||
|
invalidReligions.forEach(r => {
|
||||||
|
const invalidCells = cells.i.filter(i => cells.religion[i] === r);
|
||||||
|
invalidCells.forEach(i => (cells.religion[i] = 0));
|
||||||
|
ERROR && console.error("Data Integrity Check. Invalid religion", c, "is assigned to cells", invalidCells);
|
||||||
|
});
|
||||||
|
|
||||||
|
const invalidFeatures = [...new Set(cells.f)].filter(f => f && !pack.features[f]);
|
||||||
|
invalidFeatures.forEach(f => {
|
||||||
|
const invalidCells = cells.i.filter(i => cells.f[i] === f);
|
||||||
|
// No fix as for now
|
||||||
|
ERROR && console.error("Data Integrity Check. Invalid feature", f, "is assigned to cells", invalidCells);
|
||||||
|
});
|
||||||
|
|
||||||
|
const invalidBurgs = [...new Set(cells.burg)].filter(b => b && (!pack.burgs[b] || pack.burgs[b].removed));
|
||||||
|
invalidBurgs.forEach(b => {
|
||||||
|
const invalidCells = cells.i.filter(i => cells.burg[i] === b);
|
||||||
|
invalidCells.forEach(i => (cells.burg[i] = 0));
|
||||||
|
ERROR && console.error("Data Integrity Check. Invalid burg", b, "is assigned to cells", invalidCells);
|
||||||
|
});
|
||||||
|
|
||||||
|
const invalidRivers = [...new Set(cells.r)].filter(r => r && !pack.rivers.find(river => river.i === r));
|
||||||
|
invalidRivers.forEach(r => {
|
||||||
|
const invalidCells = cells.i.filter(i => cells.r[i] === r);
|
||||||
|
invalidCells.forEach(i => (cells.r[i] = 0));
|
||||||
|
rivers.select("river" + r).remove();
|
||||||
|
ERROR && console.error("Data Integrity Check. Invalid river", r, "is assigned to cells", invalidCells);
|
||||||
|
});
|
||||||
|
|
||||||
|
pack.burgs.forEach(b => {
|
||||||
|
if (!b.i || b.removed) return;
|
||||||
|
if (b.port < 0) {
|
||||||
|
ERROR && console.error("Data Integrity Check. Burg", b.i, "has invalid port value", b.port);
|
||||||
|
b.port = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b.cell >= cells.i.length) {
|
||||||
|
ERROR && console.error("Data Integrity Check. Burg", b.i, "is linked to invalid cell", b.cell);
|
||||||
|
b.cell = findCell(b.x, b.y);
|
||||||
|
cells.i.filter(i => cells.burg[i] === b.i).forEach(i => (cells.burg[i] = 0));
|
||||||
|
cells.burg[b.cell] = b.i;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b.state && !pack.states[b.state]) {
|
||||||
|
ERROR && console.error("Data Integrity Check. Burg", b.i, "is linked to invalid state", b.state);
|
||||||
|
b.state = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
pack.provinces.forEach(p => {
|
||||||
|
if (!p.i || p.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);
|
||||||
|
p.removed = true; // remove incorrect province
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
changeMapSize();
|
||||||
|
|
||||||
|
// remove href from emblems, to trigger rendering on load
|
||||||
|
emblems.selectAll("use").attr("href", null);
|
||||||
|
|
||||||
|
// draw data layers (no kept in svg)
|
||||||
|
if (rulers && layerIsOn("toggleRulers")) rulers.draw();
|
||||||
|
if (layerIsOn("toggleGrid")) drawGrid();
|
||||||
|
|
||||||
|
// set options
|
||||||
|
yearInput.value = options.year;
|
||||||
|
eraInput.value = options.era;
|
||||||
|
|
||||||
|
if (window.restoreDefaultEvents) restoreDefaultEvents();
|
||||||
|
focusOn(); // based on searchParams focus on point, cell or burg
|
||||||
|
invokeActiveZooming();
|
||||||
|
|
||||||
|
WARN && console.warn(`TOTAL: ${rn((performance.now() - uploadMap.timeStart) / 1000, 2)}s`);
|
||||||
|
showStatistics();
|
||||||
|
INFO && console.groupEnd("Loaded Map " + seed);
|
||||||
|
tip("Map is successfully loaded", true, "success", 7000);
|
||||||
|
} catch (error) {
|
||||||
|
ERROR && console.error(error);
|
||||||
|
clearMainTip();
|
||||||
|
|
||||||
|
alertMessage.innerHTML = `An error is occured on map loading. Select a different file to load,
|
||||||
|
<br>generate a new random map or cancel the loading
|
||||||
|
<p id="errorBox">${parseError(error)}</p>`;
|
||||||
|
$("#alert").dialog({
|
||||||
|
resizable: false,
|
||||||
|
title: "Loading error",
|
||||||
|
maxWidth: "50em",
|
||||||
|
buttons: {
|
||||||
|
"Select file": function () {
|
||||||
|
$(this).dialog("close");
|
||||||
|
mapToLoad.click();
|
||||||
|
},
|
||||||
|
"New map": function () {
|
||||||
|
$(this).dialog("close");
|
||||||
|
regenerateMap();
|
||||||
|
},
|
||||||
|
Cancel: function () {
|
||||||
|
$(this).dialog("close");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
position: {my: "center", at: "center", of: "svg"}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,60 +1,67 @@
|
||||||
(function (global, factory) {
|
(function (global, factory) {
|
||||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
typeof exports === "object" && typeof module !== "undefined" ? (module.exports = factory()) : typeof define === "function" && define.amd ? define(factory) : (global.Military = factory());
|
||||||
typeof define === 'function' && define.amd ? define(factory) :
|
})(this, function () {
|
||||||
(global.Military = factory());
|
"use strict";
|
||||||
}(this, (function () {'use strict';
|
|
||||||
|
|
||||||
const generate = function () {
|
const generate = function () {
|
||||||
TIME && console.time("generateMilitaryForces");
|
TIME && console.time("generateMilitaryForces");
|
||||||
const cells = pack.cells, p = cells.p, states = pack.states;
|
const cells = pack.cells,
|
||||||
|
p = cells.p,
|
||||||
|
states = pack.states;
|
||||||
const valid = states.filter(s => s.i && !s.removed); // valid states
|
const valid = states.filter(s => s.i && !s.removed); // valid states
|
||||||
if (!options.military) options.military = getDefaultOptions();
|
if (!options.military) options.military = getDefaultOptions();
|
||||||
|
|
||||||
const expn = d3.sum(valid.map(s => s.expansionism)); // total expansion
|
const expn = d3.sum(valid.map(s => s.expansionism)); // total expansion
|
||||||
const area = d3.sum(valid.map(s => s.area)); // total area
|
const area = d3.sum(valid.map(s => s.area)); // total area
|
||||||
const rate = {x:0, Ally:-.2, Friendly:-.1, Neutral:0, Suspicion:.1, Enemy:1, Unknown:0, Rival:.5, Vassal:.5, Suzerain:-.5};
|
const rate = {x: 0, Ally: -0.2, Friendly: -0.1, Neutral: 0, Suspicion: 0.1, Enemy: 1, Unknown: 0, Rival: 0.5, Vassal: 0.5, Suzerain: -0.5};
|
||||||
|
|
||||||
const stateModifier = {
|
const stateModifier = {
|
||||||
"melee": {"Nomadic":.5, "Highland":1.2, "Lake":1, "Naval":.7, "Hunting":1.2, "River":1.1},
|
melee: {Nomadic: 0.5, Highland: 1.2, Lake: 1, Naval: 0.7, Hunting: 1.2, River: 1.1},
|
||||||
"ranged": {"Nomadic":.9, "Highland":1.3, "Lake":1, "Naval":.8, "Hunting":2, "River":.8},
|
ranged: {Nomadic: 0.9, Highland: 1.3, Lake: 1, Naval: 0.8, Hunting: 2, River: 0.8},
|
||||||
"mounted": {"Nomadic":2.3, "Highland":.6, "Lake":.7, "Naval":.3, "Hunting":.7, "River":.8},
|
mounted: {Nomadic: 2.3, Highland: 0.6, Lake: 0.7, Naval: 0.3, Hunting: 0.7, River: 0.8},
|
||||||
"machinery":{"Nomadic":.8, "Highland":1.4, "Lake":1.1, "Naval":1.4, "Hunting":.4, "River":1.1},
|
machinery: {Nomadic: 0.8, Highland: 1.4, Lake: 1.1, Naval: 1.4, Hunting: 0.4, River: 1.1},
|
||||||
"naval": {"Nomadic":.5, "Highland":.5, "Lake":1.2, "Naval":1.8, "Hunting":.7, "River":1.2},
|
naval: {Nomadic: 0.5, Highland: 0.5, Lake: 1.2, Naval: 1.8, Hunting: 0.7, River: 1.2},
|
||||||
// non-default generic:
|
// non-default generic:
|
||||||
"armored": {"Nomadic":1, "Highland":.5, "Lake":1, "Naval":1, "Hunting":.7, "River":1.1},
|
armored: {Nomadic: 1, Highland: 0.5, Lake: 1, Naval: 1, Hunting: 0.7, River: 1.1},
|
||||||
"aviation": {"Nomadic":.5, "Highland":.5, "Lake":1.2, "Naval":1.2, "Hunting":.6, "River":1.2},
|
aviation: {Nomadic: 0.5, Highland: 0.5, Lake: 1.2, Naval: 1.2, Hunting: 0.6, River: 1.2},
|
||||||
"magical": {"Nomadic":1, "Highland":2, "Lake":1, "Naval":1, "Hunting":1, "River":1}
|
magical: {Nomadic: 1, Highland: 2, Lake: 1, Naval: 1, Hunting: 1, River: 1}
|
||||||
};
|
};
|
||||||
|
|
||||||
const cellTypeModifier = {
|
const cellTypeModifier = {
|
||||||
"nomadic": {"melee":.2, "ranged":.5, "mounted":3, "machinery":.4, "naval":.3, "armored":1.6, "aviation":1, "magical":.5},
|
nomadic: {melee: 0.2, ranged: 0.5, mounted: 3, machinery: 0.4, naval: 0.3, armored: 1.6, aviation: 1, magical: 0.5},
|
||||||
"wetland": {"melee":.8, "ranged":2, "mounted":0.3, "machinery":1.2, "naval":1.0, "armored":0.2, "aviation":.5, "magical":0.5},
|
wetland: {melee: 0.8, ranged: 2, mounted: 0.3, machinery: 1.2, naval: 1.0, armored: 0.2, aviation: 0.5, magical: 0.5},
|
||||||
"highland": {"melee":1.2, "ranged":1.6, "mounted":0.3, "machinery":3, "naval":1.0, "armored":0.8, "aviation":.3, "magical":2}
|
highland: {melee: 1.2, ranged: 1.6, mounted: 0.3, machinery: 3, naval: 1.0, armored: 0.8, aviation: 0.3, magical: 2}
|
||||||
}
|
};
|
||||||
|
|
||||||
const burgTypeModifier = {
|
const burgTypeModifier = {
|
||||||
"nomadic": {"melee":.3, "ranged":.8, "mounted":3, "machinery":.4, "naval":1.0, "armored":1.6, "aviation":1, "magical":0.5},
|
nomadic: {melee: 0.3, ranged: 0.8, mounted: 3, machinery: 0.4, naval: 1.0, armored: 1.6, aviation: 1, magical: 0.5},
|
||||||
"wetland": {"melee":1, "ranged":1.6, "mounted":.2, "machinery":1.2, "naval":1.0, "armored":0.2, "aviation":0.5, "magical":0.5},
|
wetland: {melee: 1, ranged: 1.6, mounted: 0.2, machinery: 1.2, naval: 1.0, armored: 0.2, aviation: 0.5, magical: 0.5},
|
||||||
"highland": {"melee":1.2, "ranged":2, "mounted":.3, "machinery":3, "naval":1.0, "armored":0.8, "aviation":0.3, "magical":2}
|
highland: {melee: 1.2, ranged: 2, mounted: 0.3, machinery: 3, naval: 1.0, armored: 0.8, aviation: 0.3, magical: 2}
|
||||||
}
|
};
|
||||||
|
|
||||||
valid.forEach(s => {
|
valid.forEach(s => {
|
||||||
const temp = s.temp = {}, d = s.diplomacy;
|
const temp = (s.temp = {}),
|
||||||
const expansionRate = Math.min(Math.max((s.expansionism / expn) / (s.area / area), .25), 4); // how much state expansionism is realized
|
d = s.diplomacy;
|
||||||
const diplomacyRate = d.some(d => d === "Enemy") ? 1 : d.some(d => d === "Rival") ? .8 : d.some(d => d === "Suspicion") ? .5 : .1; // peacefulness
|
const expansionRate = Math.min(Math.max(s.expansionism / expn / (s.area / area), 0.25), 4); // how much state expansionism is realized
|
||||||
const neighborsRate = Math.min(Math.max(s.neighbors.map(n => n ? pack.states[n].diplomacy[s.i] : "Suspicion").reduce((s, r) => s += rate[r], .5), .3), 3); // neighbors rate
|
const diplomacyRate = d.some(d => d === "Enemy") ? 1 : d.some(d => d === "Rival") ? 0.8 : d.some(d => d === "Suspicion") ? 0.5 : 0.1; // peacefulness
|
||||||
s.alert = Math.min(Math.max(rn(expansionRate * diplomacyRate * neighborsRate, 2), .1), 5); // war alert rate (army modifier)
|
const neighborsRate = Math.min(
|
||||||
|
Math.max(
|
||||||
|
s.neighbors.map(n => (n ? pack.states[n].diplomacy[s.i] : "Suspicion")).reduce((s, r) => (s += rate[r]), 0.5),
|
||||||
|
0.3
|
||||||
|
),
|
||||||
|
3
|
||||||
|
); // neighbors rate
|
||||||
|
s.alert = Math.min(Math.max(rn(expansionRate * diplomacyRate * neighborsRate, 2), 0.1), 5); // war alert rate (army modifier)
|
||||||
temp.platoons = [];
|
temp.platoons = [];
|
||||||
|
|
||||||
// apply overall state modifiers for unit types based on state features
|
// apply overall state modifiers for unit types based on state features
|
||||||
for (const unit of options.military) {
|
for (const unit of options.military) {
|
||||||
if (!stateModifier[unit.type]) continue;
|
if (!stateModifier[unit.type]) continue;
|
||||||
let modifier = stateModifier[unit.type][s.type] || 1;
|
let modifier = stateModifier[unit.type][s.type] || 1;
|
||||||
if (unit.type === "mounted" && s.formName.includes("Horde")) modifier *= 2; else
|
if (unit.type === "mounted" && s.formName.includes("Horde")) modifier *= 2;
|
||||||
if (unit.type === "naval" && s.form === "Republic") modifier *= 1.2;
|
else if (unit.type === "naval" && s.form === "Republic") modifier *= 1.2;
|
||||||
temp[unit.name] = modifier * s.alert;
|
temp[unit.name] = modifier * s.alert;
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const getType = cell => {
|
const getType = cell => {
|
||||||
|
|
@ -62,7 +69,7 @@
|
||||||
if ([7, 8, 9, 12].includes(cells.biome[cell])) return "wetland";
|
if ([7, 8, 9, 12].includes(cells.biome[cell])) return "wetland";
|
||||||
if (cells.h[cell] >= 70) return "highland";
|
if (cells.h[cell] >= 70) return "highland";
|
||||||
return "generic";
|
return "generic";
|
||||||
}
|
};
|
||||||
|
|
||||||
for (const i of cells.i) {
|
for (const i of cells.i) {
|
||||||
if (!cells.pop[i]) continue;
|
if (!cells.pop[i]) continue;
|
||||||
|
|
@ -79,12 +86,18 @@
|
||||||
const perc = +u.rural;
|
const perc = +u.rural;
|
||||||
if (isNaN(perc) || perc <= 0 || !s.temp[u.name]) continue;
|
if (isNaN(perc) || perc <= 0 || !s.temp[u.name]) continue;
|
||||||
|
|
||||||
const mod = type === "generic" ? 1 : cellTypeModifier[type][u.type] // cell specific modifier
|
const mod = type === "generic" ? 1 : cellTypeModifier[type][u.type]; // cell specific modifier
|
||||||
const army = m * perc * mod; // rural cell army
|
const army = m * perc * mod; // rural cell army
|
||||||
const t = rn(army * s.temp[u.name] * populationRate.value); // total troops
|
const t = rn(army * s.temp[u.name] * populationRate); // total troops
|
||||||
if (!t) continue;
|
if (!t) continue;
|
||||||
let x = p[i][0], y = p[i][1], n = 0;
|
let x = p[i][0],
|
||||||
if (u.type === "naval") {let haven = cells.haven[i]; x = p[haven][0], y = p[haven][1]; n = 1}; // place naval to sea
|
y = p[i][1],
|
||||||
|
n = 0;
|
||||||
|
if (u.type === "naval") {
|
||||||
|
let haven = cells.haven[i];
|
||||||
|
(x = p[haven][0]), (y = p[haven][1]);
|
||||||
|
n = 1;
|
||||||
|
} // place naval to sea
|
||||||
s.temp.platoons.push({cell: i, a: t, t, x, y, u: u.name, n, s: u.separate, type: u.type});
|
s.temp.platoons.push({cell: i, a: t, t, x, y, u: u.name, n, s: u.separate, type: u.type});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -93,7 +106,7 @@
|
||||||
if (!b.i || b.removed || !b.state || !b.population) continue;
|
if (!b.i || b.removed || !b.state || !b.population) continue;
|
||||||
const s = states[b.state]; // burg state
|
const s = states[b.state]; // burg state
|
||||||
|
|
||||||
let m = b.population * urbanization.value / 100; // basic urban army in percentages
|
let m = (b.population * urbanization) / 100; // basic urban army in percentages
|
||||||
if (b.capital) m *= 1.2; // capital has household troops
|
if (b.capital) m *= 1.2; // capital has household troops
|
||||||
if (b.culture !== s.culture) m = s.form === "Union" ? m / 1.2 : m / 2; // non-dominant culture
|
if (b.culture !== s.culture) m = s.form === "Union" ? m / 1.2 : m / 2; // non-dominant culture
|
||||||
if (cells.religion[b.cell] !== cells.religion[s.center]) m = s.form === "Theocracy" ? m / 2.2 : m / 1.4; // non-dominant religion
|
if (cells.religion[b.cell] !== cells.religion[s.center]) m = s.form === "Theocracy" ? m / 2.2 : m / 1.4; // non-dominant religion
|
||||||
|
|
@ -105,25 +118,31 @@
|
||||||
const perc = +u.urban;
|
const perc = +u.urban;
|
||||||
if (isNaN(perc) || perc <= 0 || !s.temp[u.name]) continue;
|
if (isNaN(perc) || perc <= 0 || !s.temp[u.name]) continue;
|
||||||
|
|
||||||
const mod = type === "generic" ? 1 : burgTypeModifier[type][u.type] // cell specific modifier
|
const mod = type === "generic" ? 1 : burgTypeModifier[type][u.type]; // cell specific modifier
|
||||||
const army = m * perc * mod; // urban cell army
|
const army = m * perc * mod; // urban cell army
|
||||||
const t = rn(army * s.temp[u.name] * populationRate.value); // total troops
|
const t = rn(army * s.temp[u.name] * populationRate); // total troops
|
||||||
if (!t) continue;
|
if (!t) continue;
|
||||||
let x = p[b.cell][0], y = p[b.cell][1], n = 0;
|
let x = p[b.cell][0],
|
||||||
if (u.type === "naval") {let haven = cells.haven[b.cell]; x = p[haven][0], y = p[haven][1]; n = 1}; // place naval in sea cell
|
y = p[b.cell][1],
|
||||||
|
n = 0;
|
||||||
|
if (u.type === "naval") {
|
||||||
|
let haven = cells.haven[b.cell];
|
||||||
|
(x = p[haven][0]), (y = p[haven][1]);
|
||||||
|
n = 1;
|
||||||
|
} // place naval in sea cell
|
||||||
s.temp.platoons.push({cell: b.cell, a: t, t, x, y, u: u.name, n, s: u.separate, type: u.type});
|
s.temp.platoons.push({cell: b.cell, a: t, t, x, y, u: u.name, n, s: u.separate, type: u.type});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void function removeExistingRegiments() {
|
void (function removeExistingRegiments() {
|
||||||
armies.selectAll("g > g").each(function () {
|
armies.selectAll("g > g").each(function () {
|
||||||
const index = notes.findIndex(n => n.id === this.id);
|
const index = notes.findIndex(n => n.id === this.id);
|
||||||
if (index != -1) notes.splice(index, 1);
|
if (index != -1) notes.splice(index, 1);
|
||||||
});
|
});
|
||||||
armies.selectAll("g").remove();
|
armies.selectAll("g").remove();
|
||||||
}()
|
})();
|
||||||
|
|
||||||
const expected = 3 * populationRate.value; // expected regiment size
|
const expected = 3 * populationRate; // expected regiment size
|
||||||
const mergeable = (n0, n1) => (!n0.s && !n1.s) || n0.type === n1.type; // check if regiments can be merged
|
const mergeable = (n0, n1) => (!n0.s && !n1.s) || n0.type === n1.type; // check if regiments can be merged
|
||||||
|
|
||||||
// get regiments for each state
|
// get regiments for each state
|
||||||
|
|
@ -136,31 +155,46 @@
|
||||||
function createRegiments(nodes, s) {
|
function createRegiments(nodes, s) {
|
||||||
if (!nodes.length) return [];
|
if (!nodes.length) return [];
|
||||||
nodes.sort((a, b) => a.a - b.a); // form regiments in cells with most troops
|
nodes.sort((a, b) => a.a - b.a); // form regiments in cells with most troops
|
||||||
const tree = d3.quadtree(nodes, d => d.x, d => d.y);
|
const tree = d3.quadtree(
|
||||||
|
nodes,
|
||||||
|
d => d.x,
|
||||||
|
d => d.y
|
||||||
|
);
|
||||||
nodes.forEach(n => {
|
nodes.forEach(n => {
|
||||||
tree.remove(n);
|
tree.remove(n);
|
||||||
const overlap = tree.find(n.x, n.y, 20);
|
const overlap = tree.find(n.x, n.y, 20);
|
||||||
if (overlap && overlap.t && mergeable(n, overlap)) {merge(n, overlap); return;}
|
if (overlap && overlap.t && mergeable(n, overlap)) {
|
||||||
|
merge(n, overlap);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (n.t > expected) return;
|
if (n.t > expected) return;
|
||||||
const r = (expected - n.t) / (n.s ? 40 : 20); // search radius
|
const r = (expected - n.t) / (n.s ? 40 : 20); // search radius
|
||||||
const candidates = tree.findAll(n.x, n.y, r);
|
const candidates = tree.findAll(n.x, n.y, r);
|
||||||
for (const c of candidates) {
|
for (const c of candidates) {
|
||||||
if (c.t < expected && mergeable(n, c)) {merge(n, c); break;}
|
if (c.t < expected && mergeable(n, c)) {
|
||||||
|
merge(n, c);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// add n0 to n1's ultimate parent
|
// add n0 to n1's ultimate parent
|
||||||
function merge(n0, n1) {
|
function merge(n0, n1) {
|
||||||
if (!n1.childen) n1.childen = [n0]; else n1.childen.push(n0);
|
if (!n1.childen) n1.childen = [n0];
|
||||||
|
else n1.childen.push(n0);
|
||||||
if (n0.childen) n0.childen.forEach(n => n1.childen.push(n));
|
if (n0.childen) n0.childen.forEach(n => n1.childen.push(n));
|
||||||
n1.t += n0.t;
|
n1.t += n0.t;
|
||||||
n0.t = 0;
|
n0.t = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse regiments data
|
// parse regiments data
|
||||||
const regiments = nodes.filter(n => n.t).sort((a,b) => b.t - a.t).map((r, i) => {
|
const regiments = nodes
|
||||||
const u = {}; u[r.u] = r.a;
|
.filter(n => n.t)
|
||||||
(r.childen||[]).forEach(n => u[n.u] = u[n.u] ? u[n.u] += n.a : n.a);
|
.sort((a, b) => b.t - a.t)
|
||||||
|
.map((r, i) => {
|
||||||
|
const u = {};
|
||||||
|
u[r.u] = r.a;
|
||||||
|
(r.childen || []).forEach(n => (u[n.u] = u[n.u] ? (u[n.u] += n.a) : n.a));
|
||||||
return {i, a: r.t, cell: r.cell, x: r.x, y: r.y, bx: r.x, by: r.y, u, n: r.n, name, state: s.i};
|
return {i, a: r.t, cell: r.cell, x: r.x, y: r.y, bx: r.x, by: r.y, u, n: r.n, name, state: s.i};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -175,36 +209,62 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
TIME && console.timeEnd("generateMilitaryForces");
|
TIME && console.timeEnd("generateMilitaryForces");
|
||||||
}
|
};
|
||||||
|
|
||||||
const getDefaultOptions = function () {
|
const getDefaultOptions = function () {
|
||||||
return [
|
return [
|
||||||
{icon: "⚔️", name:"infantry", rural:.25, urban:.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},
|
||||||
{icon: "🏹", name:"archers", rural:.12, urban:.2, crew:1, power:1, type:"ranged", separate:0},
|
{icon: "🏹", name: "archers", rural: 0.12, urban: 0.2, crew: 1, power: 1, type: "ranged", separate: 0},
|
||||||
{icon: "🐴", name:"cavalry", rural:.12, urban:.03, crew:2, power:2, type:"mounted", separate:0},
|
{icon: "🐴", name: "cavalry", rural: 0.12, urban: 0.03, crew: 2, power: 2, type: "mounted", separate: 0},
|
||||||
{icon: "💣", name:"artillery", rural:0, urban:.03, crew:8, power:12, type:"machinery", separate:0},
|
{icon: "💣", name: "artillery", rural: 0, urban: 0.03, crew: 8, power: 12, type: "machinery", separate: 0},
|
||||||
{icon: "🌊", name:"fleet", rural:0, urban:.015, crew:100, power:50, type:"naval", separate:1}
|
{icon: "🌊", name: "fleet", rural: 0, urban: 0.015, crew: 100, power: 50, type: "naval", separate: 1}
|
||||||
];
|
];
|
||||||
}
|
};
|
||||||
|
|
||||||
const drawRegiments = function (regiments, s) {
|
const drawRegiments = function (regiments, s) {
|
||||||
const size = +armies.attr("box-size");
|
const size = +armies.attr("box-size");
|
||||||
const w = d => d.n ? size * 4 : size * 6;
|
const w = d => (d.n ? size * 4 : size * 6);
|
||||||
const h = size * 2;
|
const h = size * 2;
|
||||||
const x = d => rn(d.x - w(d) / 2, 2);
|
const x = d => rn(d.x - w(d) / 2, 2);
|
||||||
const y = d => rn(d.y - size, 2);
|
const y = d => rn(d.y - size, 2);
|
||||||
|
|
||||||
const baseColor = pack.states[s].color[0] === "#" ? pack.states[s].color : "#999";
|
const baseColor = pack.states[s].color[0] === "#" ? pack.states[s].color : "#999";
|
||||||
const darkerColor = d3.color(baseColor).darker().hex();
|
const darkerColor = d3.color(baseColor).darker().hex();
|
||||||
const army = armies.append("g").attr("id", "army"+s).attr("fill", baseColor);
|
const army = armies
|
||||||
|
.append("g")
|
||||||
|
.attr("id", "army" + s)
|
||||||
|
.attr("fill", baseColor);
|
||||||
|
|
||||||
const g = army.selectAll("g").data(regiments).enter().append("g")
|
const g = army
|
||||||
.attr("id", d => "regiment"+s+"-"+d.i).attr("data-name", d => d.name).attr("data-state", s).attr("data-id", d => d.i);
|
.selectAll("g")
|
||||||
g.append("rect").attr("x", d => x(d)).attr("y", d => y(d)).attr("width", d => w(d)).attr("height", h);
|
.data(regiments)
|
||||||
g.append("text").attr("x", d => d.x).attr("y", d => d.y).text(d => getTotal(d));
|
.enter()
|
||||||
g.append("rect").attr("fill", darkerColor).attr("x", d => x(d)-h).attr("y", d => y(d)).attr("width", h).attr("height", h);
|
.append("g")
|
||||||
g.append("text").attr("class", "regimentIcon").attr("x", d => x(d)-size).attr("y", d => d.y).text(d => d.icon);
|
.attr("id", d => "regiment" + s + "-" + d.i)
|
||||||
}
|
.attr("data-name", d => d.name)
|
||||||
|
.attr("data-state", s)
|
||||||
|
.attr("data-id", d => d.i);
|
||||||
|
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", darkerColor)
|
||||||
|
.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, s) {
|
const drawRegiment = function (reg, s) {
|
||||||
const size = +armies.attr("box-size");
|
const size = +armies.attr("box-size");
|
||||||
|
|
@ -216,16 +276,33 @@
|
||||||
let army = armies.select("g#army" + s);
|
let army = armies.select("g#army" + s);
|
||||||
if (!army.size()) {
|
if (!army.size()) {
|
||||||
const baseColor = pack.states[s].color[0] === "#" ? pack.states[s].color : "#999";
|
const baseColor = pack.states[s].color[0] === "#" ? pack.states[s].color : "#999";
|
||||||
army = armies.append("g").attr("id", "army"+s).attr("fill", baseColor);
|
army = armies
|
||||||
|
.append("g")
|
||||||
|
.attr("id", "army" + s)
|
||||||
|
.attr("fill", baseColor);
|
||||||
}
|
}
|
||||||
const darkerColor = d3.color(army.attr("fill")).darker().hex();
|
const darkerColor = d3.color(army.attr("fill")).darker().hex();
|
||||||
|
|
||||||
const g = army.append("g").attr("id", "regiment"+s+"-"+reg.i).attr("data-name", reg.name).attr("data-state", s).attr("data-id", reg.i);
|
const g = army
|
||||||
|
.append("g")
|
||||||
|
.attr("id", "regiment" + s + "-" + reg.i)
|
||||||
|
.attr("data-name", reg.name)
|
||||||
|
.attr("data-state", s)
|
||||||
|
.attr("data-id", reg.i);
|
||||||
g.append("rect").attr("x", x1).attr("y", y1).attr("width", w).attr("height", h);
|
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("text").attr("x", reg.x).attr("y", reg.y).text(getTotal(reg));
|
||||||
g.append("rect").attr("fill", darkerColor).attr("x", x1-h).attr("y", y1).attr("width", h).attr("height", h);
|
g.append("rect")
|
||||||
g.append("text").attr("class", "regimentIcon").attr("x", x1-size).attr("y", reg.y).text(reg.icon);
|
.attr("fill", darkerColor)
|
||||||
}
|
.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
|
// move one regiment to another
|
||||||
const moveRegiment = function (reg, x, y) {
|
const moveRegiment = function (reg, x, y) {
|
||||||
|
|
@ -233,7 +310,8 @@
|
||||||
if (!el.size()) return;
|
if (!el.size()) return;
|
||||||
|
|
||||||
const duration = Math.hypot(reg.x - x, reg.y - y) * 8;
|
const duration = Math.hypot(reg.x - x, reg.y - y) * 8;
|
||||||
reg.x = x; reg.y = y;
|
reg.x = x;
|
||||||
|
reg.y = y;
|
||||||
const size = +armies.attr("box-size");
|
const size = +armies.attr("box-size");
|
||||||
const w = reg.n ? size * 4 : size * 6;
|
const w = reg.n ? size * 4 : size * 6;
|
||||||
const h = size * 2;
|
const h = size * 2;
|
||||||
|
|
@ -243,22 +321,26 @@
|
||||||
const move = d3.transition().duration(duration).ease(d3.easeSinInOut);
|
const move = d3.transition().duration(duration).ease(d3.easeSinInOut);
|
||||||
el.select("rect").transition(move).attr("x", x1(x)).attr("y", y1(y));
|
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.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.selectAll("rect:nth-of-type(2)")
|
||||||
el.select(".regimentIcon").transition(move).attr("x", x1(x)-size).attr("y", y);
|
.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);
|
||||||
|
|
||||||
const getName = function (r, regiments) {
|
const getName = function (r, regiments) {
|
||||||
const cells = pack.cells;
|
const cells = pack.cells;
|
||||||
const proper = r.n ? null :
|
const proper = r.n ? null : cells.province[r.cell] && pack.provinces[cells.province[r.cell]] ? pack.provinces[cells.province[r.cell]].name : cells.burg[r.cell] && pack.burgs[cells.burg[r.cell]] ? pack.burgs[cells.burg[r.cell]].name : null;
|
||||||
cells.province[r.cell] && pack.provinces[cells.province[r.cell]] ? pack.provinces[cells.province[r.cell]].name :
|
|
||||||
cells.burg[r.cell] && pack.burgs[cells.burg[r.cell]] ? pack.burgs[cells.burg[r.cell]].name : null
|
|
||||||
const number = nth(regiments.filter(reg => reg.n === r.n && reg.i < r.i).length + 1);
|
const number = nth(regiments.filter(reg => reg.n === r.n && reg.i < r.i).length + 1);
|
||||||
const form = r.n ? "Fleet" : "Regiment";
|
const form = r.n ? "Fleet" : "Regiment";
|
||||||
return `${number}${proper ? ` (${proper}) ` : ` `}${form}`;
|
return `${number}${proper ? ` (${proper}) ` : ` `}${form}`;
|
||||||
}
|
};
|
||||||
|
|
||||||
// get default regiment emblem
|
// get default regiment emblem
|
||||||
const getEmblem = function (r) {
|
const getEmblem = function (r) {
|
||||||
|
|
@ -267,15 +349,18 @@
|
||||||
const mainUnit = Object.entries(r.u).sort((a, b) => b[1] - a[1])[0][0]; // unit with more troops in regiment
|
const mainUnit = Object.entries(r.u).sort((a, b) => b[1] - a[1])[0][0]; // unit with more troops in regiment
|
||||||
const unit = options.military.find(u => u.name === mainUnit);
|
const unit = options.military.find(u => u.name === mainUnit);
|
||||||
return unit.icon;
|
return unit.icon;
|
||||||
}
|
};
|
||||||
|
|
||||||
const generateNote = function (r, s) {
|
const generateNote = function (r, s) {
|
||||||
const cells = pack.cells;
|
const cells = pack.cells;
|
||||||
const base = cells.burg[r.cell] && pack.burgs[cells.burg[r.cell]] ? pack.burgs[cells.burg[r.cell]].name :
|
const base = cells.burg[r.cell] && pack.burgs[cells.burg[r.cell]] ? pack.burgs[cells.burg[r.cell]].name : cells.province[r.cell] && pack.provinces[cells.province[r.cell]] ? pack.provinces[cells.province[r.cell]].fullName : null;
|
||||||
cells.province[r.cell] && pack.provinces[cells.province[r.cell]] ? pack.provinces[cells.province[r.cell]].fullName : null;
|
|
||||||
const station = base ? `${r.name} is ${r.n ? "based" : "stationed"} in ${base}. ` : "";
|
const station = base ? `${r.name} is ${r.n ? "based" : "stationed"} in ${base}. ` : "";
|
||||||
|
|
||||||
const composition = r.a ? Object.keys(r.u).map(t => `— ${t}: ${r.u[t]}`).join("\r\n") : null;
|
const composition = r.a
|
||||||
|
? Object.keys(r.u)
|
||||||
|
.map(t => `— ${t}: ${r.u[t]}`)
|
||||||
|
.join("\r\n")
|
||||||
|
: null;
|
||||||
const troops = composition ? `\r\n\r\nRegiment composition in ${options.year} ${options.eraShort}:\r\n${composition}.` : "";
|
const troops = composition ? `\r\n\r\nRegiment composition in ${options.year} ${options.eraShort}:\r\n${composition}.` : "";
|
||||||
|
|
||||||
const campaign = s.campaigns ? ra(s.campaigns) : null;
|
const campaign = s.campaigns ? ra(s.campaigns) : null;
|
||||||
|
|
@ -283,8 +368,7 @@
|
||||||
const conflict = campaign ? ` during the ${campaign.name}` : "";
|
const conflict = campaign ? ` during the ${campaign.name}` : "";
|
||||||
const legend = `Regiment was formed in ${year} ${options.era}${conflict}. ${station}${troops}`;
|
const legend = `Regiment was formed in ${year} ${options.era}${conflict}. ${station}${troops}`;
|
||||||
notes.push({id: `regiment${s.i}-${r.i}`, name: `${r.icon} ${r.name}`, legend});
|
notes.push({id: `regiment${s.i}-${r.i}`, name: `${r.icon} ${r.name}`, legend});
|
||||||
}
|
};
|
||||||
|
|
||||||
return {generate, getDefaultOptions, getName, generateNote, drawRegiments, drawRegiment, moveRegiment, getTotal, getEmblem};
|
return {generate, getDefaultOptions, getName, generateNote, drawRegiments, drawRegiment, moveRegiment, getTotal, getEmblem};
|
||||||
|
});
|
||||||
})));
|
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,26 @@
|
||||||
(function (global, factory) {
|
(function (global, factory) {
|
||||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
typeof exports === "object" && typeof module !== "undefined" ? (module.exports = factory()) : typeof define === "function" && define.amd ? define(factory) : (global.Names = factory());
|
||||||
typeof define === 'function' && define.amd ? define(factory) :
|
})(this, function () {
|
||||||
(global.Names = factory());
|
"use strict";
|
||||||
}(this, (function () { 'use strict';
|
|
||||||
|
|
||||||
let chains = [];
|
let chains = [];
|
||||||
|
|
||||||
// calculate Markov chain for a namesbase
|
// calculate Markov chain for a namesbase
|
||||||
const calculateChain = function (string) {
|
const calculateChain = function (string) {
|
||||||
const chain = [], array = string.split(",");
|
const chain = [];
|
||||||
|
const array = string.split(",");
|
||||||
|
|
||||||
for (const n of array) {
|
for (const n of array) {
|
||||||
let name = n.trim().toLowerCase();
|
let name = n.trim().toLowerCase();
|
||||||
const basic = !(/[^\u0000-\u007f]/.test(name)); // basic chars and English rules can be applied
|
const basic = !/[^\u0000-\u007f]/.test(name); // basic chars and English rules can be applied
|
||||||
|
|
||||||
// split word into pseudo-syllables
|
// split word into pseudo-syllables
|
||||||
for (let i=-1, syllable = ""; i < name.length; i += (syllable.length||1), syllable = "") {
|
for (let i = -1, syllable = ""; i < name.length; i += syllable.length || 1, syllable = "") {
|
||||||
let prev = name[i] || ""; // pre-onset letter
|
let prev = name[i] || ""; // pre-onset letter
|
||||||
let v = 0; // 0 if no vowels in syllable
|
let v = 0; // 0 if no vowels in syllable
|
||||||
|
|
||||||
for (let c = i + 1; name[c] && syllable.length < 5; c++) {
|
for (let c = i + 1; name[c] && syllable.length < 5; c++) {
|
||||||
const that = name[c], next = name[c+1]; // next char
|
const that = name[c],
|
||||||
|
next = name[c + 1]; // next char
|
||||||
syllable += that;
|
syllable += that;
|
||||||
if (syllable === " " || syllable === "-") break; // syllable starts with space or hyphen
|
if (syllable === " " || syllable === "-") break; // syllable starts with space or hyphen
|
||||||
if (!next || next === " " || next === "-") break; // no need to check
|
if (!next || next === " " || next === "-") break; // no need to check
|
||||||
|
|
@ -29,7 +29,8 @@
|
||||||
|
|
||||||
// do not split some diphthongs
|
// do not split some diphthongs
|
||||||
if (that === "y" && next === "e") continue; // 'ye'
|
if (that === "y" && next === "e") continue; // 'ye'
|
||||||
if (basic) { // English-like
|
if (basic) {
|
||||||
|
// English-like
|
||||||
if (that === "o" && next === "o") continue; // 'oo'
|
if (that === "o" && next === "o") continue; // 'oo'
|
||||||
if (that === "e" && next === "e") continue; // 'ee'
|
if (that === "e" && next === "e") continue; // 'ee'
|
||||||
if (that === "a" && next === "e") continue; // 'ae'
|
if (that === "a" && next === "e") continue; // 'ae'
|
||||||
|
|
@ -46,17 +47,20 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
return chain;
|
return chain;
|
||||||
}
|
};
|
||||||
|
|
||||||
// update chain for specific base
|
// update chain for specific base
|
||||||
const updateChain = (i) => chains[i] = nameBases[i] || nameBases[i].b ? calculateChain(nameBases[i].b) : null;
|
const updateChain = i => (chains[i] = nameBases[i] || nameBases[i].b ? calculateChain(nameBases[i].b) : null);
|
||||||
|
|
||||||
// update chains for all used bases
|
// update chains for all used bases
|
||||||
const clearChains = () => chains = [];
|
const clearChains = () => (chains = []);
|
||||||
|
|
||||||
// generate name using Markov's chain
|
// generate name using Markov's chain
|
||||||
const getBase = function (base, min, max, dupl) {
|
const getBase = function (base, min, max, dupl) {
|
||||||
if (base === undefined) {ERROR && console.error("Please define a base"); return;}
|
if (base === undefined) {
|
||||||
|
ERROR && console.error("Please define a base");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!chains[base]) updateChain(base);
|
if (!chains[base]) updateChain(base);
|
||||||
|
|
||||||
const data = chains[base];
|
const data = chains[base];
|
||||||
|
|
@ -70,12 +74,20 @@
|
||||||
if (!max) max = nameBases[base].max;
|
if (!max) max = nameBases[base].max;
|
||||||
if (dupl !== "") dupl = nameBases[base].d;
|
if (dupl !== "") dupl = nameBases[base].d;
|
||||||
|
|
||||||
let v = data[""], cur = ra(v), w = "";
|
let v = data[""],
|
||||||
|
cur = ra(v),
|
||||||
|
w = "";
|
||||||
for (let i = 0; i < 20; i++) {
|
for (let i = 0; i < 20; i++) {
|
||||||
if (cur === "") { // end of word
|
if (cur === "") {
|
||||||
if (w.length < min) {cur = ""; w = ""; v = data[""];} else break;
|
// end of word
|
||||||
|
if (w.length < min) {
|
||||||
|
cur = "";
|
||||||
|
w = "";
|
||||||
|
v = data[""];
|
||||||
|
} else break;
|
||||||
} else {
|
} else {
|
||||||
if (w.length + cur.length > max) { // word too long
|
if (w.length + cur.length > max) {
|
||||||
|
// word too long
|
||||||
if (w.length < min) w += cur;
|
if (w.length < min) w += cur;
|
||||||
break;
|
break;
|
||||||
} else v = data[last(cur)] || data[""];
|
} else v = data[last(cur)] || data[""];
|
||||||
|
|
@ -88,7 +100,7 @@
|
||||||
// parse word to get a final name
|
// parse word to get a final name
|
||||||
const l = last(w); // last letter
|
const l = last(w); // last letter
|
||||||
if (l === "'" || l === " " || l === "-") w = w.slice(0, -1); // not allow some characters at the end
|
if (l === "'" || l === " " || l === "-") w = w.slice(0, -1); // not allow some characters at the end
|
||||||
const basic = !(/[^\u0000-\u007f]/.test(w)); // true if word has only basic characters
|
const basic = !/[^\u0000-\u007f]/.test(w); // true if word has only basic characters
|
||||||
|
|
||||||
let name = [...w].reduce(function (r, c, i, d) {
|
let name = [...w].reduce(function (r, c, i, d) {
|
||||||
if (c === d[i + 1] && !dupl.includes(c)) return r; // duplication is not allowed
|
if (c === d[i + 1] && !dupl.includes(c)) return r; // duplication is not allowed
|
||||||
|
|
@ -103,7 +115,11 @@
|
||||||
}, "");
|
}, "");
|
||||||
|
|
||||||
// join the word if any part has only 1 letter
|
// join the word if any part has only 1 letter
|
||||||
if (name.split(" ").some(part => part.length < 2)) name = name.split(" ").map((p,i) => i ? p.toLowerCase() : p).join("");
|
if (name.split(" ").some(part => part.length < 2))
|
||||||
|
name = name
|
||||||
|
.split(" ")
|
||||||
|
.map((p, i) => (i ? p.toLowerCase() : p))
|
||||||
|
.join("");
|
||||||
|
|
||||||
if (name.length < 2) {
|
if (name.length < 2) {
|
||||||
ERROR && console.error("Name is too short! Random name will be selected");
|
ERROR && console.error("Name is too short! Random name will be selected");
|
||||||
|
|
@ -111,20 +127,26 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
return name;
|
return name;
|
||||||
}
|
};
|
||||||
|
|
||||||
// generate name for culture
|
// generate name for culture
|
||||||
const getCulture = function (culture, min, max, dupl) {
|
const getCulture = function (culture, min, max, dupl) {
|
||||||
if (culture === undefined) {ERROR && console.error("Please define a culture"); return;}
|
if (culture === undefined) {
|
||||||
|
ERROR && console.error("Please define a culture");
|
||||||
|
return;
|
||||||
|
}
|
||||||
const base = pack.cultures[culture].base;
|
const base = pack.cultures[culture].base;
|
||||||
return getBase(base, min, max, dupl);
|
return getBase(base, min, max, dupl);
|
||||||
}
|
};
|
||||||
|
|
||||||
// generate short name for culture
|
// generate short name for culture
|
||||||
const getCultureShort = function (culture) {
|
const getCultureShort = function (culture) {
|
||||||
if (culture === undefined) {ERROR && console.error("Please define a culture"); return;}
|
if (culture === undefined) {
|
||||||
return getBaseShort(pack.cultures[culture].base);
|
ERROR && console.error("Please define a culture");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
return getBaseShort(pack.cultures[culture].base);
|
||||||
|
};
|
||||||
|
|
||||||
// generate short name for base
|
// generate short name for base
|
||||||
const getBaseShort = function (base) {
|
const getBaseShort = function (base) {
|
||||||
|
|
@ -135,9 +157,10 @@
|
||||||
const min = nameBases[base].min - 1;
|
const min = nameBases[base].min - 1;
|
||||||
const max = Math.max(nameBases[base].max - 2, min);
|
const max = Math.max(nameBases[base].max - 2, min);
|
||||||
return getBase(base, min, max, "", 0);
|
return getBase(base, min, max, "", 0);
|
||||||
}
|
};
|
||||||
|
|
||||||
// generate state name based on capital or random name and culture-specific suffix
|
// generate state name based on capital or random name and culture-specific suffix
|
||||||
|
// prettier-ignore
|
||||||
const getState = function(name, culture, base) {
|
const getState = function(name, culture, base) {
|
||||||
if (name === undefined) {ERROR && console.error("Please define a base name"); return;}
|
if (name === undefined) {ERROR && console.error("Please define a base name"); return;}
|
||||||
if (culture === undefined && base === undefined) {ERROR && console.error("Please define a culture"); return;}
|
if (culture === undefined && base === undefined) {ERROR && console.error("Please define a culture"); return;}
|
||||||
|
|
@ -200,24 +223,28 @@
|
||||||
const getMapName = function (force) {
|
const getMapName = function (force) {
|
||||||
if (!force && locked("mapName")) return;
|
if (!force && locked("mapName")) return;
|
||||||
if (force && locked("mapName")) unlock("mapName");
|
if (force && locked("mapName")) unlock("mapName");
|
||||||
const base = P(.7) ? 2 : P(.5) ? rand(0, 6) : rand(0, 31);
|
const base = P(0.7) ? 2 : P(0.5) ? rand(0, 6) : rand(0, 31);
|
||||||
if (!nameBases[base]) {tip("Namebase is not found", false, "error"); return ""};
|
if (!nameBases[base]) {
|
||||||
|
tip("Namebase is not found", false, "error");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
const min = nameBases[base].min - 1;
|
const min = nameBases[base].min - 1;
|
||||||
const max = Math.max(nameBases[base].max - 3, min);
|
const max = Math.max(nameBases[base].max - 3, min);
|
||||||
const baseName = getBase(base, min, max, "", 0);
|
const baseName = getBase(base, min, max, "", 0);
|
||||||
const name = P(.7) ? addSuffix(baseName) : baseName;
|
const name = P(0.7) ? addSuffix(baseName) : baseName;
|
||||||
mapName.value = name;
|
mapName.value = name;
|
||||||
}
|
};
|
||||||
|
|
||||||
function addSuffix(name) {
|
function addSuffix(name) {
|
||||||
const suffix = P(.8) ? "ia" : "land";
|
const suffix = P(0.8) ? "ia" : "land";
|
||||||
if (suffix === "ia" && name.length > 6) name = name.slice(0, -(name.length-3)); else
|
if (suffix === "ia" && name.length > 6) name = name.slice(0, -(name.length - 3));
|
||||||
if (suffix === "land" && name.length > 6) name = name.slice(0, -(name.length-5));
|
else if (suffix === "land" && name.length > 6) name = name.slice(0, -(name.length - 5));
|
||||||
return validateSuffix(name, suffix);
|
return validateSuffix(name, suffix);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getNameBases = function () {
|
const getNameBases = function () {
|
||||||
// name, min length, max length, letters to allow duplication, multi-word name rate [deprecated]
|
// name, min length, max length, letters to allow duplication, multi-word name rate [deprecated]
|
||||||
|
// prettier-ignore
|
||||||
return [
|
return [
|
||||||
// real-world bases by Azgaar:
|
// real-world bases by Azgaar:
|
||||||
{name: "German", i: 0, min: 5, max: 12, d: "lt", m: 0, b: "Achern,Aichhalden,Aitern,Albbruck,Alpirsbach,Altensteig,Althengstett,Appenweier,Auggen,Wildbad,Badenen,Badenweiler,Baiersbronn,Ballrechten,Bellingen,Berghaupten,Bernau,Biberach,Biederbach,Binzen,Birkendorf,Birkenfeld,Bischweier,Blumberg,Bollen,Bollschweil,Bonndorf,Bosingen,Braunlingen,Breisach,Breisgau,Breitnau,Brigachtal,Buchenbach,Buggingen,Buhl,Buhlertal,Calw,Dachsberg,Dobel,Donaueschingen,Dornhan,Dornstetten,Dottingen,Dunningen,Durbach,Durrheim,Ebhausen,Ebringen,Efringen,Egenhausen,Ehrenkirchen,Ehrsberg,Eimeldingen,Eisenbach,Elzach,Elztal,Emmendingen,Endingen,Engelsbrand,Enz,Enzklosterle,Eschbronn,Ettenheim,Ettlingen,Feldberg,Fischerbach,Fischingen,Fluorn,Forbach,Freiamt,Freiburg,Freudenstadt,Friedenweiler,Friesenheim,Frohnd,Furtwangen,Gaggenau,Geisingen,Gengenbach,Gernsbach,Glatt,Glatten,Glottertal,Gorwihl,Gottenheim,Grafenhausen,Grenzach,Griesbach,Gutach,Gutenbach,Hag,Haiterbach,Hardt,Harmersbach,Hasel,Haslach,Hausach,Hausen,Hausern,Heitersheim,Herbolzheim,Herrenalb,Herrischried,Hinterzarten,Hochenschwand,Hofen,Hofstetten,Hohberg,Horb,Horben,Hornberg,Hufingen,Ibach,Ihringen,Inzlingen,Kandern,Kappel,Kappelrodeck,Karlsbad,Karlsruhe,Kehl,Keltern,Kippenheim,Kirchzarten,Konigsfeld,Krozingen,Kuppenheim,Kussaberg,Lahr,Lauchringen,Lauf,Laufenburg,Lautenbach,Lauterbach,Lenzkirch,Liebenzell,Loffenau,Loffingen,Lorrach,Lossburg,Mahlberg,Malsburg,Malsch,March,Marxzell,Marzell,Maulburg,Monchweiler,Muhlenbach,Mullheim,Munstertal,Murg,Nagold,Neubulach,Neuenburg,Neuhausen,Neuried,Neuweiler,Niedereschach,Nordrach,Oberharmersbach,Oberkirch,Oberndorf,Oberbach,Oberried,Oberwolfach,Offenburg,Ohlsbach,Oppenau,Ortenberg,otigheim,Ottenhofen,Ottersweier,Peterstal,Pfaffenweiler,Pfalzgrafenweiler,Pforzheim,Rastatt,Renchen,Rheinau,Rheinfelden,Rheinmunster,Rickenbach,Rippoldsau,Rohrdorf,Rottweil,Rummingen,Rust,Sackingen,Sasbach,Sasbachwalden,Schallbach,Schallstadt,Schapbach,Schenkenzell,Schiltach,Schliengen,Schluchsee,Schomberg,Schonach,Schonau,Schonenberg,Schonwald,Schopfheim,Schopfloch,Schramberg,Schuttertal,Schwenningen,Schworstadt,Seebach,Seelbach,Seewald,Sexau,Simmersfeld,Simonswald,Sinzheim,Solden,Staufen,Stegen,Steinach,Steinen,Steinmauern,Straubenhardt,Stuhlingen,Sulz,Sulzburg,Teinach,Tiefenbronn,Tiengen,Titisee,Todtmoos,Todtnau,Todtnauberg,Triberg,Tunau,Tuningen,uhlingen,Unterkirnach,Reichenbach,Utzenfeld,Villingen,Villingendorf,Vogtsburg,Vohrenbach,Waldachtal,Waldbronn,Waldkirch,Waldshut,Wehr,Weil,Weilheim,Weisenbach,Wembach,Wieden,Wiesental,Wildberg,Winzeln,Wittlingen,Wittnau,Wolfach,Wutach,Wutoschingen,Wyhlen,Zavelstein"},
|
{name: "German", i: 0, min: 5, max: 12, d: "lt", m: 0, b: "Achern,Aichhalden,Aitern,Albbruck,Alpirsbach,Altensteig,Althengstett,Appenweier,Auggen,Wildbad,Badenen,Badenweiler,Baiersbronn,Ballrechten,Bellingen,Berghaupten,Bernau,Biberach,Biederbach,Binzen,Birkendorf,Birkenfeld,Bischweier,Blumberg,Bollen,Bollschweil,Bonndorf,Bosingen,Braunlingen,Breisach,Breisgau,Breitnau,Brigachtal,Buchenbach,Buggingen,Buhl,Buhlertal,Calw,Dachsberg,Dobel,Donaueschingen,Dornhan,Dornstetten,Dottingen,Dunningen,Durbach,Durrheim,Ebhausen,Ebringen,Efringen,Egenhausen,Ehrenkirchen,Ehrsberg,Eimeldingen,Eisenbach,Elzach,Elztal,Emmendingen,Endingen,Engelsbrand,Enz,Enzklosterle,Eschbronn,Ettenheim,Ettlingen,Feldberg,Fischerbach,Fischingen,Fluorn,Forbach,Freiamt,Freiburg,Freudenstadt,Friedenweiler,Friesenheim,Frohnd,Furtwangen,Gaggenau,Geisingen,Gengenbach,Gernsbach,Glatt,Glatten,Glottertal,Gorwihl,Gottenheim,Grafenhausen,Grenzach,Griesbach,Gutach,Gutenbach,Hag,Haiterbach,Hardt,Harmersbach,Hasel,Haslach,Hausach,Hausen,Hausern,Heitersheim,Herbolzheim,Herrenalb,Herrischried,Hinterzarten,Hochenschwand,Hofen,Hofstetten,Hohberg,Horb,Horben,Hornberg,Hufingen,Ibach,Ihringen,Inzlingen,Kandern,Kappel,Kappelrodeck,Karlsbad,Karlsruhe,Kehl,Keltern,Kippenheim,Kirchzarten,Konigsfeld,Krozingen,Kuppenheim,Kussaberg,Lahr,Lauchringen,Lauf,Laufenburg,Lautenbach,Lauterbach,Lenzkirch,Liebenzell,Loffenau,Loffingen,Lorrach,Lossburg,Mahlberg,Malsburg,Malsch,March,Marxzell,Marzell,Maulburg,Monchweiler,Muhlenbach,Mullheim,Munstertal,Murg,Nagold,Neubulach,Neuenburg,Neuhausen,Neuried,Neuweiler,Niedereschach,Nordrach,Oberharmersbach,Oberkirch,Oberndorf,Oberbach,Oberried,Oberwolfach,Offenburg,Ohlsbach,Oppenau,Ortenberg,otigheim,Ottenhofen,Ottersweier,Peterstal,Pfaffenweiler,Pfalzgrafenweiler,Pforzheim,Rastatt,Renchen,Rheinau,Rheinfelden,Rheinmunster,Rickenbach,Rippoldsau,Rohrdorf,Rottweil,Rummingen,Rust,Sackingen,Sasbach,Sasbachwalden,Schallbach,Schallstadt,Schapbach,Schenkenzell,Schiltach,Schliengen,Schluchsee,Schomberg,Schonach,Schonau,Schonenberg,Schonwald,Schopfheim,Schopfloch,Schramberg,Schuttertal,Schwenningen,Schworstadt,Seebach,Seelbach,Seewald,Sexau,Simmersfeld,Simonswald,Sinzheim,Solden,Staufen,Stegen,Steinach,Steinen,Steinmauern,Straubenhardt,Stuhlingen,Sulz,Sulzburg,Teinach,Tiefenbronn,Tiengen,Titisee,Todtmoos,Todtnau,Todtnauberg,Triberg,Tunau,Tuningen,uhlingen,Unterkirnach,Reichenbach,Utzenfeld,Villingen,Villingendorf,Vogtsburg,Vohrenbach,Waldachtal,Waldbronn,Waldkirch,Waldshut,Wehr,Weil,Weilheim,Weisenbach,Wembach,Wieden,Wiesental,Wildberg,Winzeln,Wittlingen,Wittnau,Wolfach,Wutach,Wutoschingen,Wyhlen,Zavelstein"},
|
||||||
|
|
@ -245,7 +272,7 @@
|
||||||
{name: "Celtic", i: 22, min: 4, max: 12, d: "nld", m: 0, b: "Aberaman,Aberangell,Aberarth,Aberavon,Aberbanc,Aberbargoed,Aberbeeg,Abercanaid,Abercarn,Abercastle,Abercegir,Abercraf,Abercregan,Abercych,Abercynon,Aberdare,Aberdaron,Aberdaugleddau,Aberdeen,Aberdulais,Aberdyfi,Aberedw,Abereiddy,Abererch,Abereron,Aberfan,Aberffraw,Aberffrwd,Abergavenny,Abergele,Aberglasslyn,Abergorlech,Abergwaun,Abergwesyn,Abergwili,Abergwynfi,Abergwyngregyn,Abergynolwyn,Aberhafesp,Aberhonddu,Aberkenfig,Aberllefenni,Abermain,Abermaw,Abermorddu,Abermule,Abernant,Aberpennar,Aberporth,Aberriw,Abersoch,Abersychan,Abertawe,Aberteifi,Aberthin,Abertillery,Abertridwr,Aberystwyth,Achininver,Afonhafren,Alisaha,Antinbhearmor,Ardenna,Attacon,Beira,Bhrura,Boioduro,Bona,Boudobriga,Bravon,Brigant,Briganta,Briva,Cambodunum,Cambra,Caracta,Catumagos,Centobriga,Ceredigion,Chalain,Dinn,Diwa,Dubingen,Duro,Ebora,Ebruac,Eburodunum,Eccles,Eighe,Eireann,Ferkunos,Genua,Ghrainnse,Inbhear,Inbhir,Inbhirair,Innerleithen,Innerleven,Innerwick,Inver,Inveraldie,Inverallan,Inveralmond,Inveramsay,Inveran,Inveraray,Inverarnan,Inverbervie,Inverclyde,Inverell,Inveresk,Inverfarigaig,Invergarry,Invergordon,Invergowrie,Inverhaddon,Inverkeilor,Inverkeithing,Inverkeithney,Inverkip,Inverleigh,Inverleith,Inverloch,Inverlochlarig,Inverlochy,Invermay,Invermoriston,Inverness,Inveroran,Invershin,Inversnaid,Invertrossachs,Inverugie,Inveruglas,Inverurie,Kilninver,Kirkcaldy,Kirkintilloch,Krake,Latense,Leming,Lindomagos,Llanaber,Lochinver,Lugduno,Magoduro,Monmouthshire,Narann,Novioduno,Nowijonago,Octoduron,Penning,Pheofharain,Ricomago,Rossinver,Salodurum,Seguia,Sentica,Theorsa,Uige,Vitodurum,Windobona"},
|
{name: "Celtic", i: 22, min: 4, max: 12, d: "nld", m: 0, b: "Aberaman,Aberangell,Aberarth,Aberavon,Aberbanc,Aberbargoed,Aberbeeg,Abercanaid,Abercarn,Abercastle,Abercegir,Abercraf,Abercregan,Abercych,Abercynon,Aberdare,Aberdaron,Aberdaugleddau,Aberdeen,Aberdulais,Aberdyfi,Aberedw,Abereiddy,Abererch,Abereron,Aberfan,Aberffraw,Aberffrwd,Abergavenny,Abergele,Aberglasslyn,Abergorlech,Abergwaun,Abergwesyn,Abergwili,Abergwynfi,Abergwyngregyn,Abergynolwyn,Aberhafesp,Aberhonddu,Aberkenfig,Aberllefenni,Abermain,Abermaw,Abermorddu,Abermule,Abernant,Aberpennar,Aberporth,Aberriw,Abersoch,Abersychan,Abertawe,Aberteifi,Aberthin,Abertillery,Abertridwr,Aberystwyth,Achininver,Afonhafren,Alisaha,Antinbhearmor,Ardenna,Attacon,Beira,Bhrura,Boioduro,Bona,Boudobriga,Bravon,Brigant,Briganta,Briva,Cambodunum,Cambra,Caracta,Catumagos,Centobriga,Ceredigion,Chalain,Dinn,Diwa,Dubingen,Duro,Ebora,Ebruac,Eburodunum,Eccles,Eighe,Eireann,Ferkunos,Genua,Ghrainnse,Inbhear,Inbhir,Inbhirair,Innerleithen,Innerleven,Innerwick,Inver,Inveraldie,Inverallan,Inveralmond,Inveramsay,Inveran,Inveraray,Inverarnan,Inverbervie,Inverclyde,Inverell,Inveresk,Inverfarigaig,Invergarry,Invergordon,Invergowrie,Inverhaddon,Inverkeilor,Inverkeithing,Inverkeithney,Inverkip,Inverleigh,Inverleith,Inverloch,Inverlochlarig,Inverlochy,Invermay,Invermoriston,Inverness,Inveroran,Invershin,Inversnaid,Invertrossachs,Inverugie,Inveruglas,Inverurie,Kilninver,Kirkcaldy,Kirkintilloch,Krake,Latense,Leming,Lindomagos,Llanaber,Lochinver,Lugduno,Magoduro,Monmouthshire,Narann,Novioduno,Nowijonago,Octoduron,Penning,Pheofharain,Ricomago,Rossinver,Salodurum,Seguia,Sentica,Theorsa,Uige,Vitodurum,Windobona"},
|
||||||
{name: "Mesopotamian", i: 23, min: 4, max: 9, d: "srpl", m: .1, b: "Adab,Akkad,Akshak,Amnanum,Arbid,Arpachiyah,Arrapha,Assur,Babilim,Badtibira,Balawat,Barsip,Borsippa,Carchemish,Chagar Bazar,Chuera,Ctesiphon ,Der,Dilbat,Diniktum,Doura,Durkurigalzu,Ekallatum,Emar,Erbil,Eridu,Eshnunn,Fakhariya ,Gawra,Girsu,Hadatu,Hamoukar,Haradum,Harran,Hatra,Idu,Irisagrig,Isin,Jemdet,Kahat,Kartukulti,Khaiber,Kish ,Kisurra,Kuara,Kutha,Lagash,Larsa ,Leilan,Marad,Mardaman,Mari,Mashkan,Mumbaqat ,Nabada,Nagar,Nerebtum,Nimrud,Nineveh,Nippur,Nuzi,Qalatjarmo,Qatara,Rawda,Seleucia,Shaduppum,Shanidar,Sharrukin,Shemshara,Shibaniba,Shuruppak,Sippar,Tarbisu,Tellagrab,Tellessawwan,Tellessweyhat,Tellhassuna,Telltaya,Telul,Terqa,Thalathat,Tutub,Ubaid ,Umma,Ur,Urfa,Urkesh,Uruk,Urum,Zabalam,Zenobia"},
|
{name: "Mesopotamian", i: 23, min: 4, max: 9, d: "srpl", m: .1, b: "Adab,Akkad,Akshak,Amnanum,Arbid,Arpachiyah,Arrapha,Assur,Babilim,Badtibira,Balawat,Barsip,Borsippa,Carchemish,Chagar Bazar,Chuera,Ctesiphon ,Der,Dilbat,Diniktum,Doura,Durkurigalzu,Ekallatum,Emar,Erbil,Eridu,Eshnunn,Fakhariya ,Gawra,Girsu,Hadatu,Hamoukar,Haradum,Harran,Hatra,Idu,Irisagrig,Isin,Jemdet,Kahat,Kartukulti,Khaiber,Kish ,Kisurra,Kuara,Kutha,Lagash,Larsa ,Leilan,Marad,Mardaman,Mari,Mashkan,Mumbaqat ,Nabada,Nagar,Nerebtum,Nimrud,Nineveh,Nippur,Nuzi,Qalatjarmo,Qatara,Rawda,Seleucia,Shaduppum,Shanidar,Sharrukin,Shemshara,Shibaniba,Shuruppak,Sippar,Tarbisu,Tellagrab,Tellessawwan,Tellessweyhat,Tellhassuna,Telltaya,Telul,Terqa,Thalathat,Tutub,Ubaid ,Umma,Ur,Urfa,Urkesh,Uruk,Urum,Zabalam,Zenobia"},
|
||||||
{name: "Iranian", i: 24, min: 5, max: 11, d: "", m: .1, b: "Abali,Abrisham,Absard,Abuzeydabad,Afus,Alavicheh,Alikosh,Amol,Anarak,Anbar,Andisheh,Anshan,Aran,Ardabil,Arderica,Ardestan,Arjomand,Asgaran,Asgharabad,Ashian,Awan,Babajan,Badrud,Bafran,Baghestan,Baghshad,Bahadoran,Baharan Shahr,Baharestan,Bakun,Bam,Baqershahr,Barzok,Bastam,Behistun,Bitistar,Bumahen,Bushehr,Chadegan,Chahardangeh,Chamgardan,Chermahin,Choghabonut,Chugan,Damaneh,Damavand,Darabgard,Daran,Dastgerd,Dehaq,Dehaqan,Dezful,Dizicheh,Dorcheh,Dowlatabad,Duruntash,Ecbatana,Eslamshahr,Estakhr,Ezhiyeh,Falavarjan,Farrokhi,Fasham,Ferdowsieh,Fereydunshahr,Ferunabad,Firuzkuh,Fuladshahr,Ganjdareh,Ganzak,Gaz,Geoy,Godin,Goldasht,Golestan,Golpayegan,Golshahr,Golshan,Gorgab,Guged,Habibabad,Hafshejan,Hajjifiruz,Hana,Harand,Hasanabad,Hasanlu,Hashtgerd,Hecatompylos,Hormirzad,Imanshahr,Isfahan,Jandaq,Javadabad,Jiroft,Jowsheqan ,Jowzdan,Kabnak,Kahriz Sang,Kahrizak,Kangavar,Karaj,Karkevand,Kashan,Kelishad,Kermanshah,Khaledabad,Khansar,Khorramabad,Khur,Khvorzuq,Kilan,Komeh,Komeshcheh,Konar,Kuhpayeh,Kul,Kushk,Lavasan,Laybid,Liyan,Lyan,Mahabad,Mahallat,Majlesi,Malard,Manzariyeh,Marlik,Meshkat,Meymeh,Miandasht,Mish,Mobarakeh,Nahavand,Nain,Najafabad,Naqshe,Narezzash,Nasimshahr,Nasirshahr,Nasrabad,Natanz,Neyasar,Nikabad,Nimvar,Nushabad,Pakdasht,Parand,Pardis,Parsa,Pasargadai,Patigrabana,Pir Bakran,Pishva,Qahderijan,Qahjaverestan,Qamsar,Qarchak,Qods,Rabat,Ray-shahr,Rezvanshahr,Rhages,Robat Karim,Rozveh,Rudehen,Sabashahr,Safadasht,Sagzi,Salehieh,Sandal,Sarvestan,Sedeh,Sefidshahr,Semirom,Semnan,Shadpurabad,Shah,Shahdad,Shahedshahr,Shahin,Shahpour,Shahr,Shahreza,Shahriar,Sharifabad,Shemshak,Shiraz,Shushan,Shushtar,Sialk,Sin,Sukhteh,Tabas,Tabriz,Takhte,Talkhuncheh,Talli,Tarq,Temukan,Tepe,Tiran,Tudeshk,Tureng,Urmia,Vahidieh,Vahrkana,Vanak,Varamin,Varnamkhast,Varzaneh,Vazvan,Yahya,Yarim,Yasuj,Zarrin Shahr,Zavareh,Zayandeh,Zazeran,Ziar,Zibashahr,Zranka"},
|
{name: "Iranian", i: 24, min: 5, max: 11, d: "", m: .1, b: "Abali,Abrisham,Absard,Abuzeydabad,Afus,Alavicheh,Alikosh,Amol,Anarak,Anbar,Andisheh,Anshan,Aran,Ardabil,Arderica,Ardestan,Arjomand,Asgaran,Asgharabad,Ashian,Awan,Babajan,Badrud,Bafran,Baghestan,Baghshad,Bahadoran,Baharan Shahr,Baharestan,Bakun,Bam,Baqershahr,Barzok,Bastam,Behistun,Bitistar,Bumahen,Bushehr,Chadegan,Chahardangeh,Chamgardan,Chermahin,Choghabonut,Chugan,Damaneh,Damavand,Darabgard,Daran,Dastgerd,Dehaq,Dehaqan,Dezful,Dizicheh,Dorcheh,Dowlatabad,Duruntash,Ecbatana,Eslamshahr,Estakhr,Ezhiyeh,Falavarjan,Farrokhi,Fasham,Ferdowsieh,Fereydunshahr,Ferunabad,Firuzkuh,Fuladshahr,Ganjdareh,Ganzak,Gaz,Geoy,Godin,Goldasht,Golestan,Golpayegan,Golshahr,Golshan,Gorgab,Guged,Habibabad,Hafshejan,Hajjifiruz,Hana,Harand,Hasanabad,Hasanlu,Hashtgerd,Hecatompylos,Hormirzad,Imanshahr,Isfahan,Jandaq,Javadabad,Jiroft,Jowsheqan ,Jowzdan,Kabnak,Kahriz Sang,Kahrizak,Kangavar,Karaj,Karkevand,Kashan,Kelishad,Kermanshah,Khaledabad,Khansar,Khorramabad,Khur,Khvorzuq,Kilan,Komeh,Komeshcheh,Konar,Kuhpayeh,Kul,Kushk,Lavasan,Laybid,Liyan,Lyan,Mahabad,Mahallat,Majlesi,Malard,Manzariyeh,Marlik,Meshkat,Meymeh,Miandasht,Mish,Mobarakeh,Nahavand,Nain,Najafabad,Naqshe,Narezzash,Nasimshahr,Nasirshahr,Nasrabad,Natanz,Neyasar,Nikabad,Nimvar,Nushabad,Pakdasht,Parand,Pardis,Parsa,Pasargadai,Patigrabana,Pir Bakran,Pishva,Qahderijan,Qahjaverestan,Qamsar,Qarchak,Qods,Rabat,Ray-shahr,Rezvanshahr,Rhages,Robat Karim,Rozveh,Rudehen,Sabashahr,Safadasht,Sagzi,Salehieh,Sandal,Sarvestan,Sedeh,Sefidshahr,Semirom,Semnan,Shadpurabad,Shah,Shahdad,Shahedshahr,Shahin,Shahpour,Shahr,Shahreza,Shahriar,Sharifabad,Shemshak,Shiraz,Shushan,Shushtar,Sialk,Sin,Sukhteh,Tabas,Tabriz,Takhte,Talkhuncheh,Talli,Tarq,Temukan,Tepe,Tiran,Tudeshk,Tureng,Urmia,Vahidieh,Vahrkana,Vanak,Varamin,Varnamkhast,Varzaneh,Vazvan,Yahya,Yarim,Yasuj,Zarrin Shahr,Zavareh,Zayandeh,Zazeran,Ziar,Zibashahr,Zranka"},
|
||||||
{name: "Hawaiian", i: 25, min: 5, max: 10, d: "auo", m: 1, b: "Aapueo,Ahoa,Ahuakaio,Ahuakamalii,Ahuakeio,Ahupau,Aki,Alaakua,Alae,Alaeloa,Alaenui,Alamihi,Aleamai,Alena,Alio,Aupokopoko,Auwahi,Hahakea,Haiku,Halakaa,Halehaku,Halehana,Halemano,Haleu,Haliimaile,Hamakuapoko,Hamoa,Hanakaoo,Hanaulu,Hanawana,Hanehoi,Haneoo,Haou,Hikiaupea,Hoalua,Hokuula,Honohina,Honokahua,Honokala,Honokalani,Honokeana,Honokohau,Honokowai,Honolua,Honolulu,Honolulunui,Honomaele,Honomanu,Hononana,Honopou,Hoolawa,Hopenui,Hualele,Huelo,Hulaia,Ihuula,Ilikahi,Interisland,Kaalaea,Kaalelehinale,Kaapahu,Kaehoeho,Kaeleku,Kaeo,Kahakuloa,Kahalawe,Kahalawe,Kahalehili,Kahana,Kahilo,Kahuai,Kaiaula,Kailihiakoko,Kailua,Kainehe,Kakalahale,Kakanoni,Kakio,Kakiweka,Kalena,Kalenanui,Kaleoaihe,Kalepa,Kaliae,Kalialinui,Kalihi,Kalihi,Kalihi,Kalimaohe,Kaloi,Kamani,Kamaole,Kamehame,Kanahena,Kanaio,Kaniaula,Kaonoulu,Kaopa,Kapaloa,Kapaula,Kapewakua,Kapohue,Kapuaikini,Kapunakea,Kapuuomahuka,Kauau,Kauaula,Kaukuhalahala,Kaulalo,Kaulanamoa,Kauluohana,Kaumahalua,Kaumakani,Kaumanu,Kaunauhane,Kaunuahane,Kaupakulua,Kawaipapa,Kawaloa,Kawaloa,Kawalua,Kawela,Keaa,Keaalii,Keaaula,Keahua,Keahuapono,Keakuapauaela,Kealahou,Keanae,Keauhou,Kekuapawela,Kelawea,Keokea,Keopuka,Kepio,Kihapuhala,Kikoo,Kilolani,Kipapa,Koakupuna,Koali,Koananai,Koheo,Kolea,Kolokolo,Kooka,Kopili,Kou,Kualapa,Kuhiwa,Kuholilea,Kuhua,Kuia,Kuiaha,Kuikui,Kukoae,Kukohia,Kukuiaeo,Kukuioolu,Kukuipuka,Kukuiula,Kulahuhu,Kumunui,Lapakea,Lapalapaiki,Lapueo,Launiupoko,Loiloa,Lole,Lualailua,Maalo,Mahinahina,Mahulua,Maiana,Mailepai,Makaakini,Makaalae,Makaehu,Makaiwa,Makaliua,Makapipi,Makapuu,Makawao,Makila,Mala,Maluaka,Mamalu,Manawaiapiki,Manawainui,Maulili,Mehamenui,Miana,Mikimiki,Moalii,Moanui,Mohopili,Mohopilo,Mokae,Mokuia,Mokupapa,Mooiki,Mooloa,Moomuku,Muolea,Nahuakamalii,Nailiilipoko,Nakaaha,Nakalepo,Nakaohu,Nakapehu,Nakula,Napili,Niniau,Niumalu,Nuu,Ohia,Oloewa,Olowalu,Omaopio,Onau,Onouli,Opaeula,Opana,Opikoula,Paakea,Paeahu,Paehala,Paeohi,Pahoa,Paia,Pakakia,Pakala,Palauea,Palemo,Panaewa,Paniau,Papaaea,Papaanui,Papaauhau,Papahawahawa,Papaka,Papauluana,Pauku,Paunau,Pauwalu,Pauwela,Peahi,Piapia,Pohakanele,Pohoula,Polaiki,Polanui,Polapola,Polua,Poopoo,Popoiwi,Popoloa,Poponui,Poupouwela,Puaa,Puaaluu,Puahoowali,Puakea,Puako,Pualaea,Puehuehu,Puekahi,Pueokauiki,Pukaauhuhu,Pukalani,Pukuilua,Pulehu,Pulehuiki,Pulehunui,Punaluu,Puolua,Puou,Puuhaehae,Puuhaoa,Puuiki,Puuki,Puukohola,Puulani,Puumaneoneo,Puunau,Puunoa,Puuomaiai,Puuomaile,Uaoa,Uhao,Ukumehame,Ulaino,Ulumalu,Unknown,Various,Wahikuli,Waiahole,Waiakoa,Waianae,Waianu,Waiawa,Waiehu,Waieli,Waihee,Waikapu,Wailamoa,Wailaulau,Wailua,Wailuku,Wainee,Waiohole,Waiohonu,Waiohue,Waiohuli,Waiokama,Waiokila,Waiopai,Waiopua,Waipao,Waipio,Waipioiki,Waipionui,Waipouli,Wakiu,Wananalua"},
|
{name: "Hawaiian", i: 25, min: 5, max: 10, d: "auo", m: 1, b: "Aapueo,Ahoa,Ahuakaio,Ahuakamalii,Ahuakeio,Ahupau,Aki,Alaakua,Alae,Alaeloa,Alaenui,Alamihi,Aleamai,Alena,Alio,Aupokopoko,Auwahi,Hahakea,Haiku,Halakaa,Halehaku,Halehana,Halemano,Haleu,Haliimaile,Hamakuapoko,Hamoa,Hanakaoo,Hanaulu,Hanawana,Hanehoi,Haneoo,Haou,Hikiaupea,Hoalua,Hokuula,Honohina,Honokahua,Honokala,Honokalani,Honokeana,Honokohau,Honokowai,Honolua,Honolulu,Honolulunui,Honomaele,Honomanu,Hononana,Honopou,Hoolawa,Hopenui,Hualele,Huelo,Hulaia,Ihuula,Ilikahi,Kaalaea,Kaalelehinale,Kaapahu,Kaehoeho,Kaeleku,Kaeo,Kahakuloa,Kahalawe,Kahalawe,Kahalehili,Kahana,Kahilo,Kahuai,Kaiaula,Kailihiakoko,Kailua,Kainehe,Kakalahale,Kakanoni,Kakio,Kakiweka,Kalena,Kalenanui,Kaleoaihe,Kalepa,Kaliae,Kalialinui,Kalihi,Kalihi,Kalihi,Kalimaohe,Kaloi,Kamani,Kamaole,Kamehame,Kanahena,Kanaio,Kaniaula,Kaonoulu,Kaopa,Kapaloa,Kapaula,Kapewakua,Kapohue,Kapuaikini,Kapunakea,Kapuuomahuka,Kauau,Kauaula,Kaukuhalahala,Kaulalo,Kaulanamoa,Kauluohana,Kaumahalua,Kaumakani,Kaumanu,Kaunauhane,Kaunuahane,Kaupakulua,Kawaipapa,Kawaloa,Kawaloa,Kawalua,Kawela,Keaa,Keaalii,Keaaula,Keahua,Keahuapono,Keakuapauaela,Kealahou,Keanae,Keauhou,Kekuapawela,Kelawea,Keokea,Keopuka,Kepio,Kihapuhala,Kikoo,Kilolani,Kipapa,Koakupuna,Koali,Koananai,Koheo,Kolea,Kolokolo,Kooka,Kopili,Kou,Kualapa,Kuhiwa,Kuholilea,Kuhua,Kuia,Kuiaha,Kuikui,Kukoae,Kukohia,Kukuiaeo,Kukuioolu,Kukuipuka,Kukuiula,Kulahuhu,Kumunui,Lapakea,Lapalapaiki,Lapueo,Launiupoko,Loiloa,Lole,Lualailua,Maalo,Mahinahina,Mahulua,Maiana,Mailepai,Makaakini,Makaalae,Makaehu,Makaiwa,Makaliua,Makapipi,Makapuu,Makawao,Makila,Mala,Maluaka,Mamalu,Manawaiapiki,Manawainui,Maulili,Mehamenui,Miana,Mikimiki,Moalii,Moanui,Mohopili,Mohopilo,Mokae,Mokuia,Mokupapa,Mooiki,Mooloa,Moomuku,Muolea,Nahuakamalii,Nailiilipoko,Nakaaha,Nakalepo,Nakaohu,Nakapehu,Nakula,Napili,Niniau,Niumalu,Nuu,Ohia,Oloewa,Olowalu,Omaopio,Onau,Onouli,Opaeula,Opana,Opikoula,Paakea,Paeahu,Paehala,Paeohi,Pahoa,Paia,Pakakia,Pakala,Palauea,Palemo,Panaewa,Paniau,Papaaea,Papaanui,Papaauhau,Papahawahawa,Papaka,Papauluana,Pauku,Paunau,Pauwalu,Pauwela,Peahi,Piapia,Pohakanele,Pohoula,Polaiki,Polanui,Polapola,Polua,Poopoo,Popoiwi,Popoloa,Poponui,Poupouwela,Puaa,Puaaluu,Puahoowali,Puakea,Puako,Pualaea,Puehuehu,Puekahi,Pueokauiki,Pukaauhuhu,Pukalani,Pukuilua,Pulehu,Pulehuiki,Pulehunui,Punaluu,Puolua,Puou,Puuhaehae,Puuhaoa,Puuiki,Puuki,Puukohola,Puulani,Puumaneoneo,Puunau,Puunoa,Puuomaiai,Puuomaile,Uaoa,Uhao,Ukumehame,Ulaino,Ulumalu,Wahikuli,Waiahole,Waiakoa,Waianae,Waianu,Waiawa,Waiehu,Waieli,Waihee,Waikapu,Wailamoa,Wailaulau,Wailua,Wailuku,Wainee,Waiohole,Waiohonu,Waiohue,Waiohuli,Waiokama,Waiokila,Waiopai,Waiopua,Waipao,Waipio,Waipioiki,Waipionui,Waipouli,Wakiu,Wananalua"},
|
||||||
{name: "Karnataka", i: 26, min: 5, max: 11, d: "tnl", m: 0, b: "Adityapatna,Adyar,Afzalpur,Aland,Alnavar,Alur,Ambikanagara,Anekal,Ankola,Annigeri,Arkalgud,Arsikere,Athni,Aurad,Badami,Bagalkot,Bagepalli,Bail,Bajpe,Bangalore,Bangarapet,Bankapura,Bannur,Bantval,Basavakalyan,Basavana,Belgaum,Beltangadi,Belur,Bhadravati,Bhalki,Bhatkal,Bhimarayanagudi,Bidar,Bijapur,Bilgi,Birur,Bommasandra,Byadgi,Challakere,Chamarajanagar,Channagiri,Channapatna,Channarayapatna,Chik,Chikmagalur,Chiknayakanhalli,Chikodi,Chincholi,Chintamani,Chitapur,Chitgoppa,Chitradurga,Dandeli,Dargajogihalli,Devadurga,Devanahalli,Dod,Donimalai,Gadag,Gajendragarh,Gangawati,Gauribidanur,Gokak,Gonikoppal,Gubbi,Gudibanda,Gulbarga,Guledgudda,Gundlupet,Gurmatkal,Haliyal,Hangal,Harapanahalli,Harihar,Hassan,Hatti,Haveri,Hebbagodi,Heggadadevankote,Hirekerur,Holalkere,Hole,Homnabad,Honavar,Honnali,Hoovina,Hosakote,Hosanagara,Hosdurga,Hospet,Hubli,Hukeri,Hungund,Hunsur,Ilkal,Indi,Jagalur,Jamkhandi,Jevargi,Jog,Kadigenahalli,Kadur,Kalghatgi,Kamalapuram,Kampli,Kanakapura,Karkal,Karwar,Khanapur,Kodiyal,Kolar,Kollegal,Konnur,Koppa,Koppal,Koratagere,Kotturu,Krishnarajanagara,Krishnarajasagara,Krishnarajpet,Kudchi,Kudligi,Kudremukh,Kumta,Kundapura,Kundgol,Kunigal,Kurgunta,Kushalnagar,Kushtagi,Lakshmeshwar,Lingsugur,Londa,Maddur,Madhugiri,Madikeri,Mahalingpur,Malavalli,Mallar,Malur,Mandya,Mangalore,Manvi,Molakalmuru,Mudalgi,Mudbidri,Muddebihal,Mudgal,Mudhol,Mudigere,Mulbagal,Mulgund,Mulki,Mulur,Mundargi,Mundgod,Munirabad,Mysore,Nagamangala,Nanjangud,Narasimharajapura,Naregal,Nargund,Navalgund,Nipani,Pandavapura,Pavagada,Piriyapatna,Pudu,Puttur,Rabkavi,Raichur,Ramanagaram,Ramdurg,Ranibennur,Raybag,Robertson,Ron,Sadalgi,Sagar,Sakleshpur,Saligram,Sandur,Sankeshwar,Saundatti,Savanur,Sedam,Shahabad,Shahpur,Shaktinagar,Shiggaon,Shikarpur,Shirhatti,Shorapur,Shrirangapattana,Siddapur,Sidlaghatta,Sindgi,Sindhnur,Sira,Siralkoppa,Sirsi,Siruguppa,Somvarpet,Sorab,Sringeri,Srinivaspur,Sulya,Talikota,Tarikere,Tekkalakote,Terdal,Thumbe,Tiptur,Tirthahalli,Tirumakudal,Tumkur,Turuvekere,Udupi,Vijayapura,Wadi,Yadgir,Yelandur,Yelbarga,Yellapur,Yenagudde"},
|
{name: "Karnataka", i: 26, min: 5, max: 11, d: "tnl", m: 0, b: "Adityapatna,Adyar,Afzalpur,Aland,Alnavar,Alur,Ambikanagara,Anekal,Ankola,Annigeri,Arkalgud,Arsikere,Athni,Aurad,Badami,Bagalkot,Bagepalli,Bail,Bajpe,Bangalore,Bangarapet,Bankapura,Bannur,Bantval,Basavakalyan,Basavana,Belgaum,Beltangadi,Belur,Bhadravati,Bhalki,Bhatkal,Bhimarayanagudi,Bidar,Bijapur,Bilgi,Birur,Bommasandra,Byadgi,Challakere,Chamarajanagar,Channagiri,Channapatna,Channarayapatna,Chik,Chikmagalur,Chiknayakanhalli,Chikodi,Chincholi,Chintamani,Chitapur,Chitgoppa,Chitradurga,Dandeli,Dargajogihalli,Devadurga,Devanahalli,Dod,Donimalai,Gadag,Gajendragarh,Gangawati,Gauribidanur,Gokak,Gonikoppal,Gubbi,Gudibanda,Gulbarga,Guledgudda,Gundlupet,Gurmatkal,Haliyal,Hangal,Harapanahalli,Harihar,Hassan,Hatti,Haveri,Hebbagodi,Heggadadevankote,Hirekerur,Holalkere,Hole,Homnabad,Honavar,Honnali,Hoovina,Hosakote,Hosanagara,Hosdurga,Hospet,Hubli,Hukeri,Hungund,Hunsur,Ilkal,Indi,Jagalur,Jamkhandi,Jevargi,Jog,Kadigenahalli,Kadur,Kalghatgi,Kamalapuram,Kampli,Kanakapura,Karkal,Karwar,Khanapur,Kodiyal,Kolar,Kollegal,Konnur,Koppa,Koppal,Koratagere,Kotturu,Krishnarajanagara,Krishnarajasagara,Krishnarajpet,Kudchi,Kudligi,Kudremukh,Kumta,Kundapura,Kundgol,Kunigal,Kurgunta,Kushalnagar,Kushtagi,Lakshmeshwar,Lingsugur,Londa,Maddur,Madhugiri,Madikeri,Mahalingpur,Malavalli,Mallar,Malur,Mandya,Mangalore,Manvi,Molakalmuru,Mudalgi,Mudbidri,Muddebihal,Mudgal,Mudhol,Mudigere,Mulbagal,Mulgund,Mulki,Mulur,Mundargi,Mundgod,Munirabad,Mysore,Nagamangala,Nanjangud,Narasimharajapura,Naregal,Nargund,Navalgund,Nipani,Pandavapura,Pavagada,Piriyapatna,Pudu,Puttur,Rabkavi,Raichur,Ramanagaram,Ramdurg,Ranibennur,Raybag,Robertson,Ron,Sadalgi,Sagar,Sakleshpur,Saligram,Sandur,Sankeshwar,Saundatti,Savanur,Sedam,Shahabad,Shahpur,Shaktinagar,Shiggaon,Shikarpur,Shirhatti,Shorapur,Shrirangapattana,Siddapur,Sidlaghatta,Sindgi,Sindhnur,Sira,Siralkoppa,Sirsi,Siruguppa,Somvarpet,Sorab,Sringeri,Srinivaspur,Sulya,Talikota,Tarikere,Tekkalakote,Terdal,Thumbe,Tiptur,Tirthahalli,Tirumakudal,Tumkur,Turuvekere,Udupi,Vijayapura,Wadi,Yadgir,Yelandur,Yelbarga,Yellapur,Yenagudde"},
|
||||||
{name: "Quechua", i: 27, min: 6, max: 12, d: "l", m: 0, b: "Altomisayoq,Ancash,Andahuaylas,Apachekta,Apachita,Apu ,Apurimac,Arequipa,Atahuallpa,Atawalpa,Atico,Ayacucho,Ayllu,Cajamarca,Carhuac,Carhuacatac,Cashan,Caullaraju,Caxamalca,Cayesh,Chacchapunta,Chacraraju,Champara,Chanchan,Chekiacraju,Chinchey,Chontah,Chopicalqui,Chucuito,Chuito,Chullo,Chumpi,Chuncho,Chuquiapo,Churup,Cochapata,Cojup,Collota,Conococha,Copa,Corihuayrachina,Cusichaca,Despacho,Haika,Hanpiq,Hatun,Haywarisqa,Huaca,Hualcan,Huamanga,Huamashraju,Huancarhuas,Huandoy,Huantsan,Huarmihuanusca,Huascaran,Huaylas,Huayllabamba,Huichajanca,Huinayhuayna,Huinioch,Illiasca,Intipunku,Ishinca,Jahuacocha,Jirishanca,Juli,Jurau,Kakananpunta,Kamasqa,Karpay,Kausay,Khuya ,Kuelap,Llaca,Llactapata,Llanganuco,Llaqta,Llupachayoc,Machu,Mallku,Matarraju,Mikhuy,Milluacocha,Munay,Ocshapalca,Ollantaytambo,Pacamayo,Paccharaju,Pachacamac,Pachakamaq,Pachakuteq,Pachakuti,Pachamama ,Paititi,Pajaten,Palcaraju,Pampa,Panaka,Paqarina,Paqo,Parap,Paria,Patallacta,Phuyupatamarca,Pisac,Pongos,Pucahirca,Pucaranra,Puscanturpa,Putaca,Qawaq ,Qayqa,Qochamoqo,Qollana,Qorihuayrachina,Qorimoqo,Quenuaracra,Queshque,Quillcayhuanca,Quillya,Quitaracsa,Quitaraju,Qusqu,Rajucolta,Rajutakanan,Rajutuna,Ranrahirca,Ranrapalca,Raria,Rasac,Rimarima,Riobamba,Runkuracay,Rurec,Sacsa,Saiwa,Sarapo,Sayacmarca,Sinakara,TamboColorado,Tamboccocha,Taripaypacha,Taulliraju,Tawantinsuyu,Taytanchis,Tiwanaku,Tocllaraju,Tsacra,Tuco,Tullparaju,Tumbes,Ulta,Uruashraju,Vallunaraju,Vilcabamba,Wacho ,Wankawillka,Wayra,Yachay,Yahuarraju,Yanamarey,Yanesha,Yerupaja"},
|
{name: "Quechua", i: 27, min: 6, max: 12, d: "l", m: 0, b: "Altomisayoq,Ancash,Andahuaylas,Apachekta,Apachita,Apu ,Apurimac,Arequipa,Atahuallpa,Atawalpa,Atico,Ayacucho,Ayllu,Cajamarca,Carhuac,Carhuacatac,Cashan,Caullaraju,Caxamalca,Cayesh,Chacchapunta,Chacraraju,Champara,Chanchan,Chekiacraju,Chinchey,Chontah,Chopicalqui,Chucuito,Chuito,Chullo,Chumpi,Chuncho,Chuquiapo,Churup,Cochapata,Cojup,Collota,Conococha,Copa,Corihuayrachina,Cusichaca,Despacho,Haika,Hanpiq,Hatun,Haywarisqa,Huaca,Hualcan,Huamanga,Huamashraju,Huancarhuas,Huandoy,Huantsan,Huarmihuanusca,Huascaran,Huaylas,Huayllabamba,Huichajanca,Huinayhuayna,Huinioch,Illiasca,Intipunku,Ishinca,Jahuacocha,Jirishanca,Juli,Jurau,Kakananpunta,Kamasqa,Karpay,Kausay,Khuya ,Kuelap,Llaca,Llactapata,Llanganuco,Llaqta,Llupachayoc,Machu,Mallku,Matarraju,Mikhuy,Milluacocha,Munay,Ocshapalca,Ollantaytambo,Pacamayo,Paccharaju,Pachacamac,Pachakamaq,Pachakuteq,Pachakuti,Pachamama ,Paititi,Pajaten,Palcaraju,Pampa,Panaka,Paqarina,Paqo,Parap,Paria,Patallacta,Phuyupatamarca,Pisac,Pongos,Pucahirca,Pucaranra,Puscanturpa,Putaca,Qawaq ,Qayqa,Qochamoqo,Qollana,Qorihuayrachina,Qorimoqo,Quenuaracra,Queshque,Quillcayhuanca,Quillya,Quitaracsa,Quitaraju,Qusqu,Rajucolta,Rajutakanan,Rajutuna,Ranrahirca,Ranrapalca,Raria,Rasac,Rimarima,Riobamba,Runkuracay,Rurec,Sacsa,Saiwa,Sarapo,Sayacmarca,Sinakara,TamboColorado,Tamboccocha,Taripaypacha,Taulliraju,Tawantinsuyu,Taytanchis,Tiwanaku,Tocllaraju,Tsacra,Tuco,Tullparaju,Tumbes,Ulta,Uruashraju,Vallunaraju,Vilcabamba,Wacho ,Wankawillka,Wayra,Yachay,Yahuarraju,Yanamarey,Yanesha,Yerupaja"},
|
||||||
{name: "Swahili", i: 28, min: 4, max: 9, d: "", m: 0, b: "Abim,Adjumani,Alebtong,Amolatar,Amuria,Amuru,Apac,Arua,Arusha,Babati,Baragoi,Bombo,Budaka,Bugembe,Bugiri,Buikwe,Bukedea,Bukoba,Bukomansimbi,Bukungu,Buliisa,Bundibugyo,Bungoma,Busembatya,Bushenyi,Busia,Busia,Busolwe,Butaleja,Butambala,Butere,Buwenge,Buyende,Dadaab,Dodoma,Dokolo,Eldoret,Elegu,Emali,Embu,Entebbe,Garissa,Gede,Gulu,Handeni,Hima,Hoima,Hola,Ibanda,Iganga,Iringa,Isingiro,Isiolo,Jinja,Kaabong,Kabale,Kaberamaido,Kabuyanda,Kabwohe,Kagadi,Kahama,Kajiado,Kakamega,Kakinga,Kakira,Kakiri,Kakuma,Kalangala,Kaliro,Kalisizo,Kalongo,Kalungu,Kampala,Kamuli,Kamwenge,Kanoni,Kanungu,Kapchorwa,Kapenguria,Kasese,Kasulu,Katakwi,Kayunga,Kericho,Keroka,Kiambu,Kibaale,Kibaha,Kibingo,Kiboga,Kibwezi,Kigoma,Kihiihi,Kilifi,Kira,Kiruhura,Kiryandongo,Kisii,Kisoro,Kisumu,Kitale,Kitgum,Kitui,Koboko,Korogwe,Kotido,Kumi,Kyazanga,Kyegegwa,Kyenjojo,Kyotera,Lamu,Langata,Lindi,Lodwar,Lokichoggio,Londiani,Loyangalani,Lugazi,Lukaya,Luweero,Lwakhakha,Lwengo,Lyantonde,Machakos,Mafinga,Makambako,Makindu,Malaba,Malindi,Manafwa,Mandera,Maralal,Marsabit,Masaka,Masindi,MasindiPort,Masulita,Matugga,Mayuge,Mbale,Mbarara,Mbeya,Meru,Mitooma,Mityana,Mombasa,Morogoro,Moroto,Moshi,Moyale,Moyo,Mpanda,Mpigi,Mpondwe,Mtwara,Mubende,Mukono,Mumias,Muranga,Musoma,Mutomo,Mutukula,Mwanza,Nagongera,Nairobi,Naivasha,Nakapiripirit,Nakaseke,Nakasongola,Nakuru,Namanga,Namayingo,Namutumba,Nansana,Nanyuki,Narok,Naromoru,Nebbi,Ngora,Njeru,Njombe,Nkokonjeru,Ntungamo,Nyahururu,Nyeri,Oyam,Pader,Paidha,Pakwach,Pallisa,Rakai,Ruiru,Rukungiri,Rwimi,Sanga,Sembabule,Shimoni,Shinyanga,Singida,Sironko,Songea,Soroti,Ssabagabo,Sumbawanga,Tabora,Takaungu,Tanga,Thika,Tororo,Tunduma,Vihiga,Voi,Wajir,Wakiso,Watamu,Webuye,Wobulenzi,Wote,Wundanyi,Yumbe,Zanzibar"},
|
{name: "Swahili", i: 28, min: 4, max: 9, d: "", m: 0, b: "Abim,Adjumani,Alebtong,Amolatar,Amuria,Amuru,Apac,Arua,Arusha,Babati,Baragoi,Bombo,Budaka,Bugembe,Bugiri,Buikwe,Bukedea,Bukoba,Bukomansimbi,Bukungu,Buliisa,Bundibugyo,Bungoma,Busembatya,Bushenyi,Busia,Busia,Busolwe,Butaleja,Butambala,Butere,Buwenge,Buyende,Dadaab,Dodoma,Dokolo,Eldoret,Elegu,Emali,Embu,Entebbe,Garissa,Gede,Gulu,Handeni,Hima,Hoima,Hola,Ibanda,Iganga,Iringa,Isingiro,Isiolo,Jinja,Kaabong,Kabale,Kaberamaido,Kabuyanda,Kabwohe,Kagadi,Kahama,Kajiado,Kakamega,Kakinga,Kakira,Kakiri,Kakuma,Kalangala,Kaliro,Kalisizo,Kalongo,Kalungu,Kampala,Kamuli,Kamwenge,Kanoni,Kanungu,Kapchorwa,Kapenguria,Kasese,Kasulu,Katakwi,Kayunga,Kericho,Keroka,Kiambu,Kibaale,Kibaha,Kibingo,Kiboga,Kibwezi,Kigoma,Kihiihi,Kilifi,Kira,Kiruhura,Kiryandongo,Kisii,Kisoro,Kisumu,Kitale,Kitgum,Kitui,Koboko,Korogwe,Kotido,Kumi,Kyazanga,Kyegegwa,Kyenjojo,Kyotera,Lamu,Langata,Lindi,Lodwar,Lokichoggio,Londiani,Loyangalani,Lugazi,Lukaya,Luweero,Lwakhakha,Lwengo,Lyantonde,Machakos,Mafinga,Makambako,Makindu,Malaba,Malindi,Manafwa,Mandera,Maralal,Marsabit,Masaka,Masindi,MasindiPort,Masulita,Matugga,Mayuge,Mbale,Mbarara,Mbeya,Meru,Mitooma,Mityana,Mombasa,Morogoro,Moroto,Moshi,Moyale,Moyo,Mpanda,Mpigi,Mpondwe,Mtwara,Mubende,Mukono,Mumias,Muranga,Musoma,Mutomo,Mutukula,Mwanza,Nagongera,Nairobi,Naivasha,Nakapiripirit,Nakaseke,Nakasongola,Nakuru,Namanga,Namayingo,Namutumba,Nansana,Nanyuki,Narok,Naromoru,Nebbi,Ngora,Njeru,Njombe,Nkokonjeru,Ntungamo,Nyahururu,Nyeri,Oyam,Pader,Paidha,Pakwach,Pallisa,Rakai,Ruiru,Rukungiri,Rwimi,Sanga,Sembabule,Shimoni,Shinyanga,Singida,Sironko,Songea,Soroti,Ssabagabo,Sumbawanga,Tabora,Takaungu,Tanga,Thika,Tororo,Tunduma,Vihiga,Voi,Wajir,Wakiso,Watamu,Webuye,Wobulenzi,Wote,Wundanyi,Yumbe,Zanzibar"},
|
||||||
|
|
@ -264,7 +291,7 @@
|
||||||
{name: "Arachnid", i: 40, min: 4, max: 10, d: "erlsk", m: 0, b: "Aaqok'ser,Acah,Aiced,Aisi,Aizachis,Allinqel,As'taq,Ashrash,Caaqtos,Caq'zux,Ceek'sax,Ceezuq,Cek'siereel,Cen'qi,Ceqru,Ceqzocer,Cezeed,Chachocaq,Charis,Chashar,Chashilieth,Checib,Chen'qal,Chernul,Cherzoq,Chezi,Chiazu,Chikoqal,Chishros,Chixhi,Chizhi,Chizoser,Chollash,Choq'sha,Chouk'rix,Cinchichail,Collul,Ecush'taid,Eenqachal,Ekiqe,El'zos,El'zur,Ellu,Eq'tur,Eqa,Eqas,Er'uria,Erikas,Ertu,Es'tase,Esrub,Evirrot,Exha,Haqsho,Heekath,Hiavheesh,Hitha,Hok'thi,Hossa,Iacid,Iciever,Ik'si,Illuq,Iri,Isicer,Isnir,Ivrid,Kaalzux,Keezut,Kheellavas,Kheizoh,Khellinqesh,Khiachod,Khika,Khinchi,Khirzur,Khivila,Khonrud,Khontid,Khosi,Khrakku,Khraqshis,Khrerrith,Khrethish'ti,Khriashus,Khrika,Khrirni,Khrocoqshesh,Klashirel,Klassa,Kleil'sha,Kliakis,Klishuth,Klith'osha,Krarnit,Kras'tex,Kreelzi,Krivas,Krotieqas,Laco,Lairta,Lais'tid,Laizuh,Lasnoth,Lekkol,Len'qeer,Leqanches,Lezad,Lhezsi,Lhilir,Lhivhath,Lhok'thu,Lialliesed,Liaraq,Liarisriq,Liceva,Lichorro,Lilla,Livorzish,Lokieqib,Nakar,Nakur,Naros,Natha,Necuk'saih,Neerhaca,Neet'er,Neezoh,Nenchiled,Nerhalneth,Nir'ih,Nizus,Noreeqo,Novalsher,On'qix,Qailloncho,Qak'sovo,Qalitho,Qartori,Qas'tor,Qasol,Qavrud,Qavud,Qazar,Qazieveq,Qazru,Qeik'thoth,Qekno,Qeqravee,Qes'tor,Qhaaviq,Qhaik'sal,Qhak'sish,Qhazsakais,Qhechorte,Qheliva,Qhenchaqes,Qherazal,Qhesoh,Qhiallud,Qhon'qos,Qhoshielleed,Qish'tur,Qisih,Qollal,Qorhoci,Qouxet,Qranchiq,Racith,Rak'zes,Ranchis,Rarhie,Rarzi,Rarzisiaq,Ras'tih,Ravosho,Recad,Rekid,Relshacash,Reqishee,Rernee,Rertachis,Rezhokketh,Reziel,Rhacish,Rhail'shel,Rhairhizse,Rhakivex,Rhaqeer,Rhartix,Rheciezsei,Rheevid,Rhel'shir,Rhetovraix,Rhevhie,Rhialzub,Rhiavekot,Rhikkos,Rhiqese,Rhiqi,Rhiqracar,Rhisned,Rhokno,Rhousnateb,Rhouvaqid,Riakeesnex,Rik'sid,Rintachal,Rir'ul,Rorrucis,Rosharhir,Rourk'u,Rouzakri,Sailiqei,Sanchiqed,Sanqad,Saqshu,Sat'ier,Sazi,Seiqas,Shieth'i,Shiqsheh,Shizha,Shrachuvo,Shranqo,Shravhos,Shravuth,Shreerhod,Shrethuh,Shriantieth,Shronqash,Shrovarhir,Shrozih,Siacaqoh,Siezosh,Silrul,Siq'sha,Sirro,Sornosi,Srachussi,Sreqrud,Srirnukaaq,Szaca,Szacih,Szaqova,Szasu,Szazhilos,Szeerrud,Szeezsad,Szeknur,Szesir,Szet'as,Szetirrar,Szezhirros,Szilshith,Szon'qol,Szornuq,Xaax'uq,Xeekke,Xosax,Yaconchi,Yacozses,Yazrer,Yeek'su,Yeeq'zox,Yeqil,Yeqroq,Yeveed,Yevied,Yicaveeh,Yirresh,Yisie,Yithik'thaih,Yorhaqshes,Zacheek'sa,Zakkasa,Zaqi,Zelraq,Zeqo,Zhaivoq,Zharuncho,Zhath'arhish,Zhavirrit,Zhazilraq,Zhazot,Zhazsachiel,Zhek'tha,Zhequ,Zhias'ted,Zhicat,Zhicur,Zhiese,Zhirhacil,Zhizri,Zhochizses,Zhorkir,Ziarih,Zirnib,Zis'teq,Zivezeh"},
|
{name: "Arachnid", i: 40, min: 4, max: 10, d: "erlsk", m: 0, b: "Aaqok'ser,Acah,Aiced,Aisi,Aizachis,Allinqel,As'taq,Ashrash,Caaqtos,Caq'zux,Ceek'sax,Ceezuq,Cek'siereel,Cen'qi,Ceqru,Ceqzocer,Cezeed,Chachocaq,Charis,Chashar,Chashilieth,Checib,Chen'qal,Chernul,Cherzoq,Chezi,Chiazu,Chikoqal,Chishros,Chixhi,Chizhi,Chizoser,Chollash,Choq'sha,Chouk'rix,Cinchichail,Collul,Ecush'taid,Eenqachal,Ekiqe,El'zos,El'zur,Ellu,Eq'tur,Eqa,Eqas,Er'uria,Erikas,Ertu,Es'tase,Esrub,Evirrot,Exha,Haqsho,Heekath,Hiavheesh,Hitha,Hok'thi,Hossa,Iacid,Iciever,Ik'si,Illuq,Iri,Isicer,Isnir,Ivrid,Kaalzux,Keezut,Kheellavas,Kheizoh,Khellinqesh,Khiachod,Khika,Khinchi,Khirzur,Khivila,Khonrud,Khontid,Khosi,Khrakku,Khraqshis,Khrerrith,Khrethish'ti,Khriashus,Khrika,Khrirni,Khrocoqshesh,Klashirel,Klassa,Kleil'sha,Kliakis,Klishuth,Klith'osha,Krarnit,Kras'tex,Kreelzi,Krivas,Krotieqas,Laco,Lairta,Lais'tid,Laizuh,Lasnoth,Lekkol,Len'qeer,Leqanches,Lezad,Lhezsi,Lhilir,Lhivhath,Lhok'thu,Lialliesed,Liaraq,Liarisriq,Liceva,Lichorro,Lilla,Livorzish,Lokieqib,Nakar,Nakur,Naros,Natha,Necuk'saih,Neerhaca,Neet'er,Neezoh,Nenchiled,Nerhalneth,Nir'ih,Nizus,Noreeqo,Novalsher,On'qix,Qailloncho,Qak'sovo,Qalitho,Qartori,Qas'tor,Qasol,Qavrud,Qavud,Qazar,Qazieveq,Qazru,Qeik'thoth,Qekno,Qeqravee,Qes'tor,Qhaaviq,Qhaik'sal,Qhak'sish,Qhazsakais,Qhechorte,Qheliva,Qhenchaqes,Qherazal,Qhesoh,Qhiallud,Qhon'qos,Qhoshielleed,Qish'tur,Qisih,Qollal,Qorhoci,Qouxet,Qranchiq,Racith,Rak'zes,Ranchis,Rarhie,Rarzi,Rarzisiaq,Ras'tih,Ravosho,Recad,Rekid,Relshacash,Reqishee,Rernee,Rertachis,Rezhokketh,Reziel,Rhacish,Rhail'shel,Rhairhizse,Rhakivex,Rhaqeer,Rhartix,Rheciezsei,Rheevid,Rhel'shir,Rhetovraix,Rhevhie,Rhialzub,Rhiavekot,Rhikkos,Rhiqese,Rhiqi,Rhiqracar,Rhisned,Rhokno,Rhousnateb,Rhouvaqid,Riakeesnex,Rik'sid,Rintachal,Rir'ul,Rorrucis,Rosharhir,Rourk'u,Rouzakri,Sailiqei,Sanchiqed,Sanqad,Saqshu,Sat'ier,Sazi,Seiqas,Shieth'i,Shiqsheh,Shizha,Shrachuvo,Shranqo,Shravhos,Shravuth,Shreerhod,Shrethuh,Shriantieth,Shronqash,Shrovarhir,Shrozih,Siacaqoh,Siezosh,Silrul,Siq'sha,Sirro,Sornosi,Srachussi,Sreqrud,Srirnukaaq,Szaca,Szacih,Szaqova,Szasu,Szazhilos,Szeerrud,Szeezsad,Szeknur,Szesir,Szet'as,Szetirrar,Szezhirros,Szilshith,Szon'qol,Szornuq,Xaax'uq,Xeekke,Xosax,Yaconchi,Yacozses,Yazrer,Yeek'su,Yeeq'zox,Yeqil,Yeqroq,Yeveed,Yevied,Yicaveeh,Yirresh,Yisie,Yithik'thaih,Yorhaqshes,Zacheek'sa,Zakkasa,Zaqi,Zelraq,Zeqo,Zhaivoq,Zharuncho,Zhath'arhish,Zhavirrit,Zhazilraq,Zhazot,Zhazsachiel,Zhek'tha,Zhequ,Zhias'ted,Zhicat,Zhicur,Zhiese,Zhirhacil,Zhizri,Zhochizses,Zhorkir,Ziarih,Zirnib,Zis'teq,Zivezeh"},
|
||||||
{name: "Serpents", i: 41, min: 5, max: 11, d: "slrk", m: 0, b: "Aj'ha,Aj'i,Aj'tiss,Ajakess,Aksas,Aksiss,Al'en,An'jeshe,Apjige,Arkkess,Athaz,Atus,Azras,Caji,Cakrasar,Cal'arrun,Capji,Cathras,Cej'han,Ces,Cez'jenta,Cij'te,Cinash,Cizran,Coth'jus,Cothrash,Culzanek,Cunaless,Ej'tesh,Elzazash,Ergek,Eshjuk,Ethris,Gan'jas,Gapja,Gar'thituph,Gopjeguss,Gor'thesh,Gragishaph,Grar'theness,Grath'ji,Gressinas,Grolzesh,Grorjar,Grozrash,Guj'ika,Harji,Hej'hez,Herkush,Horgarrez,Illuph,Ipjar,Ithashin,Kaj'ess,Kar'kash,Kepjusha,Ki'kintus,Kissere,Koph,Kopjess,Kra'kasher,Krak,Krapjez,Krashjuless,Kraz'ji,Krirrigis,Krussin,Ma'lush,Mage,Maj'tak,Mal'a,Mapja,Mar'kash,Mar'kis,Marjin,Mas,Mathan,Men'jas,Meth'jaresh,Mij'hegak,Min'jash,Mith'jas,Monassu,Moss,Naj'hass,Najugash,Nak,Napjiph,Nar'ka,Nar'thuss,Narrusha,Nash,Nashjekez,Nataph,Nij'ass,Nij'tessiph,Nishjiss,Norkkuss,Nus,Olluruss,Or'thi,Or'thuss,Paj'a,Parkka,Pas,Pathujen,Paz'jaz,Pepjerras,Pirkkanar,Pituk,Porjunek,Pu'ke,Ragen,Ran'jess,Rargush,Razjuph,Rilzan,Riss,Rithruz,Rorgiss,Rossez,Rraj'asesh,Rraj'tass,Rrar'kess,Rrar'thuph,Rras,Rrazresh,Rrej'hish,Rrigelash,Rris,Rris,Rroksurrush,Rukrussush,Rurri,Russa,Ruth'jes,Sa'kitesh,Sar'thass,Sarjas,Sazjuzush,Ser'thez,Sezrass,Shajas,Shas,Shashja,Shass,Shetesh,Shijek,Shun'jaler,Shurjarri,Skaler,Skalla,Skallentas,Skaph,Skar'kerriz,Skath'jeruk,Sker'kalas,Skor,Skoz'ji,Sku'lu,Skuph,Skur'thur,Slalli,Slalt'har,Slelziress,Slil'ar,Sloz'jisa,Sojesh,Solle,Sorge,Sral'e,Sran'ji,Srapjess,Srar'thazur,Srash,Srath'jess,Srathrarre,Srerkkash,Srus,Sruss'tugeph,Sun,Suss'tir,Uzrash,Vargush,Vek,Vess'tu,Viph,Vult'ha,Vupjer,Vushjesash,Xagez,Xassa,Xulzessu,Zaj'tiss,Zan'jer,Zarriss,Zassegus,Zirres,Zsor,Zurjass"}
|
{name: "Serpents", i: 41, min: 5, max: 11, d: "slrk", m: 0, b: "Aj'ha,Aj'i,Aj'tiss,Ajakess,Aksas,Aksiss,Al'en,An'jeshe,Apjige,Arkkess,Athaz,Atus,Azras,Caji,Cakrasar,Cal'arrun,Capji,Cathras,Cej'han,Ces,Cez'jenta,Cij'te,Cinash,Cizran,Coth'jus,Cothrash,Culzanek,Cunaless,Ej'tesh,Elzazash,Ergek,Eshjuk,Ethris,Gan'jas,Gapja,Gar'thituph,Gopjeguss,Gor'thesh,Gragishaph,Grar'theness,Grath'ji,Gressinas,Grolzesh,Grorjar,Grozrash,Guj'ika,Harji,Hej'hez,Herkush,Horgarrez,Illuph,Ipjar,Ithashin,Kaj'ess,Kar'kash,Kepjusha,Ki'kintus,Kissere,Koph,Kopjess,Kra'kasher,Krak,Krapjez,Krashjuless,Kraz'ji,Krirrigis,Krussin,Ma'lush,Mage,Maj'tak,Mal'a,Mapja,Mar'kash,Mar'kis,Marjin,Mas,Mathan,Men'jas,Meth'jaresh,Mij'hegak,Min'jash,Mith'jas,Monassu,Moss,Naj'hass,Najugash,Nak,Napjiph,Nar'ka,Nar'thuss,Narrusha,Nash,Nashjekez,Nataph,Nij'ass,Nij'tessiph,Nishjiss,Norkkuss,Nus,Olluruss,Or'thi,Or'thuss,Paj'a,Parkka,Pas,Pathujen,Paz'jaz,Pepjerras,Pirkkanar,Pituk,Porjunek,Pu'ke,Ragen,Ran'jess,Rargush,Razjuph,Rilzan,Riss,Rithruz,Rorgiss,Rossez,Rraj'asesh,Rraj'tass,Rrar'kess,Rrar'thuph,Rras,Rrazresh,Rrej'hish,Rrigelash,Rris,Rris,Rroksurrush,Rukrussush,Rurri,Russa,Ruth'jes,Sa'kitesh,Sar'thass,Sarjas,Sazjuzush,Ser'thez,Sezrass,Shajas,Shas,Shashja,Shass,Shetesh,Shijek,Shun'jaler,Shurjarri,Skaler,Skalla,Skallentas,Skaph,Skar'kerriz,Skath'jeruk,Sker'kalas,Skor,Skoz'ji,Sku'lu,Skuph,Skur'thur,Slalli,Slalt'har,Slelziress,Slil'ar,Sloz'jisa,Sojesh,Solle,Sorge,Sral'e,Sran'ji,Srapjess,Srar'thazur,Srash,Srath'jess,Srathrarre,Srerkkash,Srus,Sruss'tugeph,Sun,Suss'tir,Uzrash,Vargush,Vek,Vess'tu,Viph,Vult'ha,Vupjer,Vushjesash,Xagez,Xassa,Xulzessu,Zaj'tiss,Zan'jer,Zarriss,Zassegus,Zirres,Zsor,Zurjass"}
|
||||||
];
|
];
|
||||||
}
|
};
|
||||||
|
|
||||||
return {getBase, getCulture, getCultureShort, getBaseShort, getState, updateChain, clearChains, getNameBases, getMapName, calculateChain};
|
return {getBase, getCulture, getCultureShort, getBaseShort, getState, updateChain, clearChains, getNameBases, getMapName, calculateChain};
|
||||||
})));
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
(function (global, factory) {
|
(function (global, factory) {
|
||||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
typeof exports === "object" && typeof module !== "undefined" ? (module.exports = factory()) : typeof define === "function" && define.amd ? define(factory) : (global.OceanLayers = factory());
|
||||||
typeof define === 'function' && define.amd ? define(factory) :
|
})(this, function () {
|
||||||
(global.OceanLayers = factory());
|
"use strict";
|
||||||
}(this, (function () { 'use strict';
|
|
||||||
|
|
||||||
let cells, vertices, pointsN, used;
|
let cells, vertices, pointsN, used;
|
||||||
|
|
||||||
|
|
@ -12,11 +11,11 @@
|
||||||
TIME && console.time("drawOceanLayers");
|
TIME && console.time("drawOceanLayers");
|
||||||
|
|
||||||
lineGen.curve(d3.curveBasisClosed);
|
lineGen.curve(d3.curveBasisClosed);
|
||||||
cells = grid.cells, pointsN = grid.cells.i.length, vertices = grid.vertices;
|
(cells = grid.cells), (pointsN = grid.cells.i.length), (vertices = grid.vertices);
|
||||||
const limits = outline === "random" ? randomizeOutline() : outline.split(",").map(s => +s);
|
const limits = outline === "random" ? randomizeOutline() : outline.split(",").map(s => +s);
|
||||||
|
|
||||||
const chains = [];
|
const chains = [];
|
||||||
const opacity = rn(.4 / limits.length, 2);
|
const opacity = rn(0.4 / limits.length, 2);
|
||||||
used = new Uint8Array(pointsN); // to detect already passed cells
|
used = new Uint8Array(pointsN); // to detect already passed cells
|
||||||
|
|
||||||
for (const i of cells.i) {
|
for (const i of cells.i) {
|
||||||
|
|
@ -31,7 +30,10 @@
|
||||||
const relax = 1 + t * -2; // select only n-th point
|
const relax = 1 + t * -2; // select only n-th point
|
||||||
const relaxed = chain.filter((v, i) => !(i % relax) || vertices.c[v].some(c => c >= pointsN));
|
const relaxed = chain.filter((v, i) => !(i % relax) || vertices.c[v].some(c => c >= pointsN));
|
||||||
if (relaxed.length < 4) continue;
|
if (relaxed.length < 4) continue;
|
||||||
const points = clipPoly(relaxed.map(v => vertices.p[v]), 1);
|
const points = clipPoly(
|
||||||
|
relaxed.map(v => vertices.p[v]),
|
||||||
|
1
|
||||||
|
);
|
||||||
chains.push([t, points]);
|
chains.push([t, points]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,14 +50,18 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
TIME && console.timeEnd("drawOceanLayers");
|
TIME && console.timeEnd("drawOceanLayers");
|
||||||
}
|
};
|
||||||
|
|
||||||
function randomizeOutline() {
|
function randomizeOutline() {
|
||||||
const limits = [];
|
const limits = [];
|
||||||
let odd = .2
|
let odd = 0.2;
|
||||||
for (let l = -9; l < 0; l++) {
|
for (let l = -9; l < 0; l++) {
|
||||||
if (P(odd)) {odd = .2; limits.push(l);}
|
if (P(odd)) {
|
||||||
else {odd *= 2;}
|
odd = 0.2;
|
||||||
|
limits.push(l);
|
||||||
|
} else {
|
||||||
|
odd *= 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return limits;
|
return limits;
|
||||||
}
|
}
|
||||||
|
|
@ -63,11 +69,11 @@
|
||||||
// connect vertices to chain
|
// connect vertices to chain
|
||||||
function connectVertices(start, t) {
|
function connectVertices(start, t) {
|
||||||
const chain = []; // vertices chain to form a path
|
const chain = []; // vertices chain to form a path
|
||||||
for (let i=0, current = start; i === 0 || current !== start && i < 10000; i++) {
|
for (let i = 0, current = start; i === 0 || (current !== start && i < 10000); i++) {
|
||||||
const prev = chain[chain.length - 1]; // previous vertex in chain
|
const prev = chain[chain.length - 1]; // previous vertex in chain
|
||||||
chain.push(current); // add current vertex to sequence
|
chain.push(current); // add current vertex to sequence
|
||||||
const c = vertices.c[current]; // cells adjacent to vertex
|
const c = vertices.c[current]; // cells adjacent to vertex
|
||||||
c.filter(c => cells.t[c] === t).forEach(c => used[c] = 1);
|
c.filter(c => cells.t[c] === t).forEach(c => (used[c] = 1));
|
||||||
const v = vertices.v[current]; // neighboring vertices
|
const v = vertices.v[current]; // neighboring vertices
|
||||||
const c0 = !cells.t[c[0]] || cells.t[c[0]] === t - 1;
|
const c0 = !cells.t[c[0]] || cells.t[c[0]] === t - 1;
|
||||||
const c1 = !cells.t[c[1]] || cells.t[c[1]] === t - 1;
|
const c1 = !cells.t[c[1]] || cells.t[c[1]] === t - 1;
|
||||||
|
|
@ -75,12 +81,14 @@
|
||||||
if (v[0] !== undefined && v[0] !== prev && c0 !== c1) current = v[0];
|
if (v[0] !== undefined && v[0] !== prev && c0 !== c1) current = v[0];
|
||||||
else if (v[1] !== undefined && v[1] !== prev && c1 !== c2) current = v[1];
|
else if (v[1] !== undefined && v[1] !== prev && c1 !== c2) current = v[1];
|
||||||
else if (v[2] !== undefined && v[2] !== prev && c0 !== c2) current = v[2];
|
else if (v[2] !== undefined && v[2] !== prev && c0 !== c2) current = v[2];
|
||||||
if (current === chain[chain.length - 1]) {ERROR && console.error("Next vertex is not found"); break;}
|
if (current === chain[chain.length - 1]) {
|
||||||
|
ERROR && console.error("Next vertex is not found");
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
chain.push(chain[0]); // push first vertex as the last one
|
chain.push(chain[0]); // push first vertex as the last one
|
||||||
return chain;
|
return chain;
|
||||||
}
|
}
|
||||||
|
|
||||||
return OceanLayers;
|
return OceanLayers;
|
||||||
|
});
|
||||||
})));
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
(function (global, factory) {
|
(function (global, factory) {
|
||||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
typeof exports === "object" && typeof module !== "undefined" ? (module.exports = factory()) : typeof define === "function" && define.amd ? define(factory) : (global.Rivers = factory());
|
||||||
typeof define === 'function' && define.amd ? define(factory) :
|
})(this, function () {
|
||||||
(global.Rivers = factory());
|
"use strict";
|
||||||
}(this, (function () {'use strict';
|
|
||||||
|
|
||||||
const generate = function(changeHeights = true) {
|
const generate = function (allowErosion = true) {
|
||||||
TIME && console.time('generateRivers');
|
TIME && console.time("generateRivers");
|
||||||
Math.random = aleaPRNG(seed);
|
Math.random = aleaPRNG(seed);
|
||||||
const cells = pack.cells, p = cells.p, features = pack.features;
|
const {cells, features} = pack;
|
||||||
|
const p = cells.p;
|
||||||
|
|
||||||
const riversData = []; // rivers data
|
const riversData = []; // rivers data
|
||||||
cells.fl = new Uint16Array(cells.i.length); // water flux array
|
cells.fl = new Uint16Array(cells.i.length); // water flux array
|
||||||
|
|
@ -16,44 +16,31 @@ const generate = function(changeHeights = true) {
|
||||||
let riverNext = 1; // first river id is 1
|
let riverNext = 1; // first river id is 1
|
||||||
|
|
||||||
const h = alterHeights();
|
const h = alterHeights();
|
||||||
removeStoredLakeData();
|
Lakes.prepareLakeData(h);
|
||||||
resolveDepressions(h);
|
resolveDepressions(h);
|
||||||
drainWater();
|
drainWater();
|
||||||
defineRivers();
|
defineRivers();
|
||||||
Lakes.cleanupLakeData();
|
Lakes.cleanupLakeData();
|
||||||
|
|
||||||
if (changeHeights) cells.h = Uint8Array.from(h); // apply changed heights as basic one
|
if (allowErosion) cells.h = Uint8Array.from(h); // apply changed heights as basic one
|
||||||
|
|
||||||
TIME && console.timeEnd('generateRivers');
|
TIME && console.timeEnd("generateRivers");
|
||||||
|
|
||||||
// height with added t value to make map less depressed
|
|
||||||
function alterHeights() {
|
|
||||||
const h = Array.from(cells.h)
|
|
||||||
.map((h, i) => h < 20 || cells.t[i] < 1 ? h : h + cells.t[i] / 100)
|
|
||||||
.map((h, i) => h < 20 || cells.t[i] < 1 ? h : h + d3.mean(cells.c[i].map(c => cells.t[c])) / 10000);
|
|
||||||
return h;
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeStoredLakeData() {
|
|
||||||
features.forEach(f => {
|
|
||||||
delete f.flux;
|
|
||||||
delete f.inlets;
|
|
||||||
delete f.outlet;
|
|
||||||
delete f.height;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function drainWater() {
|
function drainWater() {
|
||||||
const MIN_FLUX_TO_FORM_RIVER = 30;
|
const MIN_FLUX_TO_FORM_RIVER = 30;
|
||||||
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.setClimateData(h);
|
||||||
|
|
||||||
|
// const flow = cells.i.length < 65535 ? new Uint16Array(cells.i.length) : new Uint32Array(cells.i.length);
|
||||||
|
// flow[i] = min;
|
||||||
|
// debug.append("path").attr("class", "arrow").attr("d", `M${cells.p[i][0]},${cells.p[i][1]}L${cells.p[min][0]},${cells.p[min][1]}`);
|
||||||
|
|
||||||
land.forEach(function (i) {
|
land.forEach(function (i) {
|
||||||
cells.fl[i] += grid.cells.prec[cells.g[i]]; // flux from precipitation
|
cells.fl[i] += grid.cells.prec[cells.g[i]]; // flux from precipitation
|
||||||
const x = p[i][0], y = p[i][1];
|
const [x, y] = p[i];
|
||||||
|
|
||||||
// create lake outlet if flux > evaporation
|
// create lake outlet if lake is not in deep depression and flux > evaporation
|
||||||
const lakes = !lakeOutCells[i] ? [] : features.filter(feature => i === feature.outCell && feature.flux > feature.evaporation);
|
const lakes = lakeOutCells[i] ? features.filter(feature => i === feature.outCell && feature.flux > feature.evaporation) : [];
|
||||||
for (const lake of lakes) {
|
for (const lake of lakes) {
|
||||||
const lakeCell = cells.c[i].find(c => h[c] < 20 && cells.f[c] === lake.i);
|
const lakeCell = cells.c[i].find(c => h[c] < 20 && cells.f[c] === lake.i);
|
||||||
|
|
||||||
|
|
@ -78,25 +65,27 @@ const generate = function(changeHeights = true) {
|
||||||
|
|
||||||
// assign all tributary rivers to outlet basin
|
// assign all tributary rivers to outlet basin
|
||||||
for (let outlet = lakes[0]?.outlet, l = 0; l < lakes.length; l++) {
|
for (let outlet = lakes[0]?.outlet, l = 0; l < lakes.length; l++) {
|
||||||
lakes[l].inlets?.forEach(fork => riversData.find(r => r.river === fork).parent = outlet);
|
lakes[l].inlets?.forEach(fork => (riversData.find(r => r.river === fork).parent = outlet));
|
||||||
}
|
}
|
||||||
|
|
||||||
// near-border cell: pour water out of the screen
|
// near-border cell: pour water out of the screen
|
||||||
if (cells.b[i] && cells.r[i]) {
|
if (cells.b[i] && cells.r[i]) {
|
||||||
const to = [];
|
let to = [];
|
||||||
const min = Math.min(y, graphHeight - y, x, graphWidth - x);
|
const min = Math.min(y, graphHeight - y, x, graphWidth - x);
|
||||||
if (min === y) {to[0] = x; to[1] = 0;} else
|
if (min === y) to = [x, 0];
|
||||||
if (min === graphHeight - y) {to[0] = x; to[1] = graphHeight;} else
|
else if (min === graphHeight - y) to = [x, graphHeight];
|
||||||
if (min === x) {to[0] = 0; to[1] = y;} else
|
else if (min === x) to = [0, y];
|
||||||
if (min === graphWidth - x) {to[0] = graphWidth; to[1] = y;}
|
else if (min === graphWidth - x) to = [graphWidth, y];
|
||||||
riversData.push({river: cells.r[i], cell: i, x: to[0], y: to[1], flux: cells.fl[i]});
|
riversData.push({river: cells.r[i], cell: i, x: to[0], y: to[1], flux: cells.fl[i]});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// downhill cell (make sure it's not in the source lake)
|
// downhill cell (make sure it's not in the source lake)
|
||||||
const min = lakeOutCells[i]
|
const filtered = lakeOutCells[i] ? cells.c[i].filter(c => !lakes.map(lake => lake.i).includes(cells.f[c])) : cells.c[i];
|
||||||
? cells.c[i].filter(c => !lakes.map(lake => lake.i).includes(cells.f[c])).sort((a, b) => h[a] - h[b])[0]
|
const min = filtered.sort((a, b) => h[a] - h[b])[0];
|
||||||
: cells.c[i].sort((a, b) => h[a] - h[b])[0];
|
|
||||||
|
// cells is depressed
|
||||||
|
if (h[i] <= h[min]) return;
|
||||||
|
|
||||||
if (cells.fl[i] < MIN_FLUX_TO_FORM_RIVER) {
|
if (cells.fl[i] < MIN_FLUX_TO_FORM_RIVER) {
|
||||||
if (h[min] >= 20) cells.fl[min] += cells.fl[i];
|
if (h[min] >= 20) cells.fl[min] += cells.fl[i];
|
||||||
|
|
@ -139,7 +128,7 @@ const generate = function(changeHeights = true) {
|
||||||
waterBody.enteringFlux = fromFlux;
|
waterBody.enteringFlux = fromFlux;
|
||||||
}
|
}
|
||||||
waterBody.flux = waterBody.flux + fromFlux;
|
waterBody.flux = waterBody.flux + fromFlux;
|
||||||
waterBody.inlets ? waterBody.inlets.push(river) : waterBody.inlets = [river];
|
waterBody.inlets ? waterBody.inlets.push(river) : (waterBody.inlets = [river]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// propagate flux and add next river segment
|
// propagate flux and add next river segment
|
||||||
|
|
@ -167,10 +156,10 @@ const generate = function(changeHeights = true) {
|
||||||
const source = riverSegments[0].cell;
|
const source = riverSegments[0].cell;
|
||||||
const mouth = riverSegments[riverSegments.length - 2].cell;
|
const mouth = riverSegments[riverSegments.length - 2].cell;
|
||||||
|
|
||||||
const widthFactor = rn(.8 + Math.random() * .4, 1); // river width modifier [.8, 1.2]
|
const widthFactor = rn(0.8 + Math.random() * 0.4, 1); // river width modifier [.8, 1.2]
|
||||||
const sourceWidth = cells.h[source] >= 20 ? .1 : rn(Math.min(Math.max((cells.fl[source] / 500) ** .4, .5), 1.7), 2);
|
const sourceWidth = cells.h[source] >= 20 ? 0.1 : rn(Math.min(Math.max((cells.fl[source] / 500) ** 0.4, 0.5), 1.7), 2);
|
||||||
|
|
||||||
const riverMeandered = addMeandering(riverSegments, sourceWidth * 10, .5);
|
const riverMeandered = addMeandering(riverSegments, sourceWidth * 10, 0.5);
|
||||||
const [path, length, offset] = getPath(riverMeandered, widthFactor, sourceWidth);
|
const [path, length, offset] = getPath(riverMeandered, widthFactor, sourceWidth);
|
||||||
riverPaths.push([path, r]);
|
riverPaths.push([path, r]);
|
||||||
|
|
||||||
|
|
@ -183,59 +172,93 @@ const generate = function(changeHeights = true) {
|
||||||
// draw rivers
|
// draw rivers
|
||||||
rivers.html(riverPaths.map(d => `<path id="river${d[1]}" d="${d[0]}"/>`).join(""));
|
rivers.html(riverPaths.map(d => `<path id="river${d[1]}" d="${d[0]}"/>`).join(""));
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
// add distance to water value to land cells to make map less depressed
|
||||||
|
const alterHeights = () => {
|
||||||
|
const cells = pack.cells;
|
||||||
|
return Array.from(cells.h).map((h, i) => {
|
||||||
|
if (h < 20 || cells.t[i] < 1) return h;
|
||||||
|
return h + cells.t[i] / 100 + d3.mean(cells.c[i].map(c => cells.t[c])) / 10000;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// depression filling algorithm (for a correct water flux modeling)
|
// depression filling algorithm (for a correct water flux modeling)
|
||||||
const resolveDepressions = function (h) {
|
const resolveDepressions = function (h) {
|
||||||
const {cells, features} = pack;
|
const {cells, features} = pack;
|
||||||
const ITERATIONS = 150;
|
const maxIterations = +document.getElementById("resolveDepressionsStepsOutput").value;
|
||||||
|
const checkLakeMaxIteration = maxIterations * 0.85;
|
||||||
|
const elevateLakeMaxIteration = maxIterations * 0.75;
|
||||||
|
|
||||||
|
const height = i => features[cells.f[i]].height || h[i]; // height of lake or specific cell
|
||||||
|
|
||||||
const lakes = features.filter(f => f.type === "lake");
|
const lakes = features.filter(f => f.type === "lake");
|
||||||
lakes.forEach(l => {
|
|
||||||
const uniqueCells = new Set();
|
|
||||||
l.vertices.forEach(v => pack.vertices.c[v].forEach(c => cells.h[c] >= 20 && uniqueCells.add(c)));
|
|
||||||
l.shoreline = [...uniqueCells];
|
|
||||||
});
|
|
||||||
|
|
||||||
const land = cells.i.filter(i => h[i] >= 20 && !cells.b[i]); // exclude near-border cells
|
const land = cells.i.filter(i => h[i] >= 20 && !cells.b[i]); // exclude near-border cells
|
||||||
land.sort((a,b) => h[b] - h[a]); // highest cells go first
|
land.sort((a, b) => h[a] - h[b]); // lowest cells go first
|
||||||
|
|
||||||
|
const progress = [];
|
||||||
let depressions = Infinity;
|
let depressions = Infinity;
|
||||||
for (let l = 0; depressions && l < ITERATIONS; l++) {
|
let prevDepressions = null;
|
||||||
|
for (let iteration = 0; depressions && iteration < maxIterations; iteration++) {
|
||||||
|
if (progress.length > 5 && d3.sum(progress) > 0) {
|
||||||
|
// bad progress, abort and set heights back
|
||||||
|
h = alterHeights();
|
||||||
|
depressions = progress[0];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
depressions = 0;
|
depressions = 0;
|
||||||
|
|
||||||
|
if (iteration < checkLakeMaxIteration) {
|
||||||
for (const l of lakes) {
|
for (const l of lakes) {
|
||||||
|
if (l.closed) continue;
|
||||||
const minHeight = d3.min(l.shoreline.map(s => h[s]));
|
const minHeight = d3.min(l.shoreline.map(s => h[s]));
|
||||||
if (minHeight >= 100 || l.height > minHeight) continue;
|
if (minHeight >= 100 || l.height > minHeight) continue;
|
||||||
l.height = minHeight + 1;
|
|
||||||
|
if (iteration > elevateLakeMaxIteration) {
|
||||||
|
l.shoreline.forEach(i => (h[i] = cells.h[i]));
|
||||||
|
l.height = d3.min(l.shoreline.map(s => h[s])) - 1;
|
||||||
|
l.closed = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
depressions++;
|
depressions++;
|
||||||
|
l.height = minHeight + 0.2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const i of land) {
|
for (const i of land) {
|
||||||
const minHeight = d3.min(cells.c[i].map(c => cells.t[c] > 0 ? h[c] : pack.features[cells.f[c]].height || h[c]));
|
const minHeight = d3.min(cells.c[i].map(c => height(c)));
|
||||||
if (minHeight >= 100 || h[i] > minHeight) continue;
|
if (minHeight >= 100 || h[i] > minHeight) continue;
|
||||||
h[i] = minHeight + 1;
|
|
||||||
depressions++;
|
depressions++;
|
||||||
}
|
h[i] = minHeight + 0.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
depressions && ERROR && console.error("Heightmap is depressed. Issues with rivers expected. Remove depressed areas to resolve");
|
prevDepressions !== null && progress.push(depressions - prevDepressions);
|
||||||
|
prevDepressions = depressions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
depressions && WARN && console.warn(`Unresolved depressions: ${depressions}. Edit heightmap to fix`);
|
||||||
|
};
|
||||||
|
|
||||||
// add more river points on 1/3 and 2/3 of length
|
// add more river points on 1/3 and 2/3 of length
|
||||||
const addMeandering = function(segments, width = 1, meandering = .5) {
|
const addMeandering = function (segments, width = 1, meandering = 0.5) {
|
||||||
const riverMeandered = []; // to store enhanced segments
|
const riverMeandered = []; // to store enhanced segments
|
||||||
|
|
||||||
for (let s = 0; s < segments.length; s++, width++) {
|
for (let s = 0; s < segments.length; s++, width++) {
|
||||||
const sX = segments[s].x, sY = segments[s].y; // segment start coordinates
|
const sX = segments[s].x,
|
||||||
|
sY = segments[s].y; // segment start coordinates
|
||||||
const c = pack.cells.conf[segments[s].cell] || 0; // if segment is river confluence
|
const c = pack.cells.conf[segments[s].cell] || 0; // if segment is river confluence
|
||||||
riverMeandered.push([sX, sY, c]);
|
riverMeandered.push([sX, sY, c]);
|
||||||
|
|
||||||
if (s + 1 === segments.length) break; // do not meander last segment
|
if (s + 1 === segments.length) break; // do not meander last segment
|
||||||
|
|
||||||
const eX = segments[s+1].x, eY = segments[s+1].y; // segment end coordinates
|
const eX = segments[s + 1].x,
|
||||||
|
eY = segments[s + 1].y; // segment end coordinates
|
||||||
const angle = Math.atan2(eY - sY, eX - sX);
|
const angle = Math.atan2(eY - sY, eX - sX);
|
||||||
const sin = Math.sin(angle), cos = Math.cos(angle);
|
const sin = Math.sin(angle),
|
||||||
|
cos = Math.cos(angle);
|
||||||
|
|
||||||
const meander = meandering + 1 / width + Math.random() * Math.max(meandering - width / 100, 0);
|
const meander = meandering + 1 / width + Math.random() * Math.max(meandering - width / 100, 0);
|
||||||
const dist2 = (eX - sX) ** 2 + (eY - sY) ** 2; // square distance between segment start and end
|
const dist2 = (eX - sX) ** 2 + (eY - sY) ** 2; // square distance between segment start and end
|
||||||
|
|
@ -253,53 +276,61 @@ const addMeandering = function(segments, width = 1, meandering = .5) {
|
||||||
const p1y = (sY + eY) / 2 + cos * meander;
|
const p1y = (sY + eY) / 2 + cos * meander;
|
||||||
riverMeandered.push([p1x, p1y]);
|
riverMeandered.push([p1x, p1y]);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return riverMeandered;
|
return riverMeandered;
|
||||||
}
|
};
|
||||||
|
|
||||||
const getPath = function(points, widthFactor = 1, sourceWidth = .1) {
|
const getPath = function (points, widthFactor = 1, sourceWidth = 0.1) {
|
||||||
let offset, extraOffset = sourceWidth; // starting river width (to make river source visible)
|
let offset,
|
||||||
|
extraOffset = sourceWidth; // starting river width (to make river source visible)
|
||||||
const riverLength = points.reduce((s, v, i, p) => s + (i ? Math.hypot(v[0] - p[i - 1][0], v[1] - p[i - 1][1]) : 0), 0); // summ of segments length
|
const riverLength = points.reduce((s, v, i, p) => s + (i ? Math.hypot(v[0] - p[i - 1][0], v[1] - p[i - 1][1]) : 0), 0); // summ of segments length
|
||||||
const widening = 1000 + riverLength * 30;
|
const widening = 1000 + riverLength * 30;
|
||||||
const riverPointsLeft = [], riverPointsRight = []; // store points on both sides to build a valid polygon
|
const riverPointsLeft = [],
|
||||||
|
riverPointsRight = []; // store points on both sides to build a valid polygon
|
||||||
const last = points.length - 1;
|
const last = points.length - 1;
|
||||||
const factor = riverLength / points.length;
|
const factor = riverLength / points.length;
|
||||||
|
|
||||||
// first point
|
// first point
|
||||||
let x = points[0][0], y = points[0][1], c;
|
let x = points[0][0],
|
||||||
|
y = points[0][1],
|
||||||
|
c;
|
||||||
let angle = Math.atan2(y - points[1][1], x - points[1][0]);
|
let angle = Math.atan2(y - points[1][1], x - points[1][0]);
|
||||||
let sin = Math.sin(angle), cos = Math.cos(angle);
|
let sin = Math.sin(angle),
|
||||||
let xLeft = x + -sin * extraOffset, yLeft = y + cos * extraOffset;
|
cos = Math.cos(angle);
|
||||||
|
let xLeft = x + -sin * extraOffset,
|
||||||
|
yLeft = y + cos * extraOffset;
|
||||||
riverPointsLeft.push([xLeft, yLeft]);
|
riverPointsLeft.push([xLeft, yLeft]);
|
||||||
let xRight = x + sin * extraOffset, yRight = y + -cos * extraOffset;
|
let xRight = x + sin * extraOffset,
|
||||||
|
yRight = y + -cos * extraOffset;
|
||||||
riverPointsRight.unshift([xRight, yRight]);
|
riverPointsRight.unshift([xRight, yRight]);
|
||||||
|
|
||||||
// middle points
|
// middle points
|
||||||
for (let p = 1; p < last; p++) {
|
for (let p = 1; p < last; p++) {
|
||||||
x = points[p][0], y = points[p][1], c = points[p][2] || 0;
|
(x = points[p][0]), (y = points[p][1]), (c = points[p][2] || 0);
|
||||||
const xPrev = points[p-1][0], yPrev = points[p - 1][1];
|
const xPrev = points[p - 1][0],
|
||||||
const xNext = points[p+1][0], yNext = points[p + 1][1];
|
yPrev = points[p - 1][1];
|
||||||
|
const xNext = points[p + 1][0],
|
||||||
|
yNext = points[p + 1][1];
|
||||||
angle = Math.atan2(yPrev - yNext, xPrev - xNext);
|
angle = Math.atan2(yPrev - yNext, xPrev - xNext);
|
||||||
sin = Math.sin(angle), cos = Math.cos(angle);
|
(sin = Math.sin(angle)), (cos = Math.cos(angle));
|
||||||
offset = (Math.atan(Math.pow(p * factor, 2) / widening) / 2 * widthFactor) + extraOffset;
|
offset = (Math.atan(Math.pow(p * factor, 2) / widening) / 2) * widthFactor + extraOffset;
|
||||||
const confOffset = Math.atan(c * 5 / widening);
|
const confOffset = Math.atan((c * 5) / widening);
|
||||||
extraOffset += confOffset;
|
extraOffset += confOffset;
|
||||||
xLeft = x + -sin * offset, yLeft = y + cos * (offset + confOffset);
|
(xLeft = x + -sin * offset), (yLeft = y + cos * (offset + confOffset));
|
||||||
riverPointsLeft.push([xLeft, yLeft]);
|
riverPointsLeft.push([xLeft, yLeft]);
|
||||||
xRight = x + sin * offset, yRight = y + -cos * offset;
|
(xRight = x + sin * offset), (yRight = y + -cos * offset);
|
||||||
riverPointsRight.unshift([xRight, yRight]);
|
riverPointsRight.unshift([xRight, yRight]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// end point
|
// end point
|
||||||
x = points[last][0], y = points[last][1], c = points[last][2];
|
(x = points[last][0]), (y = points[last][1]), (c = points[last][2]);
|
||||||
if (c) extraOffset += Math.atan(c * 10 / widening); // add extra width on river confluence
|
if (c) extraOffset += Math.atan((c * 10) / widening); // add extra width on river confluence
|
||||||
angle = Math.atan2(points[last - 1][1] - y, points[last - 1][0] - x);
|
angle = Math.atan2(points[last - 1][1] - y, points[last - 1][0] - x);
|
||||||
sin = Math.sin(angle), cos = Math.cos(angle);
|
(sin = Math.sin(angle)), (cos = Math.cos(angle));
|
||||||
xLeft = x + -sin * offset, yLeft = y + cos * offset;
|
(xLeft = x + -sin * offset), (yLeft = y + cos * offset);
|
||||||
riverPointsLeft.push([xLeft, yLeft]);
|
riverPointsLeft.push([xLeft, yLeft]);
|
||||||
xRight = x + sin * offset, yRight = y + -cos * offset;
|
(xRight = x + sin * offset), (yRight = y + -cos * offset);
|
||||||
riverPointsRight.unshift([xRight, yRight]);
|
riverPointsRight.unshift([xRight, yRight]);
|
||||||
|
|
||||||
// generate polygon path and return
|
// generate polygon path and return
|
||||||
|
|
@ -308,27 +339,27 @@ const getPath = function(points, widthFactor = 1, sourceWidth = .1) {
|
||||||
let left = lineGen(riverPointsLeft);
|
let left = lineGen(riverPointsLeft);
|
||||||
left = left.substring(left.indexOf("C"));
|
left = left.substring(left.indexOf("C"));
|
||||||
return [round(right + left, 2), rn(riverLength, 2), offset];
|
return [round(right + left, 2), rn(riverLength, 2), offset];
|
||||||
}
|
};
|
||||||
|
|
||||||
const specify = function () {
|
const specify = function () {
|
||||||
const rivers = pack.rivers;
|
const rivers = pack.rivers;
|
||||||
if (!rivers.length) return;
|
if (!rivers.length) return;
|
||||||
Math.random = aleaPRNG(seed);
|
Math.random = aleaPRNG(seed);
|
||||||
const tresholdElement = Math.ceil(rivers.length * .15);
|
const thresholdElement = Math.ceil(rivers.length * 0.15);
|
||||||
const smallLength = rivers.map(r => r.length || 0).sort((a, b) => a-b)[tresholdElement];
|
const smallLength = rivers.map(r => r.length || 0).sort((a, b) => a - b)[thresholdElement];
|
||||||
const smallType = {"Creek":9, "River":3, "Brook":3, "Stream":1}; // weighted small river types
|
const smallType = {Creek: 9, River: 3, Brook: 3, Stream: 1}; // weighted small river types
|
||||||
|
|
||||||
for (const r of rivers) {
|
for (const r of rivers) {
|
||||||
r.basin = getBasin(r.i);
|
r.basin = getBasin(r.i);
|
||||||
r.name = getName(r.mouth);
|
r.name = getName(r.mouth);
|
||||||
const small = r.length < smallLength;
|
const small = r.length < smallLength;
|
||||||
r.type = r.parent && !(r.i%6) ? small ? "Branch" : "Fork" : small ? rw(smallType) : "River";
|
r.type = r.parent && !(r.i % 6) ? (small ? "Branch" : "Fork") : small ? rw(smallType) : "River";
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getName = function (cell) {
|
const getName = function (cell) {
|
||||||
return Names.getCulture(pack.cells.culture[cell]);
|
return Names.getCulture(pack.cells.culture[cell]);
|
||||||
}
|
};
|
||||||
|
|
||||||
// remove river and all its tributaries
|
// remove river and all its tributaries
|
||||||
const remove = function (id) {
|
const remove = function (id) {
|
||||||
|
|
@ -342,14 +373,13 @@ const remove = function(id) {
|
||||||
cells.conf[i] = 0;
|
cells.conf[i] = 0;
|
||||||
});
|
});
|
||||||
pack.rivers = pack.rivers.filter(r => !riversToRemove.includes(r.i));
|
pack.rivers = pack.rivers.filter(r => !riversToRemove.includes(r.i));
|
||||||
}
|
};
|
||||||
|
|
||||||
const getBasin = function (r) {
|
const getBasin = function (r) {
|
||||||
const parent = pack.rivers.find(river => river.i === r)?.parent;
|
const parent = pack.rivers.find(river => river.i === r)?.parent;
|
||||||
if (!parent || r === parent) return r;
|
if (!parent || r === parent) return r;
|
||||||
return getBasin(parent);
|
return getBasin(parent);
|
||||||
}
|
};
|
||||||
|
|
||||||
return {generate, resolveDepressions, addMeandering, getPath, specify, getName, getBasin, remove};
|
return {generate, alterHeights, resolveDepressions, addMeandering, getPath, specify, getName, getBasin, remove};
|
||||||
|
});
|
||||||
})));
|
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,36 @@
|
||||||
(function (global, factory) {
|
(function (global, factory) {
|
||||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
typeof exports === "object" && typeof module !== "undefined" ? (module.exports = factory()) : typeof define === "function" && define.amd ? define(factory) : (global.Routes = factory());
|
||||||
typeof define === 'function' && define.amd ? define(factory) :
|
})(this, function () {
|
||||||
(global.Routes = factory());
|
"use strict";
|
||||||
}(this, (function () {'use strict';
|
|
||||||
|
|
||||||
const getRoads = function () {
|
const getRoads = function () {
|
||||||
TIME && console.time("generateMainRoads");
|
TIME && console.time("generateMainRoads");
|
||||||
const cells = pack.cells, burgs = pack.burgs.filter(b => b.i && !b.removed);
|
const cells = pack.cells;
|
||||||
|
const burgs = pack.burgs.filter(b => b.i && !b.removed);
|
||||||
const capitals = burgs.filter(b => b.capital);
|
const capitals = burgs.filter(b => b.capital);
|
||||||
|
|
||||||
if (capitals.length < 2) return []; // not enough capitals to build main roads
|
if (capitals.length < 2) return []; // not enough capitals to build main roads
|
||||||
const paths = []; // array to store path segments
|
const paths = []; // array to store path segments
|
||||||
|
|
||||||
for (const b of capitals) {
|
for (const b of capitals) {
|
||||||
const connect = capitals.filter(c => c.i > b.i && c.feature === b.feature);
|
const connect = capitals.filter(c => c.i > b.i && c.feature === b.feature);
|
||||||
if (!connect.length) continue;
|
if (!connect.length) continue;
|
||||||
const farthest = d3.scan(connect, (a, c) => ((c.y - b.y) ** 2 + (c.x - b.x) ** 2) - ((a.y - b.y) ** 2 + (a.x - b.x) ** 2));
|
const farthest = d3.scan(connect, (a, c) => (c.y - b.y) ** 2 + (c.x - b.x) ** 2 - ((a.y - b.y) ** 2 + (a.x - b.x) ** 2));
|
||||||
const [from, exit] = findLandPath(b.cell, connect[farthest].cell, null);
|
const [from, exit] = findLandPath(b.cell, connect[farthest].cell, null);
|
||||||
const segments = restorePath(b.cell, exit, "main", from);
|
const segments = restorePath(b.cell, exit, "main", from);
|
||||||
segments.forEach(s => paths.push(s));
|
segments.forEach(s => paths.push(s));
|
||||||
}
|
}
|
||||||
|
|
||||||
cells.i.forEach(i => cells.s[i] += cells.road[i] / 2); // add roads to suitability score
|
cells.i.forEach(i => (cells.s[i] += cells.road[i] / 2)); // add roads to suitability score
|
||||||
TIME && console.timeEnd("generateMainRoads");
|
TIME && console.timeEnd("generateMainRoads");
|
||||||
return paths;
|
return paths;
|
||||||
}
|
};
|
||||||
|
|
||||||
const getTrails = function () {
|
const getTrails = function () {
|
||||||
TIME && console.time("generateTrails");
|
TIME && console.time("generateTrails");
|
||||||
const cells = pack.cells, burgs = pack.burgs.filter(b => b.i && !b.removed);
|
const cells = pack.cells;
|
||||||
|
const burgs = pack.burgs.filter(b => b.i && !b.removed);
|
||||||
|
|
||||||
if (burgs.length < 2) return []; // not enough burgs to build trails
|
if (burgs.length < 2) return []; // not enough burgs to build trails
|
||||||
|
|
||||||
let paths = []; // array to store path segments
|
let paths = []; // array to store path segments
|
||||||
|
|
@ -39,7 +42,7 @@
|
||||||
let path = [];
|
let path = [];
|
||||||
if (!i) {
|
if (!i) {
|
||||||
// build trail from the first burg on island to the farthest one on the same island
|
// build trail from the first burg on island to the farthest one on the same island
|
||||||
const farthest = d3.scan(isle, (a, c) => ((c.y - b.y) ** 2 + (c.x - b.x) ** 2) - ((a.y - b.y) ** 2 + (a.x - b.x) ** 2));
|
const farthest = d3.scan(isle, (a, c) => (c.y - b.y) ** 2 + (c.x - b.x) ** 2 - ((a.y - b.y) ** 2 + (a.x - b.x) ** 2));
|
||||||
const to = isle[farthest].cell;
|
const to = isle[farthest].cell;
|
||||||
if (cells.road[to]) return;
|
if (cells.road[to]) return;
|
||||||
const [from, exit] = findLandPath(b.cell, to, null);
|
const [from, exit] = findLandPath(b.cell, to, null);
|
||||||
|
|
@ -57,21 +60,26 @@
|
||||||
|
|
||||||
TIME && console.timeEnd("generateTrails");
|
TIME && console.timeEnd("generateTrails");
|
||||||
return paths;
|
return paths;
|
||||||
}
|
};
|
||||||
|
|
||||||
const getSearoutes = function () {
|
const getSearoutes = function () {
|
||||||
TIME && console.time("generateSearoutes");
|
TIME && console.time("generateSearoutes");
|
||||||
const allPorts = pack.burgs.filter(b => b.port > 0 && !b.removed);
|
const {cells, burgs, features} = pack;
|
||||||
if (allPorts.length < 2) return [];
|
const allPorts = burgs.filter(b => b.port > 0 && !b.removed);
|
||||||
|
|
||||||
const bodies = new Set(allPorts.map(b => b.port)); // features with ports
|
if (!allPorts.length) return [];
|
||||||
|
|
||||||
|
const bodies = new Set(allPorts.map(b => b.port)); // water features with ports
|
||||||
let paths = []; // array to store path segments
|
let paths = []; // array to store path segments
|
||||||
const connected = []; // store cell id of connected burgs
|
const connected = []; // store cell id of connected burgs
|
||||||
|
|
||||||
bodies.forEach(function(f) {
|
bodies.forEach(f => {
|
||||||
const ports = allPorts.filter(b => b.port === f); // all ports on the same feature
|
const ports = allPorts.filter(b => b.port === f); // all ports on the same feature
|
||||||
if (ports.length < 2) return;
|
if (!ports.length) return;
|
||||||
|
|
||||||
|
if (features[f].border) addOverseaRoute(f, ports[0]);
|
||||||
|
|
||||||
|
// get inner-map routes
|
||||||
for (let s = 0; s < ports.length; s++) {
|
for (let s = 0; s < ports.length; s++) {
|
||||||
const source = ports[s].cell;
|
const source = ports[s].cell;
|
||||||
if (connected[source]) continue;
|
if (connected[source]) continue;
|
||||||
|
|
@ -90,51 +98,53 @@
|
||||||
connected[target] = 1;
|
connected[target] = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function addOverseaRoute(f, port) {
|
||||||
|
const {x, y, cell: source} = port;
|
||||||
|
const dist = p => Math.abs(p[0] - x) + Math.abs(p[1] - y);
|
||||||
|
const [x1, y1] = [
|
||||||
|
[0, y],
|
||||||
|
[x, 0],
|
||||||
|
[graphWidth, y],
|
||||||
|
[x, graphHeight]
|
||||||
|
].sort((a, b) => dist(a) - dist(b))[0];
|
||||||
|
const target = findCell(x1, y1);
|
||||||
|
|
||||||
|
if (cells.f[target] === f && cells.h[target] < 20) {
|
||||||
|
const [from, exit, passable] = findOceanPath(target, source, true);
|
||||||
|
|
||||||
|
if (passable) {
|
||||||
|
const path = restorePath(target, exit, "ocean", from);
|
||||||
|
paths = paths.concat(path);
|
||||||
|
last(path).push([x1, y1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TIME && console.timeEnd("generateSearoutes");
|
TIME && console.timeEnd("generateSearoutes");
|
||||||
return paths;
|
return paths;
|
||||||
}
|
};
|
||||||
|
|
||||||
const draw = function(main, small, ocean) {
|
const draw = function (main, small, water) {
|
||||||
TIME && console.time("drawRoutes");
|
TIME && console.time("drawRoutes");
|
||||||
const cells = pack.cells, burgs = pack.burgs;
|
const {cells, burgs} = pack;
|
||||||
|
const {burg, p} = cells;
|
||||||
|
|
||||||
|
const getBurgCoords = b => [burgs[b].x, burgs[b].y];
|
||||||
|
const getPathPoints = cells => cells.map(i => (Array.isArray(i) ? i : burg[i] ? getBurgCoords(burg[i]) : p[i]));
|
||||||
|
const getPath = segment => round(lineGen(getPathPoints(segment)), 1);
|
||||||
|
const getPathsHTML = (paths, type) => paths.map((path, i) => `<path id="${type}${i}" d="${getPath(path)}" />`).join("");
|
||||||
|
|
||||||
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
|
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
|
||||||
|
roads.html(getPathsHTML(main, "road"));
|
||||||
|
trails.html(getPathsHTML(small, "trail"));
|
||||||
|
|
||||||
// main routes
|
|
||||||
roads.selectAll("path").data(main).enter().append("path")
|
|
||||||
.attr("id", (d, i) => "road" + i)
|
|
||||||
.attr("d", d => round(lineGen(d.map(c => {
|
|
||||||
const b = cells.burg[c];
|
|
||||||
const x = b ? burgs[b].x : cells.p[c][0];
|
|
||||||
const y = b ? burgs[b].y : cells.p[c][1];
|
|
||||||
return [x, y];
|
|
||||||
})), 1));
|
|
||||||
|
|
||||||
// small routes
|
|
||||||
trails.selectAll("path").data(small).enter().append("path")
|
|
||||||
.attr("id", (d, i) => "trail" + i)
|
|
||||||
.attr("d", d => round(lineGen(d.map(c => {
|
|
||||||
const b = cells.burg[c];
|
|
||||||
const x = b ? burgs[b].x : cells.p[c][0];
|
|
||||||
const y = b ? burgs[b].y : cells.p[c][1];
|
|
||||||
return [x, y];
|
|
||||||
})), 1));
|
|
||||||
|
|
||||||
// ocean routes
|
|
||||||
lineGen.curve(d3.curveBundle.beta(1));
|
lineGen.curve(d3.curveBundle.beta(1));
|
||||||
searoutes.selectAll("path").data(ocean).enter().append("path")
|
searoutes.html(getPathsHTML(water, "searoute"));
|
||||||
.attr("id", (d, i) => "searoute" + i)
|
|
||||||
.attr("d", d => round(lineGen(d.map(c => {
|
|
||||||
const b = cells.burg[c];
|
|
||||||
const x = b ? burgs[b].x : cells.p[c][0];
|
|
||||||
const y = b ? burgs[b].y : cells.p[c][1];
|
|
||||||
return [x, y];
|
|
||||||
})), 1));
|
|
||||||
|
|
||||||
TIME && console.timeEnd("drawRoutes");
|
TIME && console.timeEnd("drawRoutes");
|
||||||
}
|
};
|
||||||
|
|
||||||
const regenerate = function () {
|
const regenerate = function () {
|
||||||
routes.selectAll("path").remove();
|
routes.selectAll("path").remove();
|
||||||
|
|
@ -142,9 +152,9 @@
|
||||||
pack.cells.crossroad = new Uint16Array(pack.cells.i.length);
|
pack.cells.crossroad = new Uint16Array(pack.cells.i.length);
|
||||||
const main = getRoads();
|
const main = getRoads();
|
||||||
const small = getTrails();
|
const small = getTrails();
|
||||||
const ocean = getSearoutes();
|
const water = getSearoutes();
|
||||||
draw(main, small, ocean);
|
draw(main, small, water);
|
||||||
}
|
};
|
||||||
|
|
||||||
return {getRoads, getTrails, getSearoutes, draw, regenerate};
|
return {getRoads, getTrails, getSearoutes, draw, regenerate};
|
||||||
|
|
||||||
|
|
@ -152,11 +162,14 @@
|
||||||
function findLandPath(start, exit = null, toRoad = null) {
|
function findLandPath(start, exit = null, toRoad = null) {
|
||||||
const cells = pack.cells;
|
const cells = pack.cells;
|
||||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||||
const cost = [], from = [];
|
const cost = [],
|
||||||
|
from = [];
|
||||||
queue.queue({e: start, p: 0});
|
queue.queue({e: start, p: 0});
|
||||||
|
|
||||||
while (queue.length) {
|
while (queue.length) {
|
||||||
const next = queue.dequeue(), n = next.e, p = next.p;
|
const next = queue.dequeue(),
|
||||||
|
n = next.e,
|
||||||
|
p = next.p;
|
||||||
if (toRoad && cells.road[n]) return [from, n];
|
if (toRoad && cells.road[n]) return [from, n];
|
||||||
|
|
||||||
for (const c of cells.c[n]) {
|
for (const c of cells.c[n]) {
|
||||||
|
|
@ -175,7 +188,6 @@
|
||||||
cost[c] = totalCost;
|
cost[c] = totalCost;
|
||||||
queue.queue({e: c, p: totalCost});
|
queue.queue({e: c, p: totalCost});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return [from, exit];
|
return [from, exit];
|
||||||
}
|
}
|
||||||
|
|
@ -183,7 +195,9 @@
|
||||||
function restorePath(start, end, type, from) {
|
function restorePath(start, end, type, from) {
|
||||||
const cells = pack.cells;
|
const cells = pack.cells;
|
||||||
const path = []; // to store all segments;
|
const path = []; // to store all segments;
|
||||||
let segment = [], current = end, prev = end;
|
let segment = [],
|
||||||
|
current = end,
|
||||||
|
prev = end;
|
||||||
const score = type === "main" ? 5 : 1; // to incrade road score at cell
|
const score = type === "main" ? 5 : 1; // to incrade road score at cell
|
||||||
|
|
||||||
if (type === "ocean" || !cells.road[prev]) segment.push(end);
|
if (type === "ocean" || !cells.road[prev]) segment.push(end);
|
||||||
|
|
@ -197,8 +211,14 @@
|
||||||
if (segment.length) {
|
if (segment.length) {
|
||||||
segment.push(current);
|
segment.push(current);
|
||||||
path.push(segment);
|
path.push(segment);
|
||||||
if (segment[0] !== end) {cells.road[segment[0]] += score; cells.crossroad[segment[0]] += score;}
|
if (segment[0] !== end) {
|
||||||
if (current !== start) {cells.road[current] += score; cells.crossroad[current] += score;}
|
cells.road[segment[0]] += score;
|
||||||
|
cells.crossroad[segment[0]] += score;
|
||||||
|
}
|
||||||
|
if (current !== start) {
|
||||||
|
cells.road[current] += score;
|
||||||
|
cells.crossroad[current] += score;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
segment = [];
|
segment = [];
|
||||||
prev = current;
|
prev = current;
|
||||||
|
|
@ -218,29 +238,34 @@
|
||||||
|
|
||||||
// find water paths
|
// find water paths
|
||||||
function findOceanPath(start, exit = null, toRoute = null) {
|
function findOceanPath(start, exit = null, toRoute = null) {
|
||||||
const cells = pack.cells, temp = grid.cells.temp;
|
const cells = pack.cells,
|
||||||
|
temp = grid.cells.temp;
|
||||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||||
const cost = [], from = [];
|
const cost = [],
|
||||||
|
from = [];
|
||||||
queue.queue({e: start, p: 0});
|
queue.queue({e: start, p: 0});
|
||||||
|
|
||||||
while (queue.length) {
|
while (queue.length) {
|
||||||
const next = queue.dequeue(), n = next.e, p = next.p;
|
const next = queue.dequeue(),
|
||||||
|
n = next.e,
|
||||||
|
p = next.p;
|
||||||
if (toRoute && n !== start && cells.road[n]) return [from, n, true];
|
if (toRoute && n !== start && cells.road[n]) return [from, n, true];
|
||||||
|
|
||||||
for (const c of cells.c[n]) {
|
for (const c of cells.c[n]) {
|
||||||
if (c === exit) {from[c] = n; return [from, exit, true];}
|
if (c === exit) {
|
||||||
|
from[c] = n;
|
||||||
|
return [from, exit, true];
|
||||||
|
}
|
||||||
if (cells.h[c] >= 20) continue; // ignore land cells
|
if (cells.h[c] >= 20) continue; // ignore land cells
|
||||||
if (temp[cells.g[c]] <= -5) continue; // ignore cells with term <= -5
|
if (temp[cells.g[c]] <= -5) continue; // ignore cells with term <= -5
|
||||||
const dist2 = (cells.p[c][1] - cells.p[n][1]) ** 2 + (cells.p[c][0] - cells.p[n][0]) ** 2;
|
const dist2 = (cells.p[c][1] - cells.p[n][1]) ** 2 + (cells.p[c][0] - cells.p[n][0]) ** 2;
|
||||||
const totalCost = p + (cells.road[c] ? 1 + dist2 / 2 : dist2 + (cells.t[c] ? 1 : 100));
|
const totalCost = p + (cells.road[c] ? 1 + dist2 / 2 : dist2 + (cells.t[c] ? 1 : 100));
|
||||||
|
|
||||||
if (from[c] || totalCost >= cost[c]) continue;
|
if (from[c] || totalCost >= cost[c]) continue;
|
||||||
from[c] = n, cost[c] = totalCost;
|
(from[c] = n), (cost[c] = totalCost);
|
||||||
queue.queue({e: c, p: totalCost});
|
queue.queue({e: c, p: totalCost});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return [from, exit, false];
|
return [from, exit, false];
|
||||||
}
|
}
|
||||||
|
});
|
||||||
})));
|
|
||||||
|
|
|
||||||
636
modules/save.js
Normal file
636
modules/save.js
Normal file
|
|
@ -0,0 +1,636 @@
|
||||||
|
// Functions to save and load the map
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// download map as SVG
|
||||||
|
async function saveSVG() {
|
||||||
|
TIME && console.time("saveSVG");
|
||||||
|
const url = await getMapURL("svg");
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.download = getFileName() + ".svg";
|
||||||
|
link.href = url;
|
||||||
|
link.click();
|
||||||
|
|
||||||
|
tip(`${link.download} is saved. Open "Downloads" screen (crtl + J) to check. You can set image scale in options`, true, "success", 5000);
|
||||||
|
TIME && console.timeEnd("saveSVG");
|
||||||
|
}
|
||||||
|
|
||||||
|
// download map as PNG
|
||||||
|
async function savePNG() {
|
||||||
|
TIME && console.time("savePNG");
|
||||||
|
const url = await getMapURL("png");
|
||||||
|
|
||||||
|
const link = document.createElement("a");
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
canvas.width = svgWidth * pngResolutionInput.value;
|
||||||
|
canvas.height = svgHeight * pngResolutionInput.value;
|
||||||
|
const img = new Image();
|
||||||
|
img.src = url;
|
||||||
|
|
||||||
|
img.onload = function () {
|
||||||
|
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||||
|
link.download = getFileName() + ".png";
|
||||||
|
canvas.toBlob(function (blob) {
|
||||||
|
link.href = window.URL.createObjectURL(blob);
|
||||||
|
link.click();
|
||||||
|
window.setTimeout(function () {
|
||||||
|
canvas.remove();
|
||||||
|
window.URL.revokeObjectURL(link.href);
|
||||||
|
tip(`${link.download} is saved. Open "Downloads" screen (crtl + J) to check. You can set image scale in options`, true, "success", 5000);
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
TIME && console.timeEnd("savePNG");
|
||||||
|
}
|
||||||
|
|
||||||
|
// download map as JPEG
|
||||||
|
async function saveJPEG() {
|
||||||
|
TIME && console.time("saveJPEG");
|
||||||
|
const url = await getMapURL("png");
|
||||||
|
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = svgWidth * pngResolutionInput.value;
|
||||||
|
canvas.height = svgHeight * pngResolutionInput.value;
|
||||||
|
const img = new Image();
|
||||||
|
img.src = url;
|
||||||
|
|
||||||
|
img.onload = async function () {
|
||||||
|
canvas.getContext("2d").drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||||
|
const quality = Math.min(rn(1 - pngResolutionInput.value / 20, 2), 0.92);
|
||||||
|
const URL = await canvas.toDataURL("image/jpeg", quality);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.download = getFileName() + ".jpeg";
|
||||||
|
link.href = URL;
|
||||||
|
link.click();
|
||||||
|
tip(`${link.download} is saved. Open "Downloads" screen (CTRL + J) to check`, true, "success", 7000);
|
||||||
|
window.setTimeout(() => window.URL.revokeObjectURL(URL), 5000);
|
||||||
|
};
|
||||||
|
|
||||||
|
TIME && console.timeEnd("saveJPEG");
|
||||||
|
}
|
||||||
|
|
||||||
|
// download map as png tiles
|
||||||
|
async function saveTiles() {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
// download schema
|
||||||
|
const urlSchema = await getMapURL("tiles", "schema");
|
||||||
|
const zip = new JSZip();
|
||||||
|
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
canvas.width = graphWidth;
|
||||||
|
canvas.height = graphHeight;
|
||||||
|
|
||||||
|
const imgSchema = new Image();
|
||||||
|
imgSchema.src = urlSchema;
|
||||||
|
imgSchema.onload = function () {
|
||||||
|
ctx.drawImage(imgSchema, 0, 0, canvas.width, canvas.height);
|
||||||
|
canvas.toBlob(blob => zip.file(`fmg_tile_schema.png`, blob));
|
||||||
|
};
|
||||||
|
|
||||||
|
// download tiles
|
||||||
|
const url = await getMapURL("tiles");
|
||||||
|
const tilesX = +document.getElementById("tileColsInput").value;
|
||||||
|
const tilesY = +document.getElementById("tileRowsInput").value;
|
||||||
|
const scale = +document.getElementById("tileScaleInput").value;
|
||||||
|
|
||||||
|
const tileW = (graphWidth / tilesX) | 0;
|
||||||
|
const tileH = (graphHeight / tilesY) | 0;
|
||||||
|
const tolesTotal = tilesX * tilesY;
|
||||||
|
|
||||||
|
const width = graphWidth * scale;
|
||||||
|
const height = width * (tileH / tileW);
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
|
||||||
|
let loaded = 0;
|
||||||
|
const img = new Image();
|
||||||
|
img.src = url;
|
||||||
|
img.onload = function () {
|
||||||
|
for (let y = 0, i = 0; y + tileH <= graphHeight; y += tileH) {
|
||||||
|
for (let x = 0; x + tileW <= graphWidth; x += tileW, i++) {
|
||||||
|
ctx.drawImage(img, x, y, tileW, tileH, 0, 0, width, height);
|
||||||
|
const name = `fmg_tile_${i}.png`;
|
||||||
|
canvas.toBlob(blob => {
|
||||||
|
zip.file(name, blob);
|
||||||
|
loaded += 1;
|
||||||
|
if (loaded === tolesTotal) return downloadZip();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function downloadZip() {
|
||||||
|
const name = `${getFileName()}.zip`;
|
||||||
|
zip.generateAsync({type: "blob"}).then(blob => {
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = URL.createObjectURL(blob);
|
||||||
|
link.download = name;
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
|
||||||
|
setTimeout(() => URL.revokeObjectURL(link.href), 5000);
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse map svg to object url
|
||||||
|
async function getMapURL(type, subtype) {
|
||||||
|
const cloneEl = document.getElementById("map").cloneNode(true); // clone svg
|
||||||
|
cloneEl.id = "fantasyMap";
|
||||||
|
document.body.appendChild(cloneEl);
|
||||||
|
const clone = d3.select(cloneEl);
|
||||||
|
if (subtype !== "schema") clone.select("#debug").remove();
|
||||||
|
|
||||||
|
const cloneDefs = cloneEl.getElementsByTagName("defs")[0];
|
||||||
|
const svgDefs = document.getElementById("defElements");
|
||||||
|
|
||||||
|
const isFirefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
|
||||||
|
if (isFirefox && type === "mesh") clone.select("#oceanPattern").remove();
|
||||||
|
if (subtype === "globe") clone.select("#scaleBar").remove();
|
||||||
|
if (subtype === "noWater") {
|
||||||
|
clone.select("#oceanBase").attr("opacity", 0);
|
||||||
|
clone.select("#oceanPattern").attr("opacity", 0);
|
||||||
|
}
|
||||||
|
if (type !== "png") {
|
||||||
|
// reset transform to show the whole map
|
||||||
|
clone.attr("width", graphWidth).attr("height", graphHeight);
|
||||||
|
clone.select("#viewbox").attr("transform", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "svg") removeUnusedElements(clone);
|
||||||
|
if (customization && type === "mesh") updateMeshCells(clone);
|
||||||
|
inlineStyle(clone);
|
||||||
|
|
||||||
|
// remove unused filters
|
||||||
|
const filters = cloneEl.querySelectorAll("filter");
|
||||||
|
for (let i = 0; i < filters.length; i++) {
|
||||||
|
const id = filters[i].id;
|
||||||
|
if (cloneEl.querySelector("[filter='url(#" + id + ")']")) continue;
|
||||||
|
if (cloneEl.getAttribute("filter") === "url(#" + id + ")") continue;
|
||||||
|
filters[i].remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove unused patterns
|
||||||
|
const patterns = cloneEl.querySelectorAll("pattern");
|
||||||
|
for (let i = 0; i < patterns.length; i++) {
|
||||||
|
const id = patterns[i].id;
|
||||||
|
if (cloneEl.querySelector("[fill='url(#" + id + ")']")) continue;
|
||||||
|
patterns[i].remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove unused symbols
|
||||||
|
const symbols = cloneEl.querySelectorAll("symbol");
|
||||||
|
for (let i = 0; i < symbols.length; i++) {
|
||||||
|
const id = symbols[i].id;
|
||||||
|
if (cloneEl.querySelector("use[*|href='#" + id + "']")) continue;
|
||||||
|
symbols[i].remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// add displayed emblems
|
||||||
|
if (layerIsOn("toggleEmblems") && emblems.selectAll("use").size()) {
|
||||||
|
cloneEl
|
||||||
|
.getElementById("emblems")
|
||||||
|
?.querySelectorAll("use")
|
||||||
|
.forEach(el => {
|
||||||
|
const href = el.getAttribute("href") || el.getAttribute("xlink:href");
|
||||||
|
if (!href) return;
|
||||||
|
const emblem = document.getElementById(href.slice(1));
|
||||||
|
if (emblem) cloneDefs.append(emblem.cloneNode(true));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cloneDefs.querySelector("#defs-emblems")?.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace ocean pattern href to base64
|
||||||
|
if (PRODUCTION && cloneEl.getElementById("oceanicPattern")) {
|
||||||
|
const el = cloneEl.getElementById("oceanicPattern");
|
||||||
|
const url = el.getAttribute("href");
|
||||||
|
await new Promise(resolve => {
|
||||||
|
getBase64(url, base64 => {
|
||||||
|
el.setAttribute("href", base64);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// add relief icons
|
||||||
|
if (cloneEl.getElementById("terrain")) {
|
||||||
|
const uniqueElements = new Set();
|
||||||
|
const terrainNodes = cloneEl.getElementById("terrain").childNodes;
|
||||||
|
for (let i = 0; i < terrainNodes.length; i++) {
|
||||||
|
const href = terrainNodes[i].getAttribute("href") || terrainNodes[i].getAttribute("xlink:href");
|
||||||
|
uniqueElements.add(href);
|
||||||
|
}
|
||||||
|
|
||||||
|
const defsRelief = svgDefs.getElementById("defs-relief");
|
||||||
|
for (const terrain of [...uniqueElements]) {
|
||||||
|
const element = defsRelief.querySelector(terrain);
|
||||||
|
if (element) cloneDefs.appendChild(element.cloneNode(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add wind rose
|
||||||
|
if (cloneEl.getElementById("compass")) {
|
||||||
|
const rose = svgDefs.getElementById("rose");
|
||||||
|
if (rose) cloneDefs.appendChild(rose.cloneNode(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
// add port icon
|
||||||
|
if (cloneEl.getElementById("anchors")) {
|
||||||
|
const anchor = svgDefs.getElementById("icon-anchor");
|
||||||
|
if (anchor) cloneDefs.appendChild(anchor.cloneNode(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
// add grid pattern
|
||||||
|
if (cloneEl.getElementById("gridOverlay")?.hasChildNodes()) {
|
||||||
|
const type = cloneEl.getElementById("gridOverlay").getAttribute("type");
|
||||||
|
const pattern = svgDefs.getElementById("pattern_" + type);
|
||||||
|
if (pattern) cloneDefs.appendChild(pattern.cloneNode(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cloneEl.getElementById("hatching").children.length) cloneEl.getElementById("hatching").remove(); // remove unused hatching group
|
||||||
|
if (!cloneEl.getElementById("fogging-cont")) cloneEl.getElementById("fog").remove(); // remove unused fog
|
||||||
|
if (!cloneEl.getElementById("regions")) cloneEl.getElementById("statePaths").remove(); // removed unused statePaths
|
||||||
|
if (!cloneEl.getElementById("labels")) cloneEl.getElementById("textPaths").remove(); // removed unused textPaths
|
||||||
|
|
||||||
|
// add armies style
|
||||||
|
if (cloneEl.getElementById("armies")) cloneEl.insertAdjacentHTML("afterbegin", "<style>#armies text {stroke: none; fill: #fff; text-shadow: 0 0 4px #000; dominant-baseline: central; text-anchor: middle; font-family: Helvetica; fill-opacity: 1;}#armies text.regimentIcon {font-size: .8em;}</style>");
|
||||||
|
|
||||||
|
// add xlink: for href to support svg1.1
|
||||||
|
if (type === "svg") {
|
||||||
|
cloneEl.querySelectorAll("[href]").forEach(el => {
|
||||||
|
const href = el.getAttribute("href");
|
||||||
|
el.removeAttribute("href");
|
||||||
|
el.setAttribute("xlink:href", href);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fontStyle = await GFontToDataURI(getFontsToLoad(clone)); // load non-standard fonts
|
||||||
|
if (fontStyle) clone.select("defs").append("style").text(fontStyle.join("\n")); // add font to style
|
||||||
|
clone.remove();
|
||||||
|
|
||||||
|
const serialized = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>` + new XMLSerializer().serializeToString(cloneEl);
|
||||||
|
const blob = new Blob([serialized], {type: "image/svg+xml;charset=utf-8"});
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
window.setTimeout(() => window.URL.revokeObjectURL(url), 5000);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove hidden g elements and g elements without children to make downloaded svg smaller in size
|
||||||
|
function removeUnusedElements(clone) {
|
||||||
|
if (!terrain.selectAll("use").size()) clone.select("#defs-relief").remove();
|
||||||
|
if (markers.style("display") === "none") clone.select("#defs-markers").remove();
|
||||||
|
|
||||||
|
for (let empty = 1; empty; ) {
|
||||||
|
empty = 0;
|
||||||
|
clone.selectAll("g").each(function () {
|
||||||
|
if (!this.hasChildNodes() || this.style.display === "none" || this.classList.contains("hidden")) {
|
||||||
|
empty++;
|
||||||
|
this.remove();
|
||||||
|
}
|
||||||
|
if (this.hasAttribute("display") && this.style.display === "inline") this.removeAttribute("display");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMeshCells(clone) {
|
||||||
|
const data = renderOcean.checked ? grid.cells.i : grid.cells.i.filter(i => grid.cells.h[i] >= 20);
|
||||||
|
const scheme = getColorScheme();
|
||||||
|
clone.select("#heights").attr("filter", "url(#blur1)");
|
||||||
|
clone
|
||||||
|
.select("#heights")
|
||||||
|
.selectAll("polygon")
|
||||||
|
.data(data)
|
||||||
|
.join("polygon")
|
||||||
|
.attr("points", d => getGridPolygon(d))
|
||||||
|
.attr("id", d => "cell" + d)
|
||||||
|
.attr("stroke", d => getColor(grid.cells.h[d], scheme));
|
||||||
|
}
|
||||||
|
|
||||||
|
// for each g element get inline style
|
||||||
|
function inlineStyle(clone) {
|
||||||
|
const emptyG = clone.append("g").node();
|
||||||
|
const defaultStyles = window.getComputedStyle(emptyG);
|
||||||
|
|
||||||
|
clone.selectAll("g, #ruler *, #scaleBar > text").each(function () {
|
||||||
|
const compStyle = window.getComputedStyle(this);
|
||||||
|
let style = "";
|
||||||
|
|
||||||
|
for (let i = 0; i < compStyle.length; i++) {
|
||||||
|
const key = compStyle[i];
|
||||||
|
const value = compStyle.getPropertyValue(key);
|
||||||
|
|
||||||
|
// Firefox mask hack
|
||||||
|
if (key === "mask-image" && value !== defaultStyles.getPropertyValue(key)) {
|
||||||
|
style += "mask-image: url('#land');";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "cursor") continue; // cursor should be default
|
||||||
|
if (this.hasAttribute(key)) continue; // don't add style if there is the same attribute
|
||||||
|
if (value === defaultStyles.getPropertyValue(key)) continue;
|
||||||
|
style += key + ":" + value + ";";
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in compStyle) {
|
||||||
|
const value = compStyle.getPropertyValue(key);
|
||||||
|
|
||||||
|
if (key === "cursor") continue; // cursor should be default
|
||||||
|
if (this.hasAttribute(key)) continue; // don't add style if there is the same attribute
|
||||||
|
if (value === defaultStyles.getPropertyValue(key)) continue;
|
||||||
|
style += key + ":" + value + ";";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (style != "") this.setAttribute("style", style);
|
||||||
|
});
|
||||||
|
|
||||||
|
emptyG.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// get non-standard fonts used for labels to fetch them from web
|
||||||
|
function getFontsToLoad(clone) {
|
||||||
|
const webSafe = ["Georgia", "Times+New+Roman", "Comic+Sans+MS", "Lucida+Sans+Unicode", "Courier+New", "Verdana", "Arial", "Impact"]; // fonts to not fetch
|
||||||
|
|
||||||
|
const fontsInUse = new Set(); // to store fonts currently in use
|
||||||
|
clone.selectAll("#labels > g").each(function () {
|
||||||
|
if (!this.hasChildNodes()) return;
|
||||||
|
const font = this.dataset.font;
|
||||||
|
if (!font || webSafe.includes(font)) return;
|
||||||
|
fontsInUse.add(font);
|
||||||
|
});
|
||||||
|
const legendFont = legend.attr("data-font");
|
||||||
|
if (legend.node().hasChildNodes() && !webSafe.includes(legendFont)) fontsInUse.add(legendFont);
|
||||||
|
const fonts = [...fontsInUse];
|
||||||
|
return fonts.length ? "https://fonts.googleapis.com/css?family=" + fonts.join("|") : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// code from Kaiido's answer https://stackoverflow.com/questions/42402584/how-to-use-google-fonts-in-canvas-when-drawing-dom-objects-in-svg
|
||||||
|
function GFontToDataURI(url) {
|
||||||
|
if (!url) return Promise.resolve();
|
||||||
|
return fetch(url) // first fecth the embed stylesheet page
|
||||||
|
.then(resp => resp.text()) // we only need the text of it
|
||||||
|
.then(text => {
|
||||||
|
let s = document.createElement("style");
|
||||||
|
s.innerHTML = text;
|
||||||
|
document.head.appendChild(s);
|
||||||
|
const styleSheet = Array.prototype.filter.call(document.styleSheets, sS => sS.ownerNode === s)[0];
|
||||||
|
|
||||||
|
const FontRule = rule => {
|
||||||
|
const src = rule.style.getPropertyValue("src");
|
||||||
|
const url = src ? src.split("url(")[1].split(")")[0] : "";
|
||||||
|
return {rule, src, url: url.substring(url.length - 1, 1)};
|
||||||
|
};
|
||||||
|
const fontProms = [];
|
||||||
|
|
||||||
|
for (const r of styleSheet.cssRules) {
|
||||||
|
let fR = FontRule(r);
|
||||||
|
if (!fR.url) continue;
|
||||||
|
|
||||||
|
fontProms.push(
|
||||||
|
fetch(fR.url) // fetch the actual font-file (.woff)
|
||||||
|
.then(resp => resp.blob())
|
||||||
|
.then(blob => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
let f = new FileReader();
|
||||||
|
f.onload = e => resolve(f.result);
|
||||||
|
f.readAsDataURL(blob);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(dataURL => fR.rule.cssText.replace(fR.url, dataURL))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
document.head.removeChild(s); // clean up
|
||||||
|
return Promise.all(fontProms); // wait for all this has been done
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare map data for saving
|
||||||
|
function getMapData() {
|
||||||
|
TIME && console.time("createMapDataBlob");
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const date = new Date();
|
||||||
|
const dateString = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate();
|
||||||
|
const license = "File can be loaded in azgaar.github.io/Fantasy-Map-Generator";
|
||||||
|
const params = [version, license, dateString, seed, graphWidth, graphHeight, mapId].join("|");
|
||||||
|
const settings = [distanceUnitInput.value, distanceScaleInput.value, areaUnit.value, heightUnit.value, heightExponentInput.value, temperatureScale.value, barSizeInput.value, barLabel.value, barBackOpacity.value, barBackColor.value, barPosX.value, barPosY.value, populationRate, urbanization, mapSizeOutput.value, latitudeOutput.value, temperatureEquatorOutput.value, temperaturePoleOutput.value, precOutput.value, JSON.stringify(options), mapName.value, +hideLabels.checked].join("|");
|
||||||
|
const coords = JSON.stringify(mapCoordinates);
|
||||||
|
const biomes = [biomesData.color, biomesData.habitability, biomesData.name].join("|");
|
||||||
|
const notesData = JSON.stringify(notes);
|
||||||
|
const rulersString = rulers.toString();
|
||||||
|
|
||||||
|
// clone svg
|
||||||
|
const cloneEl = document.getElementById("map").cloneNode(true);
|
||||||
|
|
||||||
|
// set transform values to default
|
||||||
|
cloneEl.setAttribute("width", graphWidth);
|
||||||
|
cloneEl.setAttribute("height", graphHeight);
|
||||||
|
cloneEl.querySelector("#viewbox").removeAttribute("transform");
|
||||||
|
|
||||||
|
// always remove rulers
|
||||||
|
cloneEl.querySelector("#ruler").innerHTML = "";
|
||||||
|
|
||||||
|
const svg_xml = new XMLSerializer().serializeToString(cloneEl);
|
||||||
|
|
||||||
|
const gridGeneral = JSON.stringify({spacing: grid.spacing, cellsX: grid.cellsX, cellsY: grid.cellsY, boundary: grid.boundary, points: grid.points, features: grid.features});
|
||||||
|
const features = JSON.stringify(pack.features);
|
||||||
|
const cultures = JSON.stringify(pack.cultures);
|
||||||
|
const states = JSON.stringify(pack.states);
|
||||||
|
const burgs = JSON.stringify(pack.burgs);
|
||||||
|
const religions = JSON.stringify(pack.religions);
|
||||||
|
const provinces = JSON.stringify(pack.provinces);
|
||||||
|
const rivers = JSON.stringify(pack.rivers);
|
||||||
|
|
||||||
|
// store name array only if it is not the same as default
|
||||||
|
const defaultNB = Names.getNameBases();
|
||||||
|
const namesData = nameBases
|
||||||
|
.map((b, i) => {
|
||||||
|
const names = defaultNB[i] && defaultNB[i].b === b.b ? "" : b.b;
|
||||||
|
return `${b.name}|${b.min}|${b.max}|${b.d}|${b.m}|${names}`;
|
||||||
|
})
|
||||||
|
.join("/");
|
||||||
|
|
||||||
|
// round population to save resources
|
||||||
|
const pop = Array.from(pack.cells.pop).map(p => rn(p, 4));
|
||||||
|
|
||||||
|
// data format as below
|
||||||
|
const data = [params, settings, coords, biomes, notesData, svg_xml, gridGeneral, grid.cells.h, grid.cells.prec, grid.cells.f, grid.cells.t, grid.cells.temp, features, cultures, states, burgs, pack.cells.biome, pack.cells.burg, pack.cells.conf, pack.cells.culture, pack.cells.fl, pop, pack.cells.r, pack.cells.road, pack.cells.s, pack.cells.state, pack.cells.religion, pack.cells.province, pack.cells.crossroad, religions, provinces, namesData, rivers, rulersString].join("\r\n");
|
||||||
|
const blob = new Blob([data], {type: "text/plain"});
|
||||||
|
|
||||||
|
TIME && console.timeEnd("createMapDataBlob");
|
||||||
|
resolve(blob);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download .map file
|
||||||
|
async function saveMap() {
|
||||||
|
if (customization) return tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error");
|
||||||
|
closeDialogs("#alert");
|
||||||
|
|
||||||
|
const blob = await getMapData();
|
||||||
|
const URL = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.download = getFileName() + ".map";
|
||||||
|
link.href = URL;
|
||||||
|
link.click();
|
||||||
|
tip(`${link.download} is saved. Open "Downloads" screen (CTRL + J) to check`, true, "success", 7000);
|
||||||
|
window.URL.revokeObjectURL(URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveGeoJSON_Cells() {
|
||||||
|
const json = {type: "FeatureCollection", features: []};
|
||||||
|
const cells = pack.cells;
|
||||||
|
const getPopulation = i => {
|
||||||
|
const [r, u] = getCellPopulation(i);
|
||||||
|
return rn(r + u);
|
||||||
|
};
|
||||||
|
const getHeight = i => parseInt(getFriendlyHeight([cells.p[i][0], cells.p[i][1]]));
|
||||||
|
|
||||||
|
cells.i.forEach(i => {
|
||||||
|
const coordinates = getCellCoordinates(cells.v[i]);
|
||||||
|
const height = getHeight(i);
|
||||||
|
const biome = cells.biome[i];
|
||||||
|
const type = pack.features[cells.f[i]].type;
|
||||||
|
const population = getPopulation(i);
|
||||||
|
const state = cells.state[i];
|
||||||
|
const province = cells.province[i];
|
||||||
|
const culture = cells.culture[i];
|
||||||
|
const religion = cells.religion[i];
|
||||||
|
const neighbors = cells.c[i];
|
||||||
|
|
||||||
|
const properties = {id: i, height, biome, type, population, state, province, culture, religion, neighbors};
|
||||||
|
const feature = {type: "Feature", geometry: {type: "Polygon", coordinates}, properties};
|
||||||
|
json.features.push(feature);
|
||||||
|
});
|
||||||
|
|
||||||
|
const name = getFileName("Cells") + ".geojson";
|
||||||
|
downloadFile(JSON.stringify(json), name, "application/json");
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveGeoJSON_Routes() {
|
||||||
|
const json = {type: "FeatureCollection", features: []};
|
||||||
|
|
||||||
|
routes.selectAll("g > path").each(function () {
|
||||||
|
const coordinates = getRoutePoints(this);
|
||||||
|
const id = this.id;
|
||||||
|
const type = this.parentElement.id;
|
||||||
|
|
||||||
|
const feature = {type: "Feature", geometry: {type: "LineString", coordinates}, properties: {id, type}};
|
||||||
|
json.features.push(feature);
|
||||||
|
});
|
||||||
|
|
||||||
|
const name = getFileName("Routes") + ".geojson";
|
||||||
|
downloadFile(JSON.stringify(json), name, "application/json");
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveGeoJSON_Rivers() {
|
||||||
|
const json = {type: "FeatureCollection", features: []};
|
||||||
|
|
||||||
|
rivers.selectAll("path").each(function () {
|
||||||
|
const coordinates = getRiverPoints(this);
|
||||||
|
const id = this.id;
|
||||||
|
const width = +this.dataset.increment;
|
||||||
|
const increment = +this.dataset.increment;
|
||||||
|
const river = pack.rivers.find(r => r.i === +id.slice(5));
|
||||||
|
const name = river ? river.name : "";
|
||||||
|
const type = river ? river.type : "";
|
||||||
|
const i = river ? river.i : "";
|
||||||
|
const basin = river ? river.basin : "";
|
||||||
|
|
||||||
|
const feature = {type: "Feature", geometry: {type: "LineString", coordinates}, properties: {id, i, basin, name, type, width, increment}};
|
||||||
|
json.features.push(feature);
|
||||||
|
});
|
||||||
|
|
||||||
|
const name = getFileName("Rivers") + ".geojson";
|
||||||
|
downloadFile(JSON.stringify(json), name, "application/json");
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveGeoJSON_Markers() {
|
||||||
|
const json = {type: "FeatureCollection", features: []};
|
||||||
|
|
||||||
|
markers.selectAll("use").each(function () {
|
||||||
|
const coordinates = getQGIScoordinates(this.dataset.x, this.dataset.y);
|
||||||
|
const id = this.id;
|
||||||
|
const type = this.dataset.id.substring(1);
|
||||||
|
const icon = document.getElementById(type).textContent;
|
||||||
|
const note = notes.length ? notes.find(note => note.id === this.id) : null;
|
||||||
|
const name = note ? note.name : "";
|
||||||
|
const legend = note ? note.legend : "";
|
||||||
|
|
||||||
|
const feature = {type: "Feature", geometry: {type: "Point", coordinates}, properties: {id, type, icon, name, legend}};
|
||||||
|
json.features.push(feature);
|
||||||
|
});
|
||||||
|
|
||||||
|
const name = getFileName("Markers") + ".geojson";
|
||||||
|
downloadFile(JSON.stringify(json), name, "application/json");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCellCoordinates(vertices) {
|
||||||
|
const p = pack.vertices.p;
|
||||||
|
const coordinates = vertices.map(n => getQGIScoordinates(p[n][0], p[n][1]));
|
||||||
|
return [coordinates.concat([coordinates[0]])];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRoutePoints(node) {
|
||||||
|
let points = [];
|
||||||
|
const l = node.getTotalLength();
|
||||||
|
const increment = l / Math.ceil(l / 2);
|
||||||
|
for (let i = 0; i <= l; i += increment) {
|
||||||
|
const p = node.getPointAtLength(i);
|
||||||
|
points.push(getQGIScoordinates(p.x, p.y));
|
||||||
|
}
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRiverPoints(node) {
|
||||||
|
let points = [];
|
||||||
|
const l = node.getTotalLength() / 2; // half-length
|
||||||
|
const increment = 0.25; // defines density of points
|
||||||
|
for (let i = l, c = i; i >= 0; i -= increment, c += increment) {
|
||||||
|
const p1 = node.getPointAtLength(i);
|
||||||
|
const p2 = node.getPointAtLength(c);
|
||||||
|
const [x, y] = getQGIScoordinates((p1.x + p2.x) / 2, (p1.y + p2.y) / 2);
|
||||||
|
points.push([x, y]);
|
||||||
|
}
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function quickSave() {
|
||||||
|
if (customization) {
|
||||||
|
tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const blob = await getMapData();
|
||||||
|
if (blob) ldb.set("lastMap", blob); // auto-save map
|
||||||
|
tip("Map is saved to browser memory. Please also save as .map file to secure progress", true, "success", 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveReminder = function () {
|
||||||
|
if (localStorage.getItem("noReminder")) return;
|
||||||
|
const message = ["Please don't forget to save your work as a .map file", "Please remember to save work as a .map file", "Saving in .map format will ensure your data won't be lost in case of issues", "Safety is number one priority. Please save the map", "Don't forget to save your map on a regular basis!", "Just a gentle reminder for you to save the map", "Please don't forget to save your progress (saving as .map is the best option)", "Don't want to be reminded about need to save? Press CTRL+Q"];
|
||||||
|
|
||||||
|
saveReminder.reminder = setInterval(() => {
|
||||||
|
if (customization) return;
|
||||||
|
tip(ra(message), true, "warn", 2500);
|
||||||
|
}, 1e6);
|
||||||
|
saveReminder.status = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
saveReminder();
|
||||||
|
|
||||||
|
function toggleSaveReminder() {
|
||||||
|
if (saveReminder.status) {
|
||||||
|
tip("Save reminder is turned off. Press CTRL+Q again to re-initiate", true, "warn", 2000);
|
||||||
|
clearInterval(saveReminder.reminder);
|
||||||
|
localStorage.setItem("noReminder", true);
|
||||||
|
saveReminder.status = 0;
|
||||||
|
} else {
|
||||||
|
tip("Save reminder is turned on. Press CTRL+Q to turn off", true, "warn", 2000);
|
||||||
|
localStorage.removeItem("noReminder");
|
||||||
|
saveReminder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
class Battle {
|
class Battle {
|
||||||
|
|
||||||
constructor(attacker, defender) {
|
constructor(attacker, defender) {
|
||||||
if (customization) return;
|
if (customization) return;
|
||||||
closeDialogs(".stable");
|
closeDialogs(".stable");
|
||||||
|
|
@ -26,7 +25,9 @@ class Battle {
|
||||||
this.getInitialMorale();
|
this.getInitialMorale();
|
||||||
|
|
||||||
$("#battleScreen").dialog({
|
$("#battleScreen").dialog({
|
||||||
title: this.name, resizable: false, width: fitContent(),
|
title: this.name,
|
||||||
|
resizable: false,
|
||||||
|
width: fitContent(),
|
||||||
position: {my: "center", at: "center", of: "#map"},
|
position: {my: "center", at: "center", of: "#map"},
|
||||||
close: () => Battle.prototype.context.cancelResults()
|
close: () => Battle.prototype.context.cancelResults()
|
||||||
});
|
});
|
||||||
|
|
@ -38,7 +39,7 @@ class Battle {
|
||||||
document.getElementById("battleType").addEventListener("click", ev => this.toggleChange(ev));
|
document.getElementById("battleType").addEventListener("click", ev => this.toggleChange(ev));
|
||||||
document.getElementById("battleType").nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changeType(ev));
|
document.getElementById("battleType").nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changeType(ev));
|
||||||
document.getElementById("battleNameShow").addEventListener("click", () => Battle.prototype.context.showNameSection());
|
document.getElementById("battleNameShow").addEventListener("click", () => Battle.prototype.context.showNameSection());
|
||||||
document.getElementById("battleNamePlace").addEventListener("change", ev => Battle.prototype.context.place = ev.target.value);
|
document.getElementById("battleNamePlace").addEventListener("change", ev => (Battle.prototype.context.place = ev.target.value));
|
||||||
document.getElementById("battleNameFull").addEventListener("change", ev => Battle.prototype.context.changeName(ev));
|
document.getElementById("battleNameFull").addEventListener("change", ev => Battle.prototype.context.changeName(ev));
|
||||||
document.getElementById("battleNameCulture").addEventListener("click", () => Battle.prototype.context.generateName("culture"));
|
document.getElementById("battleNameCulture").addEventListener("click", () => Battle.prototype.context.generateName("culture"));
|
||||||
document.getElementById("battleNameRandom").addEventListener("click", () => Battle.prototype.context.generateName("random"));
|
document.getElementById("battleNameRandom").addEventListener("click", () => Battle.prototype.context.generateName("random"));
|
||||||
|
|
@ -69,9 +70,9 @@ class Battle {
|
||||||
if (typesA.every(t => t === "aviation") && typesD.every(t => t === "aviation")) return "air"; // if attackers and defender have only aviation units
|
if (typesA.every(t => t === "aviation") && typesD.every(t => t === "aviation")) return "air"; // if attackers and defender have only aviation units
|
||||||
if (attacker.n && !defender.n && typesA.some(t => t !== "naval")) return "landing"; // if attacked is naval with non-naval units and defender is not naval
|
if (attacker.n && !defender.n && typesA.some(t => t !== "naval")) return "landing"; // if attacked is naval with non-naval units and defender is not naval
|
||||||
if (!defender.n && pack.burgs[pack.cells.burg[this.cell]].walls) return "siege"; // defender is in walled town
|
if (!defender.n && pack.burgs[pack.cells.burg[this.cell]].walls) return "siege"; // defender is in walled town
|
||||||
if (P(.1) && [5,6,7,8,9,12].includes(pack.cells.biome[this.cell])) return "ambush"; // 20% if defenders are in forest or marshes
|
if (P(0.1) && [5, 6, 7, 8, 9, 12].includes(pack.cells.biome[this.cell])) return "ambush"; // 20% if defenders are in forest or marshes
|
||||||
return "field";
|
return "field";
|
||||||
}
|
};
|
||||||
|
|
||||||
this.type = getType();
|
this.type = getType();
|
||||||
this.setType();
|
this.setType();
|
||||||
|
|
@ -91,9 +92,13 @@ class Battle {
|
||||||
}
|
}
|
||||||
|
|
||||||
definePlace() {
|
definePlace() {
|
||||||
const cells = pack.cells, i = this.cell;
|
const cells = pack.cells,
|
||||||
|
i = this.cell;
|
||||||
const burg = cells.burg[i] ? pack.burgs[cells.burg[i]].name : null;
|
const burg = cells.burg[i] ? pack.burgs[cells.burg[i]].name : null;
|
||||||
const getRiver = i => {const river = pack.rivers.find(r => r.i === i); return river.name + " " + river.type};
|
const getRiver = i => {
|
||||||
|
const river = pack.rivers.find(r => r.i === i);
|
||||||
|
return river.name + " " + river.type;
|
||||||
|
};
|
||||||
const river = !burg && cells.r[i] ? getRiver(cells.r[i]) : null;
|
const river = !burg && cells.r[i] ? getRiver(cells.r[i]) : null;
|
||||||
const proper = burg || river ? null : Names.getCulture(cells.culture[this.cell]);
|
const proper = burg || river ? null : Names.getCulture(cells.culture[this.cell]);
|
||||||
return burg ? burg : river ? river : proper;
|
return burg ? burg : river ? river : proper;
|
||||||
|
|
@ -105,7 +110,7 @@ class Battle {
|
||||||
if (this.type === "siege") return "Siege of " + this.place;
|
if (this.type === "siege") return "Siege of " + this.place;
|
||||||
if (this.type === "ambush") return this.place + " Ambush";
|
if (this.type === "ambush") return this.place + " Ambush";
|
||||||
if (this.type === "landing") return this.place + " Landing";
|
if (this.type === "landing") return this.place + " Landing";
|
||||||
if (this.type === "air") return `${this.place} ${P(.8) ? "Air Battle" : "Dogfight"}`;
|
if (this.type === "air") return `${this.place} ${P(0.8) ? "Air Battle" : "Dogfight"}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getTypeName() {
|
getTypeName() {
|
||||||
|
|
@ -121,7 +126,7 @@ class Battle {
|
||||||
let headers = "<thead><tr><th></th><th></th>";
|
let headers = "<thead><tr><th></th><th></th>";
|
||||||
|
|
||||||
for (const u of options.military) {
|
for (const u of options.military) {
|
||||||
const label = capitalize(u.name.replace(/_/g, ' '));
|
const label = capitalize(u.name.replace(/_/g, " "));
|
||||||
headers += `<th data-tip="${label}">${u.icon}</th>`;
|
headers += `<th data-tip="${label}">${u.icon}</th>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,11 +135,11 @@ class Battle {
|
||||||
}
|
}
|
||||||
|
|
||||||
addRegiment(side, regiment) {
|
addRegiment(side, regiment) {
|
||||||
regiment.casualties = Object.keys(regiment.u).reduce((a,b) => (a[b]=0,a), {});
|
regiment.casualties = Object.keys(regiment.u).reduce((a, b) => ((a[b] = 0), a), {});
|
||||||
regiment.survivors = Object.assign({}, regiment.u);
|
regiment.survivors = Object.assign({}, regiment.u);
|
||||||
|
|
||||||
const state = pack.states[regiment.state];
|
const state = pack.states[regiment.state];
|
||||||
const distance = Math.hypot(this.y-regiment.by, this.x-regiment.bx) * distanceScaleInput.value | 0; // distance between regiment and its base
|
const distance = (Math.hypot(this.y - regiment.by, this.x - regiment.bx) * distanceScaleInput.value) | 0; // distance between regiment and its base
|
||||||
const color = state.color[0] === "#" ? state.color : "#999";
|
const color = state.color[0] === "#" ? state.color : "#999";
|
||||||
const icon = `<svg width="1.4em" height="1.4em" style="margin-bottom: -.6em">
|
const icon = `<svg width="1.4em" height="1.4em" style="margin-bottom: -.6em">
|
||||||
<rect x="0" y="0" width="100%" height="100%" fill="${color}" class="fillRect"></rect>
|
<rect x="0" y="0" width="100%" height="100%" fill="${color}" class="fillRect"></rect>
|
||||||
|
|
@ -164,12 +169,18 @@ class Battle {
|
||||||
addSide() {
|
addSide() {
|
||||||
const body = document.getElementById("regimentSelectorBody");
|
const body = document.getElementById("regimentSelectorBody");
|
||||||
const context = Battle.prototype.context;
|
const context = Battle.prototype.context;
|
||||||
const regiments = pack.states.filter(s => s.military && !s.removed).map(s => s.military).flat();
|
const regiments = pack.states
|
||||||
|
.filter(s => s.military && !s.removed)
|
||||||
|
.map(s => s.military)
|
||||||
|
.flat();
|
||||||
const distance = reg => rn(Math.hypot(context.y - reg.y, context.x - reg.x) * distanceScaleInput.value) + " " + distanceUnitInput.value;
|
const distance = reg => rn(Math.hypot(context.y - reg.y, context.x - reg.x) * distanceScaleInput.value) + " " + distanceUnitInput.value;
|
||||||
const isAdded = reg => context.defenders.regiments.some(r => r === reg) || context.attackers.regiments.some(r => r === reg);
|
const isAdded = reg => context.defenders.regiments.some(r => r === reg) || context.attackers.regiments.some(r => r === reg);
|
||||||
|
|
||||||
body.innerHTML = regiments.map(r => {
|
body.innerHTML = regiments
|
||||||
const s = pack.states[r.state], added = isAdded(r), dist = added ? "0 " + distanceUnitInput.value : distance(r);
|
.map(r => {
|
||||||
|
const s = pack.states[r.state],
|
||||||
|
added = isAdded(r),
|
||||||
|
dist = added ? "0 " + distanceUnitInput.value : distance(r);
|
||||||
return `<div ${added ? "class='inactive'" : ""} data-s=${s.i} data-i=${r.i} data-state=${s.name} data-regiment=${r.name}
|
return `<div ${added ? "class='inactive'" : ""} data-s=${s.i} data-i=${r.i} data-state=${s.name} data-regiment=${r.name}
|
||||||
data-total=${r.a} data-distance=${dist} data-tip="Click to select regiment">
|
data-total=${r.a} data-distance=${dist} data-tip="Click to select regiment">
|
||||||
<svg width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${s.color}" class="fillRect"></svg>
|
<svg width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${s.color}" class="fillRect"></svg>
|
||||||
|
|
@ -179,11 +190,15 @@ class Battle {
|
||||||
<div style="width:4em">${r.a}</div>
|
<div style="width:4em">${r.a}</div>
|
||||||
<div style="width:4em">${dist}</div>
|
<div style="width:4em">${dist}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join("");
|
})
|
||||||
|
.join("");
|
||||||
|
|
||||||
$("#regimentSelectorScreen").dialog({
|
$("#regimentSelectorScreen").dialog({
|
||||||
resizable: false, width: fitContent(), title: "Add regiment to the battle",
|
resizable: false,
|
||||||
position: {my: "left center", at: "right+10 center", of: "#battleScreen"}, close: addSideClosed,
|
width: fitContent(),
|
||||||
|
title: "Add regiment to the battle",
|
||||||
|
position: {my: "left center", at: "right+10 center", of: "#battleScreen"},
|
||||||
|
close: addSideClosed,
|
||||||
buttons: {
|
buttons: {
|
||||||
"Add to attackers": () => addSideClicked("attackers"),
|
"Add to attackers": () => addSideClicked("attackers"),
|
||||||
"Add to defenders": () => addSideClicked("defenders"),
|
"Add to defenders": () => addSideClicked("defenders"),
|
||||||
|
|
@ -195,13 +210,19 @@ class Battle {
|
||||||
body.addEventListener("click", selectLine);
|
body.addEventListener("click", selectLine);
|
||||||
|
|
||||||
function selectLine(ev) {
|
function selectLine(ev) {
|
||||||
if (ev.target.className === "inactive") {tip("Regiment is already in the battle", false, "error"); return};
|
if (ev.target.className === "inactive") {
|
||||||
|
tip("Regiment is already in the battle", false, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
ev.target.classList.toggle("selected");
|
ev.target.classList.toggle("selected");
|
||||||
}
|
}
|
||||||
|
|
||||||
function addSideClicked(side) {
|
function addSideClicked(side) {
|
||||||
const selected = body.querySelectorAll(".selected");
|
const selected = body.querySelectorAll(".selected");
|
||||||
if (!selected.length) {tip("Please select a regiment first", false, "error"); return}
|
if (!selected.length) {
|
||||||
|
tip("Please select a regiment first", false, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$("#regimentSelectorScreen").dialog("close");
|
$("#regimentSelectorScreen").dialog("close");
|
||||||
selected.forEach(line => {
|
selected.forEach(line => {
|
||||||
|
|
@ -212,7 +233,8 @@ class Battle {
|
||||||
Battle.prototype.getInitialMorale.call(context);
|
Battle.prototype.getInitialMorale.call(context);
|
||||||
|
|
||||||
// move regiment
|
// move regiment
|
||||||
const defenders = context.defenders.regiments, attackers = context.attackers.regiments;
|
const defenders = context.defenders.regiments,
|
||||||
|
attackers = context.attackers.regiments;
|
||||||
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;
|
||||||
|
|
@ -227,7 +249,7 @@ class Battle {
|
||||||
}
|
}
|
||||||
|
|
||||||
showNameSection() {
|
showNameSection() {
|
||||||
document.querySelectorAll("#battleBottom > button").forEach(el => el.style.display = "none");
|
document.querySelectorAll("#battleBottom > button").forEach(el => (el.style.display = "none"));
|
||||||
document.getElementById("battleNameSection").style.display = "inline-block";
|
document.getElementById("battleNameSection").style.display = "inline-block";
|
||||||
|
|
||||||
document.getElementById("battleNamePlace").value = this.place;
|
document.getElementById("battleNamePlace").value = this.place;
|
||||||
|
|
@ -235,22 +257,20 @@ class Battle {
|
||||||
}
|
}
|
||||||
|
|
||||||
hideNameSection() {
|
hideNameSection() {
|
||||||
document.querySelectorAll("#battleBottom > button").forEach(el => el.style.display = "inline-block");
|
document.querySelectorAll("#battleBottom > button").forEach(el => (el.style.display = "inline-block"));
|
||||||
document.getElementById("battleNameSection").style.display = "none";
|
document.getElementById("battleNameSection").style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
changeName(ev) {
|
changeName(ev) {
|
||||||
this.name = ev.target.value;
|
this.name = ev.target.value;
|
||||||
$("#battleScreen").dialog({"title":this.name});
|
$("#battleScreen").dialog({title: this.name});
|
||||||
}
|
}
|
||||||
|
|
||||||
generateName(type) {
|
generateName(type) {
|
||||||
const place = type === "culture"
|
const place = type === "culture" ? Names.getCulture(pack.cells.culture[this.cell], null, null, "") : Names.getBase(rand(nameBases.length - 1));
|
||||||
? Names.getCulture(pack.cells.culture[this.cell], null, null, "")
|
|
||||||
: Names.getBase(rand(nameBases.length-1));
|
|
||||||
document.getElementById("battleNamePlace").value = this.place = place;
|
document.getElementById("battleNamePlace").value = this.place = place;
|
||||||
document.getElementById("battleNameFull").value = this.name = this.defineName();
|
document.getElementById("battleNameFull").value = this.name = this.defineName();
|
||||||
$("#battleScreen").dialog({"title":this.name});
|
$("#battleScreen").dialog({title: this.name});
|
||||||
}
|
}
|
||||||
|
|
||||||
getJoinedForces(regiments) {
|
getJoinedForces(regiments) {
|
||||||
|
|
@ -266,44 +286,44 @@ class Battle {
|
||||||
calculateStrength(side) {
|
calculateStrength(side) {
|
||||||
const scheme = {
|
const scheme = {
|
||||||
// field battle phases
|
// field battle phases
|
||||||
"skirmish": {"melee":.2, "ranged":2.4, "mounted":.1, "machinery":3, "naval":1, "armored":.2, "aviation":1.8, "magical":1.8}, // ranged excel
|
skirmish: {melee: 0.2, ranged: 2.4, mounted: 0.1, machinery: 3, naval: 1, armored: 0.2, aviation: 1.8, magical: 1.8}, // ranged excel
|
||||||
"melee": {"melee":2, "ranged":1.2, "mounted":1.5, "machinery":.5, "naval":.2, "armored":2, "aviation":.8, "magical":.8}, // melee excel
|
melee: {melee: 2, ranged: 1.2, mounted: 1.5, machinery: 0.5, naval: 0.2, armored: 2, aviation: 0.8, magical: 0.8}, // melee excel
|
||||||
"pursue": {"melee":1, "ranged":1, "mounted":4, "machinery":.05, "naval":1, "armored":1, "aviation":1.5, "magical":.6}, // mounted excel
|
pursue: {melee: 1, ranged: 1, mounted: 4, machinery: 0.05, naval: 1, armored: 1, aviation: 1.5, magical: 0.6}, // mounted excel
|
||||||
"retreat": {"melee":.1, "ranged":.01, "mounted":.5, "machinery":.01, "naval":.2, "armored":.1, "aviation":.8, "magical":.05}, // reduced
|
retreat: {melee: 0.1, ranged: 0.01, mounted: 0.5, machinery: 0.01, naval: 0.2, armored: 0.1, aviation: 0.8, magical: 0.05}, // reduced
|
||||||
|
|
||||||
// naval battle phases
|
// naval battle phases
|
||||||
"shelling": {"melee":0, "ranged":.2, "mounted":0, "machinery":2, "naval":2, "armored":0, "aviation":.1, "magical":.5}, // naval and machinery excel
|
shelling: {melee: 0, ranged: 0.2, mounted: 0, machinery: 2, naval: 2, armored: 0, aviation: 0.1, magical: 0.5}, // naval and machinery excel
|
||||||
"boarding": {"melee":1, "ranged":.5, "mounted":.5, "machinery":0, "naval":.5, "armored":.4, "aviation":0, "magical":.2}, // melee excel
|
boarding: {melee: 1, ranged: 0.5, mounted: 0.5, machinery: 0, naval: 0.5, armored: 0.4, aviation: 0, magical: 0.2}, // melee excel
|
||||||
"chase": {"melee":0, "ranged":.15, "mounted":0, "machinery":1, "naval":1, "armored":0, "aviation":.15, "magical":.5}, // reduced
|
chase: {melee: 0, ranged: 0.15, mounted: 0, machinery: 1, naval: 1, armored: 0, aviation: 0.15, magical: 0.5}, // reduced
|
||||||
"withdrawal": {"melee":0, "ranged":.02, "mounted":0, "machinery":.5, "naval":.1, "armored":0, "aviation":.1, "magical":.3}, // reduced
|
withdrawal: {melee: 0, ranged: 0.02, mounted: 0, machinery: 0.5, naval: 0.1, armored: 0, aviation: 0.1, magical: 0.3}, // reduced
|
||||||
|
|
||||||
// siege phases
|
// siege phases
|
||||||
"blockade": {"melee":.25, "ranged":.25, "mounted":.2, "machinery":.5, "naval":.2, "armored":.1, "aviation":.25, "magical":.25}, // no active actions
|
blockade: {melee: 0.25, ranged: 0.25, mounted: 0.2, machinery: 0.5, naval: 0.2, armored: 0.1, aviation: 0.25, magical: 0.25}, // no active actions
|
||||||
"sheltering": {"melee":.3, "ranged":.5, "mounted":.2, "machinery":.5, "naval":.2, "armored":.1, "aviation":.25, "magical":.25}, // no active actions
|
sheltering: {melee: 0.3, ranged: 0.5, mounted: 0.2, machinery: 0.5, naval: 0.2, armored: 0.1, aviation: 0.25, magical: 0.25}, // no active actions
|
||||||
"sortie": {"melee":2, "ranged":.5, "mounted":1.2, "machinery":.2, "naval":.1, "armored":.5, "aviation":1, "magical":1}, // melee excel
|
sortie: {melee: 2, ranged: 0.5, mounted: 1.2, machinery: 0.2, naval: 0.1, armored: 0.5, aviation: 1, magical: 1}, // melee excel
|
||||||
"bombardment": {"melee":.2, "ranged":.5, "mounted":.2, "machinery":3, "naval":1, "armored":.5, "aviation":1, "magical":1}, // machinery excel
|
bombardment: {melee: 0.2, ranged: 0.5, mounted: 0.2, machinery: 3, naval: 1, armored: 0.5, aviation: 1, magical: 1}, // machinery excel
|
||||||
"storming": {"melee":1, "ranged":.6, "mounted":.5, "machinery":1, "naval":.1, "armored":.1, "aviation":.5, "magical":.5}, // melee excel
|
storming: {melee: 1, ranged: 0.6, mounted: 0.5, machinery: 1, naval: 0.1, armored: 0.1, aviation: 0.5, magical: 0.5}, // melee excel
|
||||||
"defense": {"melee":2, "ranged":3, "mounted":1, "machinery":1, "naval":.1, "armored":1, "aviation":.5, "magical":1}, // ranged excel
|
defense: {melee: 2, ranged: 3, mounted: 1, machinery: 1, naval: 0.1, armored: 1, aviation: 0.5, magical: 1}, // ranged excel
|
||||||
"looting": {"melee":1.6, "ranged":1.6, "mounted":.5, "machinery":.2, "naval":.02, "armored":.2, "aviation":.1, "magical":.3}, // melee excel
|
looting: {melee: 1.6, ranged: 1.6, mounted: 0.5, machinery: 0.2, naval: 0.02, armored: 0.2, aviation: 0.1, magical: 0.3}, // melee excel
|
||||||
"surrendering": {"melee":.1, "ranged":.1, "mounted":.05, "machinery":.01, "naval":.01, "armored":.02, "aviation":.01, "magical":.03}, // reduced
|
surrendering: {melee: 0.1, ranged: 0.1, mounted: 0.05, machinery: 0.01, naval: 0.01, armored: 0.02, aviation: 0.01, magical: 0.03}, // reduced
|
||||||
|
|
||||||
// ambush phases
|
// ambush phases
|
||||||
"surprise": {"melee":2, "ranged":2.4, "mounted":1, "machinery":1, "naval":1, "armored":1, "aviation":.8, "magical":1.2}, // increased
|
surprise: {melee: 2, ranged: 2.4, mounted: 1, machinery: 1, naval: 1, armored: 1, aviation: 0.8, magical: 1.2}, // increased
|
||||||
"shock": {"melee":.5, "ranged":.5, "mounted":.5, "machinery":.4, "naval":.3, "armored":.1, "aviation":.4, "magical":.5}, // reduced
|
shock: {melee: 0.5, ranged: 0.5, mounted: 0.5, machinery: 0.4, naval: 0.3, armored: 0.1, aviation: 0.4, magical: 0.5}, // reduced
|
||||||
|
|
||||||
// langing phases
|
// langing phases
|
||||||
"landing": {"melee":.8, "ranged":.6, "mounted":.6, "machinery":.5, "naval":.5, "armored":.5, "aviation":.5, "magical":.6}, // reduced
|
landing: {melee: 0.8, ranged: 0.6, mounted: 0.6, machinery: 0.5, naval: 0.5, armored: 0.5, aviation: 0.5, magical: 0.6}, // reduced
|
||||||
"flee": {"melee":.1, "ranged":.01, "mounted":.5, "machinery":.01, "naval":.5, "armored":.1, "aviation":.2, "magical":.05}, // reduced
|
flee: {melee: 0.1, ranged: 0.01, mounted: 0.5, machinery: 0.01, naval: 0.5, armored: 0.1, aviation: 0.2, magical: 0.05}, // reduced
|
||||||
"waiting": {"melee":.05, "ranged":.5, "mounted":.05, "machinery":.5, "naval":2, "armored":.05, "aviation":.5, "magical":.5}, // reduced
|
waiting: {melee: 0.05, ranged: 0.5, mounted: 0.05, machinery: 0.5, naval: 2, armored: 0.05, aviation: 0.5, magical: 0.5}, // reduced
|
||||||
|
|
||||||
// air battle phases
|
// air battle phases
|
||||||
"maneuvering": {"melee":0, "ranged":.1, "mounted":0, "machinery":.2, "naval":0, "armored":0, "aviation":1, "magical":.2}, // aviation
|
maneuvering: {melee: 0, ranged: 0.1, mounted: 0, machinery: 0.2, naval: 0, armored: 0, aviation: 1, magical: 0.2}, // aviation
|
||||||
"dogfight": {"melee":0, "ranged":.1, "mounted":0, "machinery":.1, "naval":0, "armored":0, "aviation":2, "magical":.1} // aviation
|
dogfight: {melee: 0, ranged: 0.1, mounted: 0, machinery: 0.1, naval: 0, armored: 0, aviation: 2, magical: 0.1} // aviation
|
||||||
};
|
};
|
||||||
|
|
||||||
const forces = this.getJoinedForces(this[side].regiments);
|
const forces = this.getJoinedForces(this[side].regiments);
|
||||||
const phase = this[side].phase;
|
const phase = this[side].phase;
|
||||||
const adjuster = Math.max(populationRate.value / 10, 10); // population adjuster, by default 100
|
const adjuster = Math.max(populationRate / 10, 10); // population adjuster, by default 100
|
||||||
this[side].power = d3.sum(options.military.map(u => (forces[u.name] || 0) * u.power * scheme[phase][u.type])) / adjuster;
|
this[side].power = d3.sum(options.military.map(u => (forces[u.name] || 0) * u.power * scheme[phase][u.type])) / adjuster;
|
||||||
const UIvalue = this[side].power ? Math.max(this[side].power | 0, 1) : 0;
|
const UIvalue = this[side].power ? Math.max(this[side].power | 0, 1) : 0;
|
||||||
document.getElementById("battlePower_" + side).innerHTML = UIvalue;
|
document.getElementById("battlePower_" + side).innerHTML = UIvalue;
|
||||||
|
|
@ -337,7 +357,9 @@ class Battle {
|
||||||
rollDie(side) {
|
rollDie(side) {
|
||||||
const el = document.getElementById("battleDie_" + side);
|
const el = document.getElementById("battleDie_" + side);
|
||||||
const prev = +el.innerHTML;
|
const prev = +el.innerHTML;
|
||||||
do {el.innerHTML = rand(1, 6)} while (el.innerHTML == prev)
|
do {
|
||||||
|
el.innerHTML = rand(1, 6);
|
||||||
|
} while (el.innerHTML == prev);
|
||||||
this[side].die = +el.innerHTML;
|
this[side].die = +el.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -357,12 +379,18 @@ class Battle {
|
||||||
if (prev[0] === "skirmish" && prev[1] === "skirmish") {
|
if (prev[0] === "skirmish" && prev[1] === "skirmish") {
|
||||||
const forces = this.getJoinedForces(this.attackers.regiments.concat(this.defenders.regiments));
|
const forces = this.getJoinedForces(this.attackers.regiments.concat(this.defenders.regiments));
|
||||||
const total = d3.sum(Object.values(forces)); // total forces
|
const total = d3.sum(Object.values(forces)); // total forces
|
||||||
const ranged = d3.sum(options.military.filter(u => u.type === "ranged").map(u => u.name).map(u => forces[u])) / total; // ranged units
|
const ranged =
|
||||||
if (P(ranged) || P(.8-i/10)) return ["skirmish", "skirmish"];
|
d3.sum(
|
||||||
|
options.military
|
||||||
|
.filter(u => u.type === "ranged")
|
||||||
|
.map(u => u.name)
|
||||||
|
.map(u => forces[u])
|
||||||
|
) / total; // ranged units
|
||||||
|
if (P(ranged) || P(0.8 - i / 10)) return ["skirmish", "skirmish"];
|
||||||
}
|
}
|
||||||
|
|
||||||
return ["melee", "melee"]; // default option
|
return ["melee", "melee"]; // default option
|
||||||
}
|
};
|
||||||
|
|
||||||
const getNavalBattlePhase = () => {
|
const getNavalBattlePhase = () => {
|
||||||
const prev = [this.attackers.phase || "shelling", this.defenders.phase || "shelling"]; // previous phase
|
const prev = [this.attackers.phase || "shelling", this.defenders.phase || "shelling"]; // previous phase
|
||||||
|
|
@ -372,19 +400,19 @@ class Battle {
|
||||||
|
|
||||||
// withdrawal phase when power imbalanced
|
// withdrawal phase when power imbalanced
|
||||||
if (!prev[0] === "boarding") {
|
if (!prev[0] === "boarding") {
|
||||||
if (powerRatio < .5 || P(this.attackers.casualties) && powerRatio < 1) return ["withdrawal", "chase"];
|
if (powerRatio < 0.5 || (P(this.attackers.casualties) && powerRatio < 1)) return ["withdrawal", "chase"];
|
||||||
if (powerRatio > 2 || P(this.defenders.casualties) && powerRatio > 1) return ["chase", "withdrawal"];
|
if (powerRatio > 2 || (P(this.defenders.casualties) && powerRatio > 1)) return ["chase", "withdrawal"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// boarding phase can start from 2nd iteration
|
// boarding phase can start from 2nd iteration
|
||||||
if (prev[0] === "boarding" || P(i/10 - .1)) return ["boarding", "boarding"];
|
if (prev[0] === "boarding" || P(i / 10 - 0.1)) return ["boarding", "boarding"];
|
||||||
|
|
||||||
return ["shelling", "shelling"]; // default option
|
return ["shelling", "shelling"]; // default option
|
||||||
}
|
};
|
||||||
|
|
||||||
const getSiegePhase = () => {
|
const getSiegePhase = () => {
|
||||||
const prev = [this.attackers.phase || "blockade", this.defenders.phase || "sheltering"]; // previous phase
|
const prev = [this.attackers.phase || "blockade", this.defenders.phase || "sheltering"]; // previous phase
|
||||||
let phase = ["blockade", "sheltering"] // default phase
|
let phase = ["blockade", "sheltering"]; // default phase
|
||||||
|
|
||||||
if (prev[0] === "retreat" || prev[0] === "looting") return prev;
|
if (prev[0] === "retreat" || prev[0] === "looting") return prev;
|
||||||
|
|
||||||
|
|
@ -398,40 +426,40 @@ class Battle {
|
||||||
|
|
||||||
const attackers = this.getJoinedForces(this.attackers.regiments);
|
const attackers = this.getJoinedForces(this.attackers.regiments);
|
||||||
const machineryA = d3.sum(machinery.map(u => attackers[u]));
|
const machineryA = d3.sum(machinery.map(u => attackers[u]));
|
||||||
if (i && machineryA && P(.9)) phase[0] = "bombardment";
|
if (i && machineryA && P(0.9)) phase[0] = "bombardment";
|
||||||
|
|
||||||
const defenders = this.getJoinedForces(this.defenders.regiments);
|
const defenders = this.getJoinedForces(this.defenders.regiments);
|
||||||
const machineryD = d3.sum(machinery.map(u => defenders[u]));
|
const machineryD = d3.sum(machinery.map(u => defenders[u]));
|
||||||
if (machineryD && P(.9)) phase[1] = "bombardment";
|
if (machineryD && P(0.9)) phase[1] = "bombardment";
|
||||||
|
|
||||||
if (i && prev[1] !== "sortie" && machineryD < machineryA && P(.25) && P(morale[1]/70)) phase[1] = "sortie"; // defenders sortie
|
if (i && prev[1] !== "sortie" && machineryD < machineryA && P(0.25) && P(morale[1] / 70)) phase[1] = "sortie"; // defenders sortie
|
||||||
}
|
}
|
||||||
|
|
||||||
return phase;
|
return phase;
|
||||||
}
|
};
|
||||||
|
|
||||||
const getAmbushPhase = () => {
|
const getAmbushPhase = () => {
|
||||||
const prev = [this.attackers.phase || "shock", this.defenders.phase || "surprise"]; // previous phase
|
const prev = [this.attackers.phase || "shock", this.defenders.phase || "surprise"]; // previous phase
|
||||||
|
|
||||||
if (prev[1] === "surprise" && P(1-powerRatio*i/5)) return ["shock", "surprise"];
|
if (prev[1] === "surprise" && P(1 - (powerRatio * i) / 5)) return ["shock", "surprise"];
|
||||||
|
|
||||||
// chance if moral < 25
|
// chance if moral < 25
|
||||||
if (P(1 - morale[0] / 25)) return ["retreat", "pursue"];
|
if (P(1 - morale[0] / 25)) return ["retreat", "pursue"];
|
||||||
if (P(1 - morale[1] / 25)) return ["pursue", "retreat"];
|
if (P(1 - morale[1] / 25)) return ["pursue", "retreat"];
|
||||||
|
|
||||||
return ["melee", "melee"]; // default option
|
return ["melee", "melee"]; // default option
|
||||||
}
|
};
|
||||||
|
|
||||||
const getLandingPhase = () => {
|
const getLandingPhase = () => {
|
||||||
const prev = [this.attackers.phase || "landing", this.defenders.phase || "defense"]; // previous phase
|
const prev = [this.attackers.phase || "landing", this.defenders.phase || "defense"]; // previous phase
|
||||||
|
|
||||||
if (prev[1] === "waiting") return ["flee", "waiting"];
|
if (prev[1] === "waiting") return ["flee", "waiting"];
|
||||||
if (prev[1] === "pursue") return ["flee", P(.3) ? "pursue" : "waiting"];
|
if (prev[1] === "pursue") return ["flee", P(0.3) ? "pursue" : "waiting"];
|
||||||
if (prev[1] === "retreat") return ["pursue", "retreat"];
|
if (prev[1] === "retreat") return ["pursue", "retreat"];
|
||||||
|
|
||||||
if (prev[0] === "landing") {
|
if (prev[0] === "landing") {
|
||||||
const attackers = P(i / 2) ? "melee" : "landing";
|
const attackers = P(i / 2) ? "melee" : "landing";
|
||||||
const defenders = i ? prev[1] : P(.5) ? "defense" : "shock";
|
const defenders = i ? prev[1] : P(0.5) ? "defense" : "shock";
|
||||||
return [attackers, defenders];
|
return [attackers, defenders];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -439,7 +467,7 @@ class Battle {
|
||||||
if (P(1 - morale[1] / 25)) return ["pursue", "retreat"]; // chance if moral < 25
|
if (P(1 - morale[1] / 25)) return ["pursue", "retreat"]; // chance if moral < 25
|
||||||
|
|
||||||
return ["melee", "melee"]; // default option
|
return ["melee", "melee"]; // default option
|
||||||
}
|
};
|
||||||
|
|
||||||
const getAirBattlePhase = () => {
|
const getAirBattlePhase = () => {
|
||||||
const prev = [this.attackers.phase || "maneuvering", this.defenders.phase || "maneuvering"]; // previous phase
|
const prev = [this.attackers.phase || "maneuvering", this.defenders.phase || "maneuvering"]; // previous phase
|
||||||
|
|
@ -451,19 +479,26 @@ class Battle {
|
||||||
if (prev[0] === "maneuvering" && P(1 - i / 10)) return ["maneuvering", "maneuvering"];
|
if (prev[0] === "maneuvering" && P(1 - i / 10)) return ["maneuvering", "maneuvering"];
|
||||||
|
|
||||||
return ["dogfight", "dogfight"]; // default option
|
return ["dogfight", "dogfight"]; // default option
|
||||||
}
|
};
|
||||||
|
|
||||||
const phase = function(type) {
|
const phase = (function (type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "field": return getFieldBattlePhase();
|
case "field":
|
||||||
case "naval": return getNavalBattlePhase();
|
return getFieldBattlePhase();
|
||||||
case "siege": return getSiegePhase();
|
case "naval":
|
||||||
case "ambush": return getAmbushPhase();
|
return getNavalBattlePhase();
|
||||||
case "landing": return getLandingPhase();
|
case "siege":
|
||||||
case "air": return getAirBattlePhase();
|
return getSiegePhase();
|
||||||
default: getFieldBattlePhase();
|
case "ambush":
|
||||||
|
return getAmbushPhase();
|
||||||
|
case "landing":
|
||||||
|
return getLandingPhase();
|
||||||
|
case "air":
|
||||||
|
return getAirBattlePhase();
|
||||||
|
default:
|
||||||
|
getFieldBattlePhase();
|
||||||
}
|
}
|
||||||
}(this.type);
|
})(this.type);
|
||||||
|
|
||||||
this.attackers.phase = phase[0];
|
this.attackers.phase = phase[0];
|
||||||
this.defenders.phase = phase[1];
|
this.defenders.phase = phase[1];
|
||||||
|
|
@ -479,22 +514,49 @@ class Battle {
|
||||||
|
|
||||||
run() {
|
run() {
|
||||||
// validations
|
// validations
|
||||||
if (!this.attackers.power) {tip("Attackers army destroyed", false, "warn"); return}
|
if (!this.attackers.power) {
|
||||||
if (!this.defenders.power) {tip("Defenders army destroyed", false, "warn"); return}
|
tip("Attackers army destroyed", false, "warn");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.defenders.power) {
|
||||||
|
tip("Defenders army destroyed", false, "warn");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// calculate casualties
|
// calculate casualties
|
||||||
const attack = this.attackers.power * (this.attackers.die / 10 + .4);
|
const attack = this.attackers.power * (this.attackers.die / 10 + 0.4);
|
||||||
const defense = this.defenders.power * (this.defenders.die / 10 + .4);
|
const defense = this.defenders.power * (this.defenders.die / 10 + 0.4);
|
||||||
|
|
||||||
// casualties modifier for phase
|
// casualties modifier for phase
|
||||||
const phase = {
|
const phase = {
|
||||||
"skirmish":.1, "melee":.2, "pursue":.3, "retreat":.3, "boarding":.2, "shelling":.1, "chase":.03, "withdrawal": .03,
|
skirmish: 0.1,
|
||||||
"blockade":0, "sheltering":0, "sortie":.1, "bombardment":.05, "storming":.2, "defense":.2, "looting":.5, "surrendering":.5,
|
melee: 0.2,
|
||||||
"surprise":.3, "shock":.3, "landing":.3, "flee":0, "waiting":0, "maneuvering":.1, "dogfight":.2};
|
pursue: 0.3,
|
||||||
|
retreat: 0.3,
|
||||||
|
boarding: 0.2,
|
||||||
|
shelling: 0.1,
|
||||||
|
chase: 0.03,
|
||||||
|
withdrawal: 0.03,
|
||||||
|
blockade: 0,
|
||||||
|
sheltering: 0,
|
||||||
|
sortie: 0.1,
|
||||||
|
bombardment: 0.05,
|
||||||
|
storming: 0.2,
|
||||||
|
defense: 0.2,
|
||||||
|
looting: 0.5,
|
||||||
|
surrendering: 0.5,
|
||||||
|
surprise: 0.3,
|
||||||
|
shock: 0.3,
|
||||||
|
landing: 0.3,
|
||||||
|
flee: 0,
|
||||||
|
waiting: 0,
|
||||||
|
maneuvering: 0.1,
|
||||||
|
dogfight: 0.2
|
||||||
|
};
|
||||||
|
|
||||||
const casualties = Math.random() * (Math.max(phase[this.attackers.phase], phase[this.defenders.phase])); // total casualties, ~10% per iteration
|
const casualties = Math.random() * Math.max(phase[this.attackers.phase], phase[this.defenders.phase]); // total casualties, ~10% per iteration
|
||||||
const casualtiesA = casualties * defense / (attack + defense); // attackers casualties, ~5% per iteration
|
const casualtiesA = (casualties * defense) / (attack + defense); // attackers casualties, ~5% per iteration
|
||||||
const casualtiesD = casualties * attack / (attack + defense); // defenders casualties, ~5% per iteration
|
const casualtiesD = (casualties * attack) / (attack + defense); // defenders casualties, ~5% per iteration
|
||||||
|
|
||||||
this.calculateCasualties("attackers", casualtiesA);
|
this.calculateCasualties("attackers", casualtiesA);
|
||||||
this.calculateCasualties("defenders", casualtiesD);
|
this.calculateCasualties("defenders", casualtiesD);
|
||||||
|
|
@ -519,7 +581,7 @@ class Battle {
|
||||||
calculateCasualties(side, casualties) {
|
calculateCasualties(side, casualties) {
|
||||||
for (const r of this[side].regiments) {
|
for (const r of this[side].regiments) {
|
||||||
for (const unit in r.u) {
|
for (const unit in r.u) {
|
||||||
const rand = .8 + Math.random() * .4;
|
const rand = 0.8 + Math.random() * 0.4;
|
||||||
const died = Math.min(Pint(r.u[unit] * casualties * rand), r.survivors[unit]);
|
const died = Math.min(Pint(r.u[unit] * casualties * rand), r.survivors[unit]);
|
||||||
r.casualties[unit] -= died;
|
r.casualties[unit] -= died;
|
||||||
r.survivors[unit] -= died;
|
r.survivors[unit] -= died;
|
||||||
|
|
@ -551,10 +613,16 @@ class Battle {
|
||||||
const button = ev.target;
|
const button = ev.target;
|
||||||
const div = button.nextElementSibling;
|
const div = button.nextElementSibling;
|
||||||
|
|
||||||
const hideSection = function() {button.style.opacity = 1; div.style.display = "none"}
|
const hideSection = function () {
|
||||||
if (div.style.display === "block") {hideSection(); return}
|
button.style.opacity = 1;
|
||||||
|
div.style.display = "none";
|
||||||
|
};
|
||||||
|
if (div.style.display === "block") {
|
||||||
|
hideSection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
button.style.opacity = .5;
|
button.style.opacity = 0.5;
|
||||||
div.style.display = "block";
|
div.style.display = "block";
|
||||||
|
|
||||||
document.getElementsByTagName("body")[0].addEventListener("click", hideSection, {once: true});
|
document.getElementsByTagName("body")[0].addEventListener("click", hideSection, {once: true});
|
||||||
|
|
@ -568,12 +636,12 @@ class Battle {
|
||||||
this.calculateStrength("attackers");
|
this.calculateStrength("attackers");
|
||||||
this.calculateStrength("defenders");
|
this.calculateStrength("defenders");
|
||||||
this.name = this.defineName();
|
this.name = this.defineName();
|
||||||
$("#battleScreen").dialog({"title":this.name});
|
$("#battleScreen").dialog({title: this.name});
|
||||||
}
|
}
|
||||||
|
|
||||||
changePhase(ev, side) {
|
changePhase(ev, side) {
|
||||||
if (ev.target.tagName !== "BUTTON") return;
|
if (ev.target.tagName !== "BUTTON") return;
|
||||||
const phase = this[side].phase = ev.target.dataset.phase;
|
const phase = (this[side].phase = ev.target.dataset.phase);
|
||||||
const button = document.getElementById("battlePhase_" + side);
|
const button = document.getElementById("battlePhase_" + side);
|
||||||
button.className = "icon-button-" + phase;
|
button.className = "icon-button-" + phase;
|
||||||
button.dataset.tip = ev.target.dataset.tip;
|
button.dataset.tip = ev.target.dataset.tip;
|
||||||
|
|
@ -587,12 +655,12 @@ class Battle {
|
||||||
const battleStatus = getBattleStatus(relativeCasualties, maxCasualties);
|
const battleStatus = getBattleStatus(relativeCasualties, maxCasualties);
|
||||||
function getBattleStatus(relative, max) {
|
function getBattleStatus(relative, max) {
|
||||||
if (isNaN(relative)) return ["standoff", "standoff"]; // if no casualties at all
|
if (isNaN(relative)) return ["standoff", "standoff"]; // if no casualties at all
|
||||||
if (max < .05) return ["minor skirmishes", "minor skirmishes"];
|
if (max < 0.05) return ["minor skirmishes", "minor skirmishes"];
|
||||||
if (relative > 95) return ["attackers flawless victory", "disorderly retreat of defenders"];
|
if (relative > 95) return ["attackers flawless victory", "disorderly retreat of defenders"];
|
||||||
if (relative > .7) return ["attackers decisive victory", "defenders disastrous defeat"];
|
if (relative > 0.7) return ["attackers decisive victory", "defenders disastrous defeat"];
|
||||||
if (relative > .6) return ["attackers victory", "defenders defeat"];
|
if (relative > 0.6) return ["attackers victory", "defenders defeat"];
|
||||||
if (relative > .4) return ["stalemate", "stalemate"];
|
if (relative > 0.4) return ["stalemate", "stalemate"];
|
||||||
if (relative > .3) return ["attackers defeat", "defenders victory"];
|
if (relative > 0.3) return ["attackers defeat", "defenders victory"];
|
||||||
if (relative > 0.5) return ["attackers disastrous defeat", "decisive victory of defenders"];
|
if (relative > 0.5) return ["attackers disastrous defeat", "decisive victory of defenders"];
|
||||||
if (relative >= 0) return ["attackers disorderly retreat", "flawless victory of defenders"];
|
if (relative >= 0) return ["attackers disorderly retreat", "flawless victory of defenders"];
|
||||||
return ["stalemate", "stalemate"]; // exception
|
return ["stalemate", "stalemate"]; // exception
|
||||||
|
|
@ -609,16 +677,10 @@ class Battle {
|
||||||
if (note) {
|
if (note) {
|
||||||
const status = side === "attackers" ? battleStatus[0] : battleStatus[1];
|
const status = side === "attackers" ? battleStatus[0] : battleStatus[1];
|
||||||
const losses = r.a ? Math.abs(d3.sum(Object.values(r.casualties))) / r.a : 1;
|
const losses = r.a ? Math.abs(d3.sum(Object.values(r.casualties))) / r.a : 1;
|
||||||
const regStatus =
|
const regStatus = losses === 1 ? "is destroyed" : losses > 0.8 ? "is almost completely destroyed" : losses > 0.5 ? "suffered terrible losses" : losses > 0.3 ? "suffered severe losses" : losses > 0.2 ? "suffered heavy losses" : losses > 0.05 ? "suffered significant losses" : losses > 0 ? "suffered unsignificant losses" : "left the battle without loss";
|
||||||
losses === 1 ? "is destroyed" :
|
const casualties = Object.keys(r.casualties)
|
||||||
losses > .8 ? "is almost completely destroyed" :
|
.map(t => (r.casualties[t] ? `${Math.abs(r.casualties[t])} ${t}` : null))
|
||||||
losses > .5 ? "suffered terrible losses" :
|
.filter(c => c);
|
||||||
losses > .3 ? "suffered severe losses" :
|
|
||||||
losses > .2 ? "suffered heavy losses" :
|
|
||||||
losses > .05 ? "suffered significant losses" :
|
|
||||||
losses > 0 ? "suffered unsignificant losses" :
|
|
||||||
"left the battle without loss";
|
|
||||||
const casualties = Object.keys(r.casualties).map(t => r.casualties[t] ? `${Math.abs(r.casualties[t])} ${t}` : null).filter(c => c);
|
|
||||||
const casualtiesText = casualties.length ? " Casualties: " + list(casualties) + "." : "";
|
const casualtiesText = casualties.length ? " Casualties: " + list(casualties) + "." : "";
|
||||||
const legend = `\r\n\r\n${battleName} (${options.year} ${options.eraShort}): ${status}. The regiment ${regStatus}.${casualtiesText}`;
|
const legend = `\r\n\r\n${battleName} (${options.year} ${options.eraShort}): ${status}. The regiment ${regStatus}.${casualtiesText}`;
|
||||||
note.legend += legend;
|
note.legend += legend;
|
||||||
|
|
@ -630,21 +692,18 @@ class Battle {
|
||||||
}
|
}
|
||||||
|
|
||||||
// append battlefield marker
|
// append battlefield marker
|
||||||
void function addMarkerSymbol() {
|
void (function addMarkerSymbol() {
|
||||||
if (svg.select("#defs-markers").select("#marker_battlefield").size()) return;
|
if (svg.select("#defs-markers").select("#marker_battlefield").size()) return;
|
||||||
const symbol = svg.select("#defs-markers").append("symbol").attr("id", "marker_battlefield").attr("viewBox", "0 0 30 30");
|
const symbol = svg.select("#defs-markers").append("symbol").attr("id", "marker_battlefield").attr("viewBox", "0 0 30 30");
|
||||||
symbol.append("path").attr("d", "M6,19 l9,10 L24,19").attr("fill", "#000000").attr("stroke", "none");
|
symbol.append("path").attr("d", "M6,19 l9,10 L24,19").attr("fill", "#000000").attr("stroke", "none");
|
||||||
symbol.append("circle").attr("cx", 15).attr("cy", 15).attr("r", 10).attr("fill", "#ffffff").attr("stroke", "#000000").attr("stroke-width", 1);
|
symbol.append("circle").attr("cx", 15).attr("cy", 15).attr("r", 10).attr("fill", "#ffffff").attr("stroke", "#000000").attr("stroke-width", 1);
|
||||||
symbol.append("text").attr("x", "50%").attr("y", "52%").attr("fill", "#000000").attr("stroke", "#3200ff").attr("stroke-width", 0)
|
symbol.append("text").attr("x", "50%").attr("y", "52%").attr("fill", "#000000").attr("stroke", "#3200ff").attr("stroke-width", 0).attr("font-size", "12px").attr("dominant-baseline", "central").text("⚔️");
|
||||||
.attr("font-size", "12px").attr("dominant-baseline", "central").text("⚔️");
|
})();
|
||||||
}()
|
|
||||||
|
|
||||||
const getSide = (regs, n) => regs.length > 1
|
const getSide = (regs, n) => (regs.length > 1 ? `${n ? "regiments" : "forces"} of ${list([...new Set(regs.map(r => pack.states[r.state].name))])}` : getAdjective(pack.states[regs[0].state].name) + " " + regs[0].name);
|
||||||
? `${n ? "regiments" : "forces"} of ${list([... new Set(regs.map(r => pack.states[r.state].name))])}`
|
|
||||||
: getAdjective(pack.states[regs[0].state].name) + " " + regs[0].name;
|
|
||||||
const getLosses = casualties => Math.min(rn(casualties * 100), 100);
|
const getLosses = casualties => Math.min(rn(casualties * 100), 100);
|
||||||
|
|
||||||
const status = battleStatus[+P(.7)];
|
const status = battleStatus[+P(0.7)];
|
||||||
const result = `The ${this.getTypeName(this.type)} ended in ${status}`;
|
const result = `The ${this.getTypeName(this.type)} ended in ${status}`;
|
||||||
const legend = `${this.name} took place in ${options.year} ${options.eraShort}. It was fought between ${getSide(this.attackers.regiments, 1)} and ${getSide(this.defenders.regiments, 0)}. ${result}.
|
const legend = `${this.name} took place in ${options.year} ${options.eraShort}. It was fought between ${getSide(this.attackers.regiments, 1)} and ${getSide(this.defenders.regiments, 0)}. ${result}.
|
||||||
\r\nAttackers losses: ${getLosses(this.attackers.casualties)}%, defenders losses: ${getLosses(this.defenders.casualties)}%`;
|
\r\nAttackers losses: ${getLosses(this.attackers.casualties)}%, defenders losses: ${getLosses(this.defenders.casualties)}%`;
|
||||||
|
|
@ -653,10 +712,18 @@ class Battle {
|
||||||
|
|
||||||
tip(`${this.name} is over. ${result}`, true, "success", 4000);
|
tip(`${this.name} is over. ${result}`, true, "success", 4000);
|
||||||
|
|
||||||
markers.append("use").attr("id", id)
|
markers
|
||||||
.attr("xlink:href", "#marker_battlefield").attr("data-id", "#marker_battlefield")
|
.append("use")
|
||||||
.attr("data-x", this.x).attr("data-y", this.y).attr("x", this.x - 15).attr("y", this.y - 30)
|
.attr("id", id)
|
||||||
.attr("data-size", 1).attr("width", 30).attr("height", 30);
|
.attr("xlink:href", "#marker_battlefield")
|
||||||
|
.attr("data-id", "#marker_battlefield")
|
||||||
|
.attr("data-x", this.x)
|
||||||
|
.attr("data-y", this.y)
|
||||||
|
.attr("x", this.x - 15)
|
||||||
|
.attr("y", this.y - 30)
|
||||||
|
.attr("data-size", 1)
|
||||||
|
.attr("width", 30)
|
||||||
|
.attr("height", 30);
|
||||||
|
|
||||||
$("#battleScreen").dialog("destroy");
|
$("#battleScreen").dialog("destroy");
|
||||||
this.cleanData();
|
this.cleanData();
|
||||||
|
|
@ -682,5 +749,4 @@ class Battle {
|
||||||
});
|
});
|
||||||
delete Battle.prototype.context;
|
delete Battle.prototype.context;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -16,7 +16,10 @@ function editBiomes() {
|
||||||
modules.editBiomes = true;
|
modules.editBiomes = true;
|
||||||
|
|
||||||
$("#biomesEditor").dialog({
|
$("#biomesEditor").dialog({
|
||||||
title: "Biomes Editor", resizable: false, width: fitContent(), close: closeBiomesEditor,
|
title: "Biomes Editor",
|
||||||
|
resizable: false,
|
||||||
|
width: fitContent(),
|
||||||
|
close: closeBiomesEditor,
|
||||||
position: {my: "right top", at: "right-10 top+10", of: "svg"}
|
position: {my: "right top", at: "right-10 top+10", of: "svg"}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -34,17 +37,19 @@ function editBiomes() {
|
||||||
document.getElementById("biomesExport").addEventListener("click", downloadBiomesData);
|
document.getElementById("biomesExport").addEventListener("click", downloadBiomesData);
|
||||||
|
|
||||||
body.addEventListener("click", function (ev) {
|
body.addEventListener("click", function (ev) {
|
||||||
const el = ev.target, cl = el.classList;
|
const el = ev.target,
|
||||||
if (cl.contains("fillRect")) biomeChangeColor(el); else
|
cl = el.classList;
|
||||||
if (cl.contains("icon-info-circled")) openWiki(el); else
|
if (cl.contains("fillRect")) biomeChangeColor(el);
|
||||||
if (cl.contains("icon-trash-empty")) removeCustomBiome(el);
|
else if (cl.contains("icon-info-circled")) openWiki(el);
|
||||||
|
else if (cl.contains("icon-trash-empty")) removeCustomBiome(el);
|
||||||
if (customization === 6) selectBiomeOnLineClick(el);
|
if (customization === 6) selectBiomeOnLineClick(el);
|
||||||
});
|
});
|
||||||
|
|
||||||
body.addEventListener("change", function (ev) {
|
body.addEventListener("change", function (ev) {
|
||||||
const el = ev.target, cl = el.classList;
|
const el = ev.target,
|
||||||
if (cl.contains("biomeName")) biomeChangeName(el); else
|
cl = el.classList;
|
||||||
if (cl.contains("biomeHabitability")) biomeChangeHabitability(el);
|
if (cl.contains("biomeName")) biomeChangeName(el);
|
||||||
|
else if (cl.contains("biomeHabitability")) biomeChangeHabitability(el);
|
||||||
});
|
});
|
||||||
|
|
||||||
function refreshBiomesEditor() {
|
function refreshBiomesEditor() {
|
||||||
|
|
@ -73,13 +78,15 @@ function editBiomes() {
|
||||||
function biomesEditorAddLines() {
|
function biomesEditorAddLines() {
|
||||||
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
|
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
|
||||||
const b = biomesData;
|
const b = biomesData;
|
||||||
let lines = "", totalArea = 0, totalPopulation = 0;;
|
let lines = "",
|
||||||
|
totalArea = 0,
|
||||||
|
totalPopulation = 0;
|
||||||
|
|
||||||
for (const i of b.i) {
|
for (const i of b.i) {
|
||||||
if (!i || biomesData.name[i] === "removed") continue; // ignore water and removed biomes
|
if (!i || biomesData.name[i] === "removed") continue; // ignore water and removed biomes
|
||||||
const area = b.area[i] * distanceScaleInput.value ** 2;
|
const area = b.area[i] * distanceScaleInput.value ** 2;
|
||||||
const rural = b.rural[i] * populationRate.value;
|
const rural = b.rural[i] * populationRate;
|
||||||
const urban = b.urban[i] * populationRate.value * urbanization.value;
|
const urban = b.urban[i] * populationRate * urbanization;
|
||||||
const population = rn(rural + urban);
|
const population = rn(rural + urban);
|
||||||
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
|
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
|
||||||
totalArea += area;
|
totalArea += area;
|
||||||
|
|
@ -98,7 +105,7 @@ function editBiomes() {
|
||||||
<span data-tip="${populationTip}" class="icon-male hide"></span>
|
<span data-tip="${populationTip}" class="icon-male hide"></span>
|
||||||
<div data-tip="${populationTip}" class="biomePopulation hide">${si(population)}</div>
|
<div data-tip="${populationTip}" class="biomePopulation hide">${si(population)}</div>
|
||||||
<span data-tip="Open Wikipedia article about the biome" class="icon-info-circled pointer hide"></span>
|
<span data-tip="Open Wikipedia article about the biome" class="icon-info-circled pointer hide"></span>
|
||||||
${i>12 && !b.cells[i] ? '<span data-tip="Remove the custom biome" class="icon-trash-empty hide"></span>' : ''}
|
${i > 12 && !b.cells[i] ? '<span data-tip="Remove the custom biome" class="icon-trash-empty hide"></span>' : ""}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
body.innerHTML = lines;
|
body.innerHTML = lines;
|
||||||
|
|
@ -115,7 +122,10 @@ function editBiomes() {
|
||||||
body.querySelectorAll("div.biomes").forEach(el => el.addEventListener("mouseenter", ev => biomeHighlightOn(ev)));
|
body.querySelectorAll("div.biomes").forEach(el => el.addEventListener("mouseenter", ev => biomeHighlightOn(ev)));
|
||||||
body.querySelectorAll("div.biomes").forEach(el => el.addEventListener("mouseleave", ev => biomeHighlightOff(ev)));
|
body.querySelectorAll("div.biomes").forEach(el => el.addEventListener("mouseleave", ev => biomeHighlightOff(ev)));
|
||||||
|
|
||||||
if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
|
if (body.dataset.type === "percentage") {
|
||||||
|
body.dataset.type = "absolute";
|
||||||
|
togglePercentageMode();
|
||||||
|
}
|
||||||
applySorting(biomesHeader);
|
applySorting(biomesHeader);
|
||||||
$("#biomesEditor").dialog({width: fitContent()});
|
$("#biomesEditor").dialog({width: fitContent()});
|
||||||
}
|
}
|
||||||
|
|
@ -123,14 +133,23 @@ function editBiomes() {
|
||||||
function biomeHighlightOn(event) {
|
function biomeHighlightOn(event) {
|
||||||
if (customization === 6) return;
|
if (customization === 6) return;
|
||||||
const biome = +event.target.dataset.id;
|
const biome = +event.target.dataset.id;
|
||||||
biomes.select("#biome"+biome).raise().transition(animate).attr("stroke-width", 2).attr("stroke", "#cd4c11");
|
biomes
|
||||||
|
.select("#biome" + biome)
|
||||||
|
.raise()
|
||||||
|
.transition(animate)
|
||||||
|
.attr("stroke-width", 2)
|
||||||
|
.attr("stroke", "#cd4c11");
|
||||||
}
|
}
|
||||||
|
|
||||||
function biomeHighlightOff(event) {
|
function biomeHighlightOff(event) {
|
||||||
if (customization === 6) return;
|
if (customization === 6) return;
|
||||||
const biome = +event.target.dataset.id;
|
const biome = +event.target.dataset.id;
|
||||||
const color = biomesData.color[biome];
|
const color = biomesData.color[biome];
|
||||||
biomes.select("#biome"+biome).transition().attr("stroke-width", .7).attr("stroke", color);
|
biomes
|
||||||
|
.select("#biome" + biome)
|
||||||
|
.transition()
|
||||||
|
.attr("stroke-width", 0.7)
|
||||||
|
.attr("stroke", color);
|
||||||
}
|
}
|
||||||
|
|
||||||
function biomeChangeColor(el) {
|
function biomeChangeColor(el) {
|
||||||
|
|
@ -140,8 +159,11 @@ function editBiomes() {
|
||||||
const callback = function (fill) {
|
const callback = function (fill) {
|
||||||
el.setAttribute("fill", fill);
|
el.setAttribute("fill", fill);
|
||||||
biomesData.color[biome] = fill;
|
biomesData.color[biome] = fill;
|
||||||
biomes.select("#biome"+biome).attr("fill", fill).attr("stroke", fill);
|
biomes
|
||||||
}
|
.select("#biome" + biome)
|
||||||
|
.attr("fill", fill)
|
||||||
|
.attr("stroke", fill);
|
||||||
|
};
|
||||||
|
|
||||||
openPicker(currentFill, callback);
|
openPicker(currentFill, callback);
|
||||||
}
|
}
|
||||||
|
|
@ -168,30 +190,52 @@ function editBiomes() {
|
||||||
|
|
||||||
function openWiki(el) {
|
function openWiki(el) {
|
||||||
const name = el.parentNode.dataset.name;
|
const name = el.parentNode.dataset.name;
|
||||||
if (name === "Custom" || !name) {tip("Please provide a biome name", false, "error"); return;}
|
if (name === "Custom" || !name) {
|
||||||
|
tip("Please provide a biome name", false, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
const wiki = "https://en.wikipedia.org/wiki/";
|
const wiki = "https://en.wikipedia.org/wiki/";
|
||||||
|
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case "Hot desert": openURL(wiki + "Desert_climate#Hot_desert_climates");
|
case "Hot desert":
|
||||||
case "Cold desert": openURL(wiki + "Desert_climate#Cold_desert_climates");
|
openURL(wiki + "Desert_climate#Hot_desert_climates");
|
||||||
case "Savanna": openURL(wiki + "Tropical_and_subtropical_grasslands,_savannas,_and_shrublands");
|
case "Cold desert":
|
||||||
case "Grassland": openURL(wiki + "Temperate_grasslands,_savannas,_and_shrublands");
|
openURL(wiki + "Desert_climate#Cold_desert_climates");
|
||||||
case "Tropical seasonal forest": openURL(wiki + "Seasonal_tropical_forest");
|
case "Savanna":
|
||||||
case "Temperate deciduous forest": openURL(wiki + "Temperate_deciduous_forest");
|
openURL(wiki + "Tropical_and_subtropical_grasslands,_savannas,_and_shrublands");
|
||||||
case "Tropical rainforest": openURL(wiki + "Tropical_rainforest");
|
case "Grassland":
|
||||||
case "Temperate rainforest": openURL(wiki + "Temperate_rainforest");
|
openURL(wiki + "Temperate_grasslands,_savannas,_and_shrublands");
|
||||||
case "Taiga": openURL(wiki + "Taiga");
|
case "Tropical seasonal forest":
|
||||||
case "Tundra": openURL(wiki + "Tundra");
|
openURL(wiki + "Seasonal_tropical_forest");
|
||||||
case "Glacier": openURL(wiki + "Glacier");
|
case "Temperate deciduous forest":
|
||||||
case "Wetland": openURL(wiki + "Wetland");
|
openURL(wiki + "Temperate_deciduous_forest");
|
||||||
default: openURL(`https://en.wikipedia.org/w/index.php?search=${name}`);
|
case "Tropical rainforest":
|
||||||
|
openURL(wiki + "Tropical_rainforest");
|
||||||
|
case "Temperate rainforest":
|
||||||
|
openURL(wiki + "Temperate_rainforest");
|
||||||
|
case "Taiga":
|
||||||
|
openURL(wiki + "Taiga");
|
||||||
|
case "Tundra":
|
||||||
|
openURL(wiki + "Tundra");
|
||||||
|
case "Glacier":
|
||||||
|
openURL(wiki + "Glacier");
|
||||||
|
case "Wetland":
|
||||||
|
openURL(wiki + "Wetland");
|
||||||
|
default:
|
||||||
|
openURL(`https://en.wikipedia.org/w/index.php?search=${name}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleLegend() {
|
function toggleLegend() {
|
||||||
if (legend.selectAll("*").size()) {clearLegend(); return;}; // hide legend
|
if (legend.selectAll("*").size()) {
|
||||||
|
clearLegend();
|
||||||
|
return;
|
||||||
|
} // hide legend
|
||||||
const d = biomesData;
|
const d = biomesData;
|
||||||
const data = Array.from(d.i).filter(i => d.cells[i]).sort((a, b) => d.area[b] - d.area[a]).map(i => [i, d.color[i], d.name[i]]);
|
const data = Array.from(d.i)
|
||||||
|
.filter(i => d.cells[i])
|
||||||
|
.sort((a, b) => d.area[b] - d.area[a])
|
||||||
|
.map(i => [i, d.color[i], d.name[i]]);
|
||||||
drawLegend("Biomes", data);
|
drawLegend("Biomes", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -203,9 +247,9 @@ function editBiomes() {
|
||||||
const totalPopulation = +biomesFooterPopulation.dataset.population;
|
const totalPopulation = +biomesFooterPopulation.dataset.population;
|
||||||
|
|
||||||
body.querySelectorAll(":scope> div").forEach(function (el) {
|
body.querySelectorAll(":scope> div").forEach(function (el) {
|
||||||
el.querySelector(".biomeCells").innerHTML = rn(+el.dataset.cells / totalCells * 100) + "%";
|
el.querySelector(".biomeCells").innerHTML = rn((+el.dataset.cells / totalCells) * 100) + "%";
|
||||||
el.querySelector(".biomeArea").innerHTML = rn(+el.dataset.area / totalArea * 100) + "%";
|
el.querySelector(".biomeArea").innerHTML = rn((+el.dataset.area / totalArea) * 100) + "%";
|
||||||
el.querySelector(".biomePopulation").innerHTML = rn(+el.dataset.population / totalPopulation * 100) + "%";
|
el.querySelector(".biomePopulation").innerHTML = rn((+el.dataset.population / totalPopulation) * 100) + "%";
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
body.dataset.type = "absolute";
|
body.dataset.type = "absolute";
|
||||||
|
|
@ -214,8 +258,12 @@ function editBiomes() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function addCustomBiome() {
|
function addCustomBiome() {
|
||||||
const b = biomesData, i = biomesData.i.length;
|
const b = biomesData,
|
||||||
if (i > 254) {tip("Maximum number of biomes reached (255), data cleansing is required", false, "error"); return;}
|
i = biomesData.i.length;
|
||||||
|
if (i > 254) {
|
||||||
|
tip("Maximum number of biomes reached (255), data cleansing is required", false, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
b.i.push(i);
|
b.i.push(i);
|
||||||
b.color.push(getRandomColor());
|
b.color.push(getRandomColor());
|
||||||
|
|
@ -285,20 +333,17 @@ function editBiomes() {
|
||||||
customization = 6;
|
customization = 6;
|
||||||
biomes.append("g").attr("id", "temp");
|
biomes.append("g").attr("id", "temp");
|
||||||
|
|
||||||
document.querySelectorAll("#biomesBottom > button").forEach(el => el.style.display = "none");
|
document.querySelectorAll("#biomesBottom > button").forEach(el => (el.style.display = "none"));
|
||||||
document.querySelectorAll("#biomesBottom > div").forEach(el => el.style.display = "block");
|
document.querySelectorAll("#biomesBottom > div").forEach(el => (el.style.display = "block"));
|
||||||
body.querySelector("div.biomes").classList.add("selected");
|
body.querySelector("div.biomes").classList.add("selected");
|
||||||
|
|
||||||
biomesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
|
biomesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
|
||||||
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "none");
|
body.querySelectorAll("div > input, select, span, svg").forEach(e => (e.style.pointerEvents = "none"));
|
||||||
biomesFooter.style.display = "none";
|
biomesFooter.style.display = "none";
|
||||||
$("#biomesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}});
|
$("#biomesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}});
|
||||||
|
|
||||||
tip("Click on biome to select, drag the circle to change biome", true);
|
tip("Click on biome to select, drag the circle to change biome", true);
|
||||||
viewbox.style("cursor", "crosshair")
|
viewbox.style("cursor", "crosshair").on("click", selectBiomeOnMapClick).call(d3.drag().on("start", dragBiomeBrush)).on("touchmove mousemove", moveBiomeBrush);
|
||||||
.on("click", selectBiomeOnMapClick)
|
|
||||||
.call(d3.drag().on("start", dragBiomeBrush))
|
|
||||||
.on("touchmove mousemove", moveBiomeBrush);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectBiomeOnLineClick(line) {
|
function selectBiomeOnLineClick(line) {
|
||||||
|
|
@ -310,7 +355,10 @@ function editBiomes() {
|
||||||
function selectBiomeOnMapClick() {
|
function selectBiomeOnMapClick() {
|
||||||
const point = d3.mouse(this);
|
const point = d3.mouse(this);
|
||||||
const i = findCell(point[0], point[1]);
|
const i = findCell(point[0], point[1]);
|
||||||
if (pack.cells.h[i] < 20) {tip("You cannot reassign water via biomes. Please edit the Heightmap to change water", false, "error"); return;}
|
if (pack.cells.h[i] < 20) {
|
||||||
|
tip("You cannot reassign water via biomes. Please edit the Heightmap to change water", false, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const assigned = biomes.select("#temp").select("polygon[data-cell='" + i + "']");
|
const assigned = biomes.select("#temp").select("polygon[data-cell='" + i + "']");
|
||||||
const biome = assigned.size() ? +assigned.attr("data-biome") : pack.cells.biome[i];
|
const biome = assigned.size() ? +assigned.attr("data-biome") : pack.cells.biome[i];
|
||||||
|
|
@ -379,10 +427,10 @@ function editBiomes() {
|
||||||
biomes.select("#temp").remove();
|
biomes.select("#temp").remove();
|
||||||
removeCircle();
|
removeCircle();
|
||||||
|
|
||||||
document.querySelectorAll("#biomesBottom > button").forEach(el => el.style.display = "inline-block");
|
document.querySelectorAll("#biomesBottom > button").forEach(el => (el.style.display = "inline-block"));
|
||||||
document.querySelectorAll("#biomesBottom > div").forEach(el => el.style.display = "none");
|
document.querySelectorAll("#biomesBottom > div").forEach(el => (el.style.display = "none"));
|
||||||
|
|
||||||
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "all");
|
body.querySelectorAll("div > input, select, span, svg").forEach(e => (e.style.pointerEvents = "all"));
|
||||||
biomesEditor.querySelectorAll(".hide").forEach(el => el.classList.remove("hidden"));
|
biomesEditor.querySelectorAll(".hide").forEach(el => el.classList.remove("hidden"));
|
||||||
biomesFooter.style.display = "block";
|
biomesFooter.style.display = "block";
|
||||||
if (!close) $("#biomesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}});
|
if (!close) $("#biomesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}});
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ function editBurg(id) {
|
||||||
|
|
||||||
document.getElementById('burgName').value = b.name;
|
document.getElementById('burgName').value = b.name;
|
||||||
document.getElementById('burgType').value = b.type || 'Generic';
|
document.getElementById('burgType').value = b.type || 'Generic';
|
||||||
document.getElementById('burgPopulation').value = rn(b.population * populationRate.value * urbanization.value);
|
document.getElementById('burgPopulation').value = rn(b.population * populationRate * urbanization);
|
||||||
document.getElementById('burgEditAnchorStyle').style.display = +b.port ? 'inline-block' : 'none';
|
document.getElementById('burgEditAnchorStyle').style.display = +b.port ? 'inline-block' : 'none';
|
||||||
|
|
||||||
// update list and select culture
|
// update list and select culture
|
||||||
|
|
@ -123,7 +123,7 @@ function editBurg(id) {
|
||||||
'Okhotsk (Russia)',
|
'Okhotsk (Russia)',
|
||||||
'Fairbanks (Alaska)',
|
'Fairbanks (Alaska)',
|
||||||
'Nuuk (Greenland)',
|
'Nuuk (Greenland)',
|
||||||
'Murmansk',
|
'Murmansk', // -5 - 0
|
||||||
'Arkhangelsk',
|
'Arkhangelsk',
|
||||||
'Anchorage',
|
'Anchorage',
|
||||||
'Tromsø',
|
'Tromsø',
|
||||||
|
|
@ -133,7 +133,7 @@ function editBurg(id) {
|
||||||
'Halifax',
|
'Halifax',
|
||||||
'Prague',
|
'Prague',
|
||||||
'Copenhagen',
|
'Copenhagen',
|
||||||
'London',
|
'London', // 1 - 10
|
||||||
'Antwerp',
|
'Antwerp',
|
||||||
'Paris',
|
'Paris',
|
||||||
'Milan',
|
'Milan',
|
||||||
|
|
@ -143,7 +143,7 @@ function editBurg(id) {
|
||||||
'Lisbon',
|
'Lisbon',
|
||||||
'Barcelona',
|
'Barcelona',
|
||||||
'Marrakesh',
|
'Marrakesh',
|
||||||
'Alexandria',
|
'Alexandria', // 11 - 20
|
||||||
'Tegucigalpa',
|
'Tegucigalpa',
|
||||||
'Guangzhou',
|
'Guangzhou',
|
||||||
'Rio de Janeiro',
|
'Rio de Janeiro',
|
||||||
|
|
@ -153,11 +153,10 @@ function editBurg(id) {
|
||||||
'Mogadishu',
|
'Mogadishu',
|
||||||
'Bangkok',
|
'Bangkok',
|
||||||
'Aden',
|
'Aden',
|
||||||
'Khartoum',
|
'Khartoum'
|
||||||
'Mecca'
|
]; // 21 - 30
|
||||||
];
|
if (temperature > 30) return 'Mecca';
|
||||||
const city = cities[temperature + 5];
|
return cities[temperature + 5] || null;
|
||||||
return city ? 'in ' + city : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function dragBurgLabel() {
|
function dragBurgLabel() {
|
||||||
|
|
@ -332,7 +331,7 @@ function editBurg(id) {
|
||||||
|
|
||||||
function changePopulation() {
|
function changePopulation() {
|
||||||
const id = +elSelected.attr('data-id');
|
const id = +elSelected.attr('data-id');
|
||||||
pack.burgs[id].population = rn(burgPopulation.value / populationRate.value / urbanization.value, 4);
|
pack.burgs[id].population = rn(burgPopulation.value / populationRate / urbanization, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleFeature() {
|
function toggleFeature() {
|
||||||
|
|
@ -423,7 +422,7 @@ function editBurg(id) {
|
||||||
const cells = pack.cells;
|
const cells = pack.cells;
|
||||||
const name = elSelected.text();
|
const name = elSelected.text();
|
||||||
const size = Math.max(Math.min(rn(burg.population), 100), 6); // to be removed once change on MFDC is done
|
const size = Math.max(Math.min(rn(burg.population), 100), 6); // to be removed once change on MFDC is done
|
||||||
const population = rn(burg.population * populationRate.value * urbanization.value);
|
const population = rn(burg.population * populationRate * urbanization);
|
||||||
|
|
||||||
const s = burg.MFCG || defSeed;
|
const s = burg.MFCG || defSeed;
|
||||||
const cell = burg.cell;
|
const cell = burg.cell;
|
||||||
|
|
@ -547,7 +546,7 @@ function editBurg(id) {
|
||||||
const id = +elSelected.attr('data-id');
|
const id = +elSelected.attr('data-id');
|
||||||
if (pack.burgs[id].capital) {
|
if (pack.burgs[id].capital) {
|
||||||
alertMessage.innerHTML = `You cannot remove the burg as it is a state capital.<br><br>
|
alertMessage.innerHTML = `You cannot remove the burg as it is a state capital.<br><br>
|
||||||
Please change state capital first. You can do it using Burgs Editor (shift + T)`;
|
You can change the capital using Burgs Editor (shift + T)`;
|
||||||
$('#alert').dialog({
|
$('#alert').dialog({
|
||||||
resizable: false,
|
resizable: false,
|
||||||
title: 'Remove burg',
|
title: 'Remove burg',
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ function overviewBurgs() {
|
||||||
totalPopulation = 0;
|
totalPopulation = 0;
|
||||||
|
|
||||||
for (const b of filtered) {
|
for (const b of filtered) {
|
||||||
const population = b.population * populationRate.value * urbanization.value;
|
const population = b.population * populationRate * urbanization;
|
||||||
totalPopulation += population;
|
totalPopulation += population;
|
||||||
const type = b.capital && b.port ? 'a-capital-port' : b.capital ? 'c-capital' : b.port ? 'p-port' : 'z-burg';
|
const type = b.capital && b.port ? 'a-capital-port' : b.capital ? 'c-capital' : b.port ? 'p-port' : 'z-burg';
|
||||||
const state = pack.states[b.state].name;
|
const state = pack.states[b.state].name;
|
||||||
|
|
@ -91,8 +91,8 @@ function overviewBurgs() {
|
||||||
<span data-tip="${b.capital ? ' This burg is a state capital' : 'Click to assign a capital status'}" class="icon-star-empty${b.capital ? '' : ' inactive pointer'}"></span>
|
<span data-tip="${b.capital ? ' This burg is a state capital' : 'Click to assign a capital status'}" class="icon-star-empty${b.capital ? '' : ' inactive pointer'}"></span>
|
||||||
<span data-tip="Click to toggle port status" class="icon-anchor pointer${b.port ? '' : ' inactive'}" style="font-size:.9em"></span>
|
<span data-tip="Click to toggle port status" class="icon-anchor pointer${b.port ? '' : ' inactive'}" style="font-size:.9em"></span>
|
||||||
</div>
|
</div>
|
||||||
|
<span data-tip="Edit burg" class="icon-pencil"></span>
|
||||||
<span class="locks pointer ${b.lock ? 'icon-lock' : 'icon-lock-open inactive'}"></span>
|
<span class="locks pointer ${b.lock ? 'icon-lock' : 'icon-lock-open inactive'}"></span>
|
||||||
<span data-tip="Click to zoom into view" class="icon-dot-circled pointer"></span>
|
|
||||||
<span data-tip="Remove burg" class="icon-trash-empty"></span>
|
<span data-tip="Remove burg" class="icon-trash-empty"></span>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
@ -163,10 +163,10 @@ function overviewBurgs() {
|
||||||
const burg = +this.parentNode.dataset.id;
|
const burg = +this.parentNode.dataset.id;
|
||||||
if (this.value == '' || isNaN(+this.value)) {
|
if (this.value == '' || isNaN(+this.value)) {
|
||||||
tip('Please provide an integer number (like 10000, not 10K)', false, 'error');
|
tip('Please provide an integer number (like 10000, not 10K)', false, 'error');
|
||||||
this.value = si(pack.burgs[burg].population * populationRate.value * urbanization.value);
|
this.value = si(pack.burgs[burg].population * populationRate * urbanization);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pack.burgs[burg].population = this.value / populationRate.value / urbanization.value;
|
pack.burgs[burg].population = this.value / populationRate / urbanization;
|
||||||
this.parentNode.dataset.population = this.value;
|
this.parentNode.dataset.population = this.value;
|
||||||
this.value = si(this.value);
|
this.value = si(this.value);
|
||||||
|
|
||||||
|
|
@ -255,14 +255,9 @@ function overviewBurgs() {
|
||||||
function addBurgOnClick() {
|
function addBurgOnClick() {
|
||||||
const point = d3.mouse(this);
|
const point = d3.mouse(this);
|
||||||
const cell = findCell(point[0], point[1]);
|
const cell = findCell(point[0], point[1]);
|
||||||
if (pack.cells.h[cell] < 20) {
|
if (pack.cells.h[cell] < 20) return tip('You cannot place state into the water. Please click on a land cell', false, 'error');
|
||||||
tip('You cannot place state into the water. Please click on a land cell', false, 'error');
|
if (pack.cells.burg[cell]) return tip('There is already a burg in this cell. Please select a free cell', false, 'error');
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (pack.cells.burg[cell]) {
|
|
||||||
tip('There is already a burg in this cell. Please select a free cell', false, 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
addBurg(point); // add new burg
|
addBurg(point); // add new burg
|
||||||
|
|
||||||
if (d3.event.shiftKey === false) {
|
if (d3.event.shiftKey === false) {
|
||||||
|
|
@ -347,7 +342,7 @@ function overviewBurgs() {
|
||||||
d3.select(ev.target).transition().duration(1500).attr('stroke', '#c13119');
|
d3.select(ev.target).transition().duration(1500).attr('stroke', '#c13119');
|
||||||
const name = d.data.name;
|
const name = d.data.name;
|
||||||
const parent = d.parent.data.name;
|
const parent = d.parent.data.name;
|
||||||
const population = si(d.value * populationRate.value * urbanization.value);
|
const population = si(d.value * populationRate * urbanization);
|
||||||
|
|
||||||
burgsInfo.innerHTML = `${name}. ${parent}. Population: ${population}`;
|
burgsInfo.innerHTML = `${name}. ${parent}. Population: ${population}`;
|
||||||
burgHighlightOn(ev);
|
burgHighlightOn(ev);
|
||||||
|
|
@ -449,7 +444,7 @@ function overviewBurgs() {
|
||||||
data += b.state ? pack.states[b.state].fullName + ',' : pack.states[b.state].name + ',';
|
data += b.state ? pack.states[b.state].fullName + ',' : pack.states[b.state].name + ',';
|
||||||
data += pack.cultures[b.culture].name + ',';
|
data += pack.cultures[b.culture].name + ',';
|
||||||
data += pack.religions[pack.cells.religion[b.cell]].name + ',';
|
data += pack.religions[pack.cells.religion[b.cell]].name + ',';
|
||||||
data += rn(b.population * populationRate.value * urbanization.value) + ',';
|
data += rn(b.population * populationRate * urbanization) + ',';
|
||||||
|
|
||||||
// add geography data
|
// add geography data
|
||||||
data += mapCoordinates.lonW + (b.x / graphWidth) * mapCoordinates.lonT + ',';
|
data += mapCoordinates.lonW + (b.x / graphWidth) * mapCoordinates.lonT + ',';
|
||||||
|
|
@ -497,15 +492,9 @@ function overviewBurgs() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function importBurgNames(dataLoaded) {
|
function importBurgNames(dataLoaded) {
|
||||||
if (!dataLoaded) {
|
if (!dataLoaded) return tip('Cannot load the file, please check the format', false, 'error');
|
||||||
tip('Cannot load the file, please check the format', false, 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const data = dataLoaded.split('\r\n');
|
const data = dataLoaded.split('\r\n');
|
||||||
if (!data.length) {
|
if (!data.length) return tip('Cannot parse the list, please check the file format', false, 'error');
|
||||||
tip('Cannot parse the list, please check the file format', false, 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let change = [],
|
let change = [],
|
||||||
message = `Burgs will be renamed as below. Please confirm`;
|
message = `Burgs will be renamed as below. Please confirm`;
|
||||||
|
|
|
||||||
|
|
@ -72,8 +72,8 @@ function editCultures() {
|
||||||
for (const c of pack.cultures) {
|
for (const c of pack.cultures) {
|
||||||
if (c.removed) continue;
|
if (c.removed) continue;
|
||||||
const area = c.area * distanceScaleInput.value ** 2;
|
const area = c.area * distanceScaleInput.value ** 2;
|
||||||
const rural = c.rural * populationRate.value;
|
const rural = c.rural * populationRate;
|
||||||
const urban = c.urban * populationRate.value * urbanization.value;
|
const urban = c.urban * populationRate * urbanization;
|
||||||
const population = rn(rural + urban);
|
const population = rn(rural + urban);
|
||||||
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}. Click to edit`;
|
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}. Click to edit`;
|
||||||
totalArea += area;
|
totalArea += area;
|
||||||
|
|
@ -186,8 +186,8 @@ function editCultures() {
|
||||||
.select("g[data-id='" + culture + "'] > path")
|
.select("g[data-id='" + culture + "'] > path")
|
||||||
.classed('selected', 1);
|
.classed('selected', 1);
|
||||||
const c = pack.cultures[culture];
|
const c = pack.cultures[culture];
|
||||||
const rural = c.rural * populationRate.value;
|
const rural = c.rural * populationRate;
|
||||||
const urban = c.urban * populationRate.value * urbanization.value;
|
const urban = c.urban * populationRate * urbanization;
|
||||||
const population = rural + urban > 0 ? si(rn(rural + urban)) + ' people' : 'Extinct';
|
const population = rural + urban > 0 ? si(rn(rural + urban)) + ' people' : 'Extinct';
|
||||||
info.innerHTML = `${c.name} culture. ${c.type}. ${population}`;
|
info.innerHTML = `${c.name} culture. ${c.type}. ${population}`;
|
||||||
tip('Drag to change parent, drag to itself to move to the top level. Hold CTRL and click to change abbreviation');
|
tip('Drag to change parent, drag to itself to move to the top level. Hold CTRL and click to change abbreviation');
|
||||||
|
|
@ -323,8 +323,8 @@ function editCultures() {
|
||||||
tip('Culture does not have any cells, cannot change population', false, 'error');
|
tip('Culture does not have any cells, cannot change population', false, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const rural = rn(c.rural * populationRate.value);
|
const rural = rn(c.rural * populationRate);
|
||||||
const urban = rn(c.urban * populationRate.value * urbanization.value);
|
const urban = rn(c.urban * populationRate * urbanization);
|
||||||
const total = rural + urban;
|
const total = rural + urban;
|
||||||
const l = (n) => Number(n).toLocaleString();
|
const l = (n) => Number(n).toLocaleString();
|
||||||
const burgs = pack.burgs.filter((b) => !b.removed && b.culture === culture);
|
const burgs = pack.burgs.filter((b) => !b.removed && b.culture === culture);
|
||||||
|
|
@ -367,7 +367,7 @@ function editCultures() {
|
||||||
cells.forEach((i) => (pack.cells.pop[i] *= ruralChange));
|
cells.forEach((i) => (pack.cells.pop[i] *= ruralChange));
|
||||||
}
|
}
|
||||||
if (!isFinite(ruralChange) && +ruralPop.value > 0) {
|
if (!isFinite(ruralChange) && +ruralPop.value > 0) {
|
||||||
const points = ruralPop.value / populationRate.value;
|
const points = ruralPop.value / populationRate;
|
||||||
const cells = pack.cells.i.filter((i) => pack.cells.culture[i] === culture);
|
const cells = pack.cells.i.filter((i) => pack.cells.culture[i] === culture);
|
||||||
const pop = rn(points / cells.length);
|
const pop = rn(points / cells.length);
|
||||||
cells.forEach((i) => (pack.cells.pop[i] = pop));
|
cells.forEach((i) => (pack.cells.pop[i] = pop));
|
||||||
|
|
@ -378,7 +378,7 @@ function editCultures() {
|
||||||
burgs.forEach((b) => (b.population = rn(b.population * urbanChange, 4)));
|
burgs.forEach((b) => (b.population = rn(b.population * urbanChange, 4)));
|
||||||
}
|
}
|
||||||
if (!isFinite(urbanChange) && +urbanPop.value > 0) {
|
if (!isFinite(urbanChange) && +urbanPop.value > 0) {
|
||||||
const points = urbanPop.value / populationRate.value / urbanization.value;
|
const points = urbanPop.value / populationRate / urbanization;
|
||||||
const population = rn(points / burgs.length, 4);
|
const population = rn(points / burgs.length, 4);
|
||||||
burgs.forEach((b) => (b.population = population));
|
burgs.forEach((b) => (b.population = population));
|
||||||
}
|
}
|
||||||
|
|
@ -402,8 +402,12 @@ function editCultures() {
|
||||||
if (customization === 4) return;
|
if (customization === 4) return;
|
||||||
const culture = +this.parentNode.dataset.id;
|
const culture = +this.parentNode.dataset.id;
|
||||||
|
|
||||||
const message = 'Are you sure you want to remove the culture? <br>This action cannot be reverted';
|
alertMessage.innerHTML = 'Are you sure you want to remove the culture? <br>This action cannot be reverted';
|
||||||
const onConfirm = () => {
|
$('#alert').dialog({
|
||||||
|
resizable: false,
|
||||||
|
title: 'Remove culture',
|
||||||
|
buttons: {
|
||||||
|
Remove: function () {
|
||||||
cults.select('#culture' + culture).remove();
|
cults.select('#culture' + culture).remove();
|
||||||
debug.select('#cultureCenter' + culture).remove();
|
debug.select('#cultureCenter' + culture).remove();
|
||||||
|
|
||||||
|
|
@ -421,8 +425,13 @@ function editCultures() {
|
||||||
if (c.origin === culture) c.origin = origin;
|
if (c.origin === culture) c.origin = origin;
|
||||||
});
|
});
|
||||||
refreshCulturesEditor();
|
refreshCulturesEditor();
|
||||||
};
|
$(this).dialog('close');
|
||||||
confirmationDialog({title: 'Remove culture', message, confirm: 'Remove', onConfirm});
|
},
|
||||||
|
Cancel: function () {
|
||||||
|
$(this).dialog('close');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawCultureCenters() {
|
function drawCultureCenters() {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,10 @@
|
||||||
|
|
||||||
function showEPForRoute(node) {
|
function showEPForRoute(node) {
|
||||||
const points = [];
|
const points = [];
|
||||||
debug.select("#controlPoints").selectAll("circle").each(function() {
|
debug
|
||||||
|
.select("#controlPoints")
|
||||||
|
.selectAll("circle")
|
||||||
|
.each(function () {
|
||||||
const i = findCell(this.getAttribute("cx"), this.getAttribute("cy"));
|
const i = findCell(this.getAttribute("cx"), this.getAttribute("cy"));
|
||||||
points.push(i);
|
points.push(i);
|
||||||
});
|
});
|
||||||
|
|
@ -13,7 +16,10 @@ function showEPForRoute(node) {
|
||||||
|
|
||||||
function showEPForRiver(node) {
|
function showEPForRiver(node) {
|
||||||
const points = [];
|
const points = [];
|
||||||
debug.select("#controlPoints").selectAll("circle").each(function() {
|
debug
|
||||||
|
.select("#controlPoints")
|
||||||
|
.selectAll("circle")
|
||||||
|
.each(function () {
|
||||||
const i = findCell(this.getAttribute("cx"), this.getAttribute("cy"));
|
const i = findCell(this.getAttribute("cx"), this.getAttribute("cy"));
|
||||||
points.push(i);
|
points.push(i);
|
||||||
});
|
});
|
||||||
|
|
@ -29,7 +35,9 @@ function showElevationProfile(data, routeLen, isRiver) {
|
||||||
document.getElementById("epSave").addEventListener("click", downloadCSV);
|
document.getElementById("epSave").addEventListener("click", downloadCSV);
|
||||||
|
|
||||||
$("#elevationProfile").dialog({
|
$("#elevationProfile").dialog({
|
||||||
title: "Elevation profile", resizable: false, width: window.width,
|
title: "Elevation profile",
|
||||||
|
resizable: false,
|
||||||
|
width: window.width,
|
||||||
close: closeElevationProfile,
|
close: closeElevationProfile,
|
||||||
position: {my: "left top", at: "left+20 bottom-500", of: window, collision: "fit"}
|
position: {my: "left top", at: "left+20 bottom-500", of: window, collision: "fit"}
|
||||||
});
|
});
|
||||||
|
|
@ -44,8 +52,10 @@ function showElevationProfile(data, routeLen, isRiver) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const chartWidth = window.innerWidth-180, chartHeight = 300; // height of our land/sea profile, excluding the biomes data below
|
const chartWidth = window.innerWidth - 180,
|
||||||
const xOffset = 80, yOffset = 80; // this is our drawing starting point from top-left (y = 0) of SVG
|
chartHeight = 300; // height of our land/sea profile, excluding the biomes data below
|
||||||
|
const xOffset = 80,
|
||||||
|
yOffset = 80; // this is our drawing starting point from top-left (y = 0) of SVG
|
||||||
const biomesHeight = 40;
|
const biomesHeight = 40;
|
||||||
|
|
||||||
let lastBurgIndex = 0;
|
let lastBurgIndex = 0;
|
||||||
|
|
@ -57,7 +67,8 @@ function showElevationProfile(data, routeLen, isRiver) {
|
||||||
let h = pack.cells.h[cell];
|
let h = pack.cells.h[cell];
|
||||||
if (h < 20) {
|
if (h < 20) {
|
||||||
const f = pack.features[pack.cells.f[cell]];
|
const f = pack.features[pack.cells.f[cell]];
|
||||||
if (f.type === "lake") h = f.height; else h = 20;
|
if (f.type === "lake") h = f.height;
|
||||||
|
else h = 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for river up-hill
|
// check for river up-hill
|
||||||
|
|
@ -73,13 +84,17 @@ function showElevationProfile(data, routeLen, isRiver) {
|
||||||
let b = pack.cells.burg[cell];
|
let b = pack.cells.burg[cell];
|
||||||
if (b == prevB) b = 0;
|
if (b == prevB) b = 0;
|
||||||
else prevB = b;
|
else prevB = b;
|
||||||
if (b) { burgCount++; lastBurgIndex = i; lastBurgCell = cell; }
|
if (b) {
|
||||||
|
burgCount++;
|
||||||
|
lastBurgIndex = i;
|
||||||
|
lastBurgCell = cell;
|
||||||
|
}
|
||||||
|
|
||||||
chartData.biome[i] = pack.cells.biome[cell];
|
chartData.biome[i] = pack.cells.biome[cell];
|
||||||
chartData.burg[i] = b;
|
chartData.burg[i] = b;
|
||||||
chartData.cell[i] = cell;
|
chartData.cell[i] = cell;
|
||||||
let sh = getHeight(h);
|
let sh = getHeight(h);
|
||||||
chartData.height[i] = parseInt(sh.substr(0, sh.indexOf(' ')));
|
chartData.height[i] = parseInt(sh.substr(0, sh.indexOf(" ")));
|
||||||
chartData.mih = Math.min(chartData.mih, h);
|
chartData.mih = Math.min(chartData.mih, h);
|
||||||
chartData.mah = Math.max(chartData.mah, h);
|
chartData.mah = Math.max(chartData.mah, h);
|
||||||
chartData.mi = Math.min(chartData.mi, chartData.height[i]);
|
chartData.mi = Math.min(chartData.mi, chartData.height[i]);
|
||||||
|
|
@ -113,10 +128,10 @@ function showElevationProfile(data, routeLen, isRiver) {
|
||||||
data += cell + ",";
|
data += cell + ",";
|
||||||
data += getHeight(h) + ",";
|
data += getHeight(h) + ",";
|
||||||
data += h + ",";
|
data += h + ",";
|
||||||
data += rn(pop * populationRate.value) + ",";
|
data += rn(pop * populationRate) + ",";
|
||||||
if (burg) {
|
if (burg) {
|
||||||
data += pack.burgs[burg].name + ",";
|
data += pack.burgs[burg].name + ",";
|
||||||
data += (pack.burgs[burg].population * populationRate.value * urbanization.value) + ",";
|
data += pack.burgs[burg].population * populationRate * urbanization + ",";
|
||||||
} else {
|
} else {
|
||||||
data += ",0,";
|
data += ",0,";
|
||||||
}
|
}
|
||||||
|
|
@ -142,10 +157,13 @@ function showElevationProfile(data, routeLen, isRiver) {
|
||||||
chartData.points = [];
|
chartData.points = [];
|
||||||
let heightScale = 100 / parseInt(epScaleRange.value);
|
let heightScale = 100 / parseInt(epScaleRange.value);
|
||||||
|
|
||||||
heightScale *= .9; // curves cause the heights to go slightly higher, adjust here
|
heightScale *= 0.9; // curves cause the heights to go slightly higher, adjust here
|
||||||
|
|
||||||
const xscale = d3.scaleLinear().domain([0, data.length]).range([0, chartWidth]);
|
const xscale = d3.scaleLinear().domain([0, data.length]).range([0, chartWidth]);
|
||||||
const yscale = d3.scaleLinear().domain([0, chartData.ma * heightScale]).range([chartHeight, 0]);
|
const yscale = d3
|
||||||
|
.scaleLinear()
|
||||||
|
.domain([0, chartData.ma * heightScale])
|
||||||
|
.range([chartHeight, 0]);
|
||||||
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
for (let i = 0; i < data.length; i++) {
|
||||||
chartData.points.push([xscale(i) + xOffset, yscale(chartData.height[i]) + yOffset]);
|
chartData.points.push([xscale(i) + xOffset, yscale(chartData.height[i]) + yOffset]);
|
||||||
|
|
@ -153,7 +171,13 @@ function showElevationProfile(data, routeLen, isRiver) {
|
||||||
|
|
||||||
document.getElementById("elevationGraph").innerHTML = "";
|
document.getElementById("elevationGraph").innerHTML = "";
|
||||||
|
|
||||||
const chart = d3.select("#elevationGraph").append("svg").attr("width", chartWidth+120).attr("height", chartHeight+yOffset+biomesHeight).attr("id", "elevationSVG").attr("class", "epbackground");
|
const chart = d3
|
||||||
|
.select("#elevationGraph")
|
||||||
|
.append("svg")
|
||||||
|
.attr("width", chartWidth + 120)
|
||||||
|
.attr("height", chartHeight + yOffset + biomesHeight)
|
||||||
|
.attr("id", "elevationSVG")
|
||||||
|
.attr("class", "epbackground");
|
||||||
// arrow-head definition
|
// arrow-head definition
|
||||||
chart.append("defs").append("marker").attr("id", "arrowhead").attr("orient", "auto").attr("markerWidth", "2").attr("markerHeight", "4").attr("refX", "0.1").attr("refY", "2").append("path").attr("d", "M0,0 V4 L2,2 Z").attr("fill", "darkgray");
|
chart.append("defs").append("marker").attr("id", "arrowhead").attr("orient", "auto").attr("markerWidth", "2").attr("markerHeight", "4").attr("refX", "0.1").attr("refY", "2").append("path").attr("d", "M0,0 V4 L2,2 Z").attr("fill", "darkgray");
|
||||||
|
|
||||||
|
|
@ -161,12 +185,21 @@ function showElevationProfile(data, routeLen, isRiver) {
|
||||||
const landdef = chart.select("defs").append("linearGradient").attr("id", "landdef").attr("x1", "0%").attr("y1", "0%").attr("x2", "0%").attr("y2", "100%");
|
const landdef = chart.select("defs").append("linearGradient").attr("id", "landdef").attr("x1", "0%").attr("y1", "0%").attr("x2", "0%").attr("y2", "100%");
|
||||||
|
|
||||||
if (chartData.mah == chartData.mih) {
|
if (chartData.mah == chartData.mih) {
|
||||||
landdef.append("stop").attr("offset", "0%").attr("style", "stop-color:" + getColor(chartData.mih, colors) + ";stop-opacity:1");
|
landdef
|
||||||
landdef.append("stop").attr("offset", "100%").attr("style", "stop-color:" + getColor(chartData.mah, colors) + ";stop-opacity:1");
|
.append("stop")
|
||||||
|
.attr("offset", "0%")
|
||||||
|
.attr("style", "stop-color:" + getColor(chartData.mih, colors) + ";stop-opacity:1");
|
||||||
|
landdef
|
||||||
|
.append("stop")
|
||||||
|
.attr("offset", "100%")
|
||||||
|
.attr("style", "stop-color:" + getColor(chartData.mah, colors) + ";stop-opacity:1");
|
||||||
} else {
|
} else {
|
||||||
for (let k = chartData.mah; k >= chartData.mih; k--) {
|
for (let k = chartData.mah; k >= chartData.mih; k--) {
|
||||||
let perc = 1 - (k - chartData.mih) / (chartData.mah - chartData.mih);
|
let perc = 1 - (k - chartData.mih) / (chartData.mah - chartData.mih);
|
||||||
landdef.append("stop").attr("offset", perc*100 + "%").attr("style", "stop-color:" + getColor(k, colors) + ";stop-opacity:1");
|
landdef
|
||||||
|
.append("stop")
|
||||||
|
.attr("offset", perc * 100 + "%")
|
||||||
|
.attr("style", "stop-color:" + getColor(k, colors) + ";stop-opacity:1");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -174,12 +207,24 @@ function showElevationProfile(data, routeLen, isRiver) {
|
||||||
let curve = d3.line().curve(d3.curveBasis); // see https://github.com/d3/d3-shape#curves
|
let curve = d3.line().curve(d3.curveBasis); // see https://github.com/d3/d3-shape#curves
|
||||||
let epCurveIndex = parseInt(epCurve.selectedIndex);
|
let epCurveIndex = parseInt(epCurve.selectedIndex);
|
||||||
switch (epCurveIndex) {
|
switch (epCurveIndex) {
|
||||||
case 0 : curve = d3.line().curve(d3.curveLinear); break;
|
case 0:
|
||||||
case 1 : curve = d3.line().curve(d3.curveBasis); break;
|
curve = d3.line().curve(d3.curveLinear);
|
||||||
case 2 : curve = d3.line().curve(d3.curveBundle.beta(1)); break;
|
break;
|
||||||
case 3 : curve = d3.line().curve(d3.curveCatmullRom.alpha(0.5)); break;
|
case 1:
|
||||||
case 4 : curve = d3.line().curve(d3.curveMonotoneX); break;
|
curve = d3.line().curve(d3.curveBasis);
|
||||||
case 5 : curve = d3.line().curve(d3.curveNatural); break;
|
break;
|
||||||
|
case 2:
|
||||||
|
curve = d3.line().curve(d3.curveBundle.beta(1));
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
curve = d3.line().curve(d3.curveCatmullRom.alpha(0.5));
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
curve = d3.line().curve(d3.curveMonotoneX);
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
curve = d3.line().curve(d3.curveNatural);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// copy the points so that we can add extra straight pieces, else we get curves at the ends of the chart
|
// copy the points so that we can add extra straight pieces, else we get curves at the ends of the chart
|
||||||
|
|
@ -207,45 +252,53 @@ function showElevationProfile(data, routeLen, isRiver) {
|
||||||
const state = pack.cells.state[cell];
|
const state = pack.cells.state[cell];
|
||||||
let pop = pack.cells.pop[cell];
|
let pop = pack.cells.pop[cell];
|
||||||
if (chartData.burg[k]) {
|
if (chartData.burg[k]) {
|
||||||
pop += pack.burgs[chartData.burg[k]].population * urbanization.value;
|
pop += pack.burgs[chartData.burg[k]].population * urbanization;
|
||||||
}
|
}
|
||||||
|
|
||||||
const populationDesc = rn(pop * populationRate.value);
|
const populationDesc = rn(pop * populationRate);
|
||||||
|
|
||||||
const provinceDesc = province ? ", " + pack.provinces[province].name : "";
|
const provinceDesc = province ? ", " + pack.provinces[province].name : "";
|
||||||
const dataTip = biomesData.name[chartData.biome[k]] +
|
const dataTip = biomesData.name[chartData.biome[k]] + provinceDesc + ", " + pack.states[state].name + ", " + pack.religions[religion].name + ", " + pack.cultures[culture].name + " (height: " + chartData.height[k] + " " + hu + ", population " + populationDesc + ", cell " + chartData.cell[k] + ")";
|
||||||
provinceDesc +
|
|
||||||
", " + pack.states[state].name +
|
|
||||||
", " + pack.religions[religion].name +
|
|
||||||
", " + pack.cultures[culture].name +
|
|
||||||
" (height: " + chartData.height[k] + " " + hu + ", population " + populationDesc + ", cell " + chartData.cell[k] + ")";
|
|
||||||
|
|
||||||
g.append("rect").attr("stroke", c).attr("fill", c).attr("x", x).attr("y", y).attr("width", xscale(1)).attr("height", 15).attr("data-tip", dataTip);
|
g.append("rect").attr("stroke", c).attr("fill", c).attr("x", x).attr("y", y).attr("width", xscale(1)).attr("height", 15).attr("data-tip", dataTip);
|
||||||
}
|
}
|
||||||
|
|
||||||
const xAxis = d3.axisBottom(xscale).ticks(10).tickFormat(function(d){ return (rn(d / chartData.points.length * routeLen) + " " + distanceUnitInput.value);});
|
const xAxis = d3
|
||||||
const yAxis = d3.axisLeft(yscale).ticks(5).tickFormat(function(d) { return d + " " + hu; });
|
.axisBottom(xscale)
|
||||||
|
.ticks(10)
|
||||||
|
.tickFormat(function (d) {
|
||||||
|
return rn((d / chartData.points.length) * routeLen) + " " + distanceUnitInput.value;
|
||||||
|
});
|
||||||
|
const yAxis = d3
|
||||||
|
.axisLeft(yscale)
|
||||||
|
.ticks(5)
|
||||||
|
.tickFormat(function (d) {
|
||||||
|
return d + " " + hu;
|
||||||
|
});
|
||||||
|
|
||||||
const xGrid = d3.axisBottom(xscale).ticks(10).tickSize(-chartHeight).tickFormat("");
|
const xGrid = d3.axisBottom(xscale).ticks(10).tickSize(-chartHeight).tickFormat("");
|
||||||
const yGrid = d3.axisLeft(yscale).ticks(5).tickSize(-chartWidth).tickFormat("");
|
const yGrid = d3.axisLeft(yscale).ticks(5).tickSize(-chartWidth).tickFormat("");
|
||||||
|
|
||||||
chart.append("g")
|
chart
|
||||||
|
.append("g")
|
||||||
.attr("id", "epxaxis")
|
.attr("id", "epxaxis")
|
||||||
.attr("transform", "translate(" + xOffset + "," + parseInt(chartHeight + +yOffset + 20) + ")")
|
.attr("transform", "translate(" + xOffset + "," + parseInt(chartHeight + +yOffset + 20) + ")")
|
||||||
.call(xAxis)
|
.call(xAxis)
|
||||||
.selectAll("text")
|
.selectAll("text")
|
||||||
.style("text-anchor", "center")
|
.style("text-anchor", "center")
|
||||||
.attr("transform", function (d) {
|
.attr("transform", function (d) {
|
||||||
return "rotate(0)" // used to rotate labels, - anti-clockwise, + clockwise
|
return "rotate(0)"; // used to rotate labels, - anti-clockwise, + clockwise
|
||||||
});
|
});
|
||||||
|
|
||||||
chart.append("g")
|
chart
|
||||||
|
.append("g")
|
||||||
.attr("id", "epyaxis")
|
.attr("id", "epyaxis")
|
||||||
.attr("transform", "translate(" + parseInt(+xOffset - 10) + "," + parseInt(+yOffset) + ")")
|
.attr("transform", "translate(" + parseInt(+xOffset - 10) + "," + parseInt(+yOffset) + ")")
|
||||||
.call(yAxis);
|
.call(yAxis);
|
||||||
|
|
||||||
// add the X gridlines
|
// add the X gridlines
|
||||||
chart.append("g")
|
chart
|
||||||
|
.append("g")
|
||||||
.attr("id", "epxgrid")
|
.attr("id", "epxgrid")
|
||||||
.attr("class", "epgrid")
|
.attr("class", "epgrid")
|
||||||
.attr("stroke-dasharray", "4 1")
|
.attr("stroke-dasharray", "4 1")
|
||||||
|
|
@ -253,7 +306,8 @@ function showElevationProfile(data, routeLen, isRiver) {
|
||||||
.call(xGrid);
|
.call(xGrid);
|
||||||
|
|
||||||
// add the Y gridlines
|
// add the Y gridlines
|
||||||
chart.append("g")
|
chart
|
||||||
|
.append("g")
|
||||||
.attr("id", "epygrid")
|
.attr("id", "epygrid")
|
||||||
.attr("class", "epgrid")
|
.attr("class", "epgrid")
|
||||||
.attr("stroke-dasharray", "4 1")
|
.attr("stroke-dasharray", "4 1")
|
||||||
|
|
@ -277,11 +331,22 @@ function showElevationProfile(data, routeLen, isRiver) {
|
||||||
if (y1 >= yOffset) y1 = add;
|
if (y1 >= yOffset) y1 = add;
|
||||||
|
|
||||||
// burg name
|
// burg name
|
||||||
g.append("text").attr("id", "ep" + b).attr("class", "epburglabel").attr("x", x1).attr("y", y1).attr("text-anchor", "middle");
|
g.append("text")
|
||||||
|
.attr("id", "ep" + b)
|
||||||
|
.attr("class", "epburglabel")
|
||||||
|
.attr("x", x1)
|
||||||
|
.attr("y", y1)
|
||||||
|
.attr("text-anchor", "middle");
|
||||||
document.getElementById("ep" + b).innerHTML = pack.burgs[b].name;
|
document.getElementById("ep" + b).innerHTML = pack.burgs[b].name;
|
||||||
|
|
||||||
// arrow from burg name to graph line
|
// arrow from burg name to graph line
|
||||||
g.append("path").attr("id", "eparrow" + b).attr("d", "M" + x1.toString() + "," + (y1+3).toString() + "L" + x1.toString() + "," + parseInt(chartData.points[k][1]-3).toString()).attr("stroke", "darkgray").attr("fill", "lightgray").attr("stroke-width", "1").attr("marker-end", "url(#arrowhead)");
|
g.append("path")
|
||||||
|
.attr("id", "eparrow" + b)
|
||||||
|
.attr("d", "M" + x1.toString() + "," + (y1 + 3).toString() + "L" + x1.toString() + "," + parseInt(chartData.points[k][1] - 3).toString())
|
||||||
|
.attr("stroke", "darkgray")
|
||||||
|
.attr("fill", "lightgray")
|
||||||
|
.attr("stroke-width", "1")
|
||||||
|
.attr("marker-end", "url(#arrowhead)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,23 @@
|
||||||
// Module to store general UI functions
|
// Module to store general UI functions
|
||||||
"use strict";
|
'use strict';
|
||||||
|
|
||||||
// fit full-screen map if window is resized
|
// fit full-screen map if window is resized
|
||||||
$(window).resize(function (e) {
|
$(window).resize(function (e) {
|
||||||
if (localStorage.getItem("mapWidth") && localStorage.getItem("mapHeight")) return;
|
if (localStorage.getItem('mapWidth') && localStorage.getItem('mapHeight')) return;
|
||||||
mapWidthInput.value = window.innerWidth;
|
mapWidthInput.value = window.innerWidth;
|
||||||
mapHeightInput.value = window.innerHeight;
|
mapHeightInput.value = window.innerHeight;
|
||||||
changeMapSize();
|
changeMapSize();
|
||||||
});
|
});
|
||||||
|
|
||||||
window.onbeforeunload = () => "Are you sure you want to navigate away?";
|
window.onbeforeunload = () => 'Are you sure you want to navigate away?';
|
||||||
|
|
||||||
// Tooltips
|
// Tooltips
|
||||||
const tooltip = document.getElementById("tooltip");
|
const tooltip = document.getElementById('tooltip');
|
||||||
|
|
||||||
// show tip for non-svg elemets with data-tip
|
// show tip for non-svg elemets with data-tip
|
||||||
document.getElementById("dialogs").addEventListener("mousemove", showDataTip);
|
document.getElementById('dialogs').addEventListener('mousemove', showDataTip);
|
||||||
document.getElementById("optionsContainer").addEventListener("mousemove", showDataTip);
|
document.getElementById('optionsContainer').addEventListener('mousemove', showDataTip);
|
||||||
document.getElementById("exitCustomization").addEventListener("mousemove", showDataTip);
|
document.getElementById('exitCustomization').addEventListener('mousemove', showDataTip);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} tip Tooltip text
|
* @param {string} tip Tooltip text
|
||||||
|
|
@ -25,15 +25,15 @@ document.getElementById("exitCustomization").addEventListener("mousemove", showD
|
||||||
* @param {string} type Message type (color): error, warn, success
|
* @param {string} type Message type (color): error, warn, success
|
||||||
* @param {number} time Timeout to auto hide, ms
|
* @param {number} time Timeout to auto hide, ms
|
||||||
*/
|
*/
|
||||||
function tip(tip = "Tip is undefined", main, type, time) {
|
function tip(tip = 'Tip is undefined', main, type, time) {
|
||||||
tooltip.innerHTML = tip;
|
tooltip.innerHTML = tip;
|
||||||
tooltip.style.background = "linear-gradient(0.1turn, #ffffff00, #5e5c5c80, #ffffff00)";
|
tooltip.style.background = 'linear-gradient(0.1turn, #ffffff00, #5e5c5c80, #ffffff00)';
|
||||||
if (type === "error") tooltip.style.background = "linear-gradient(0.1turn, #ffffff00, #e11d1dcc, #ffffff00)"; else
|
if (type === 'error') tooltip.style.background = 'linear-gradient(0.1turn, #ffffff00, #e11d1dcc, #ffffff00)';
|
||||||
if (type === "warn") tooltip.style.background = "linear-gradient(0.1turn, #ffffff00, #be5d08cc, #ffffff00)"; else
|
else if (type === 'warn') tooltip.style.background = 'linear-gradient(0.1turn, #ffffff00, #be5d08cc, #ffffff00)';
|
||||||
if (type === "success") tooltip.style.background = "linear-gradient(0.1turn, #ffffff00, #127912cc, #ffffff00)";
|
else if (type === 'success') tooltip.style.background = 'linear-gradient(0.1turn, #ffffff00, #127912cc, #ffffff00)';
|
||||||
|
|
||||||
if (main) tooltip.dataset.main = tip; // set main tip
|
if (main) tooltip.dataset.main = tip; // set main tip
|
||||||
if (time) setTimeout(() => tooltip.dataset.main = "", time); // clear main in some time
|
if (time) setTimeout(() => (tooltip.dataset.main = ''), time); // clear main in some time
|
||||||
}
|
}
|
||||||
|
|
||||||
function showMainTip() {
|
function showMainTip() {
|
||||||
|
|
@ -41,8 +41,8 @@ function showMainTip() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearMainTip() {
|
function clearMainTip() {
|
||||||
tooltip.dataset.main = "";
|
tooltip.dataset.main = '';
|
||||||
tooltip.innerHTML = "";
|
tooltip.innerHTML = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// show tip at the bottom of the screen, consider possible translation
|
// show tip at the bottom of the screen, consider possible translation
|
||||||
|
|
@ -62,7 +62,8 @@ function mouseMove() {
|
||||||
if (i === undefined) return;
|
if (i === undefined) return;
|
||||||
showNotes(d3.event, i);
|
showNotes(d3.event, i);
|
||||||
const g = findGridCell(point[0], point[1]); // grid cell id
|
const g = findGridCell(point[0], point[1]); // grid cell id
|
||||||
if (tooltip.dataset.main) showMainTip(); else showMapTooltip(point, d3.event, i, g);
|
if (tooltip.dataset.main) showMainTip();
|
||||||
|
else showMapTooltip(point, d3.event, i, g);
|
||||||
if (cellInfo.offsetParent) updateCellInfo(point, i, g);
|
if (cellInfo.offsetParent) updateCellInfo(point, i, g);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,24 +71,24 @@ function mouseMove() {
|
||||||
function showNotes(e, i) {
|
function showNotes(e, i) {
|
||||||
if (notesEditor.offsetParent) return;
|
if (notesEditor.offsetParent) return;
|
||||||
let id = e.target.id || e.target.parentNode.id || e.target.parentNode.parentNode.id;
|
let id = e.target.id || e.target.parentNode.id || e.target.parentNode.parentNode.id;
|
||||||
if (e.target.parentNode.parentNode.id === "burgLabels") id = "burg" + e.target.dataset.id; else
|
if (e.target.parentNode.parentNode.id === 'burgLabels') id = 'burg' + e.target.dataset.id;
|
||||||
if (e.target.parentNode.parentNode.id === "burgIcons") id = "burg" + e.target.dataset.id;
|
else if (e.target.parentNode.parentNode.id === 'burgIcons') id = 'burg' + e.target.dataset.id;
|
||||||
|
|
||||||
const note = notes.find(note => note.id === id);
|
const note = notes.find((note) => note.id === id);
|
||||||
if (note !== undefined && note.legend !== "") {
|
if (note !== undefined && note.legend !== '') {
|
||||||
document.getElementById("notes").style.display = "block";
|
document.getElementById('notes').style.display = 'block';
|
||||||
document.getElementById("notesHeader").innerHTML = note.name;
|
document.getElementById('notesHeader').innerHTML = note.name;
|
||||||
document.getElementById("notesBody").innerHTML = note.legend;
|
document.getElementById('notesBody').innerHTML = note.legend;
|
||||||
} else if (!options.pinNotes) {
|
} else if (!options.pinNotes) {
|
||||||
document.getElementById("notes").style.display = "none";
|
document.getElementById('notes').style.display = 'none';
|
||||||
document.getElementById("notesHeader").innerHTML = "";
|
document.getElementById('notesHeader').innerHTML = '';
|
||||||
document.getElementById("notesBody").innerHTML = "";
|
document.getElementById('notesBody').innerHTML = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// show viewbox tooltip if main tooltip is blank
|
// show viewbox tooltip if main tooltip is blank
|
||||||
function showMapTooltip(point, e, i, g) {
|
function showMapTooltip(point, e, i, g) {
|
||||||
tip(""); // clear tip
|
tip(''); // clear tip
|
||||||
const path = e.composedPath ? e.composedPath() : getComposedPath(e.target); // apply polyfill
|
const path = e.composedPath ? e.composedPath() : getComposedPath(e.target); // apply polyfill
|
||||||
if (!path[path.length - 8]) return;
|
if (!path[path.length - 8]) return;
|
||||||
const group = path[path.length - 7].id;
|
const group = path[path.length - 7].id;
|
||||||
|
|
@ -95,16 +96,14 @@ function showMapTooltip(point, e, i, g) {
|
||||||
const land = pack.cells.h[i] >= 20;
|
const land = pack.cells.h[i] >= 20;
|
||||||
|
|
||||||
// specific elements
|
// specific elements
|
||||||
if (group === "armies") {
|
if (group === 'armies') {
|
||||||
tip(e.target.parentNode.dataset.name + ". Click to edit");
|
tip(e.target.parentNode.dataset.name + '. Click to edit');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (group === "emblems" && e.target.tagName === "use") {
|
if (group === 'emblems' && e.target.tagName === 'use') {
|
||||||
const parent = e.target.parentNode;
|
const parent = e.target.parentNode;
|
||||||
const [g, type] = parent.id === "burgEmblems" ? [pack.burgs, "burg"] :
|
const [g, type] = parent.id === 'burgEmblems' ? [pack.burgs, 'burg'] : parent.id === 'provinceEmblems' ? [pack.provinces, 'province'] : [pack.states, 'state'];
|
||||||
parent.id === "provinceEmblems" ? [pack.provinces, "province"] :
|
|
||||||
[pack.states, "state"];
|
|
||||||
const i = +e.target.dataset.i;
|
const i = +e.target.dataset.i;
|
||||||
if (event.shiftKey) highlightEmblemElement(type, g[i]);
|
if (event.shiftKey) highlightEmblemElement(type, g[i]);
|
||||||
|
|
||||||
|
|
@ -116,126 +115,168 @@ function showMapTooltip(point, e, i, g) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (group === "goods") {
|
if (group === 'goods') {
|
||||||
const id = +e.target.dataset.i;
|
const id = +e.target.dataset.i;
|
||||||
const resource = pack.resources.find(resource => resource.i === id);
|
const resource = pack.resources.find((resource) => resource.i === id);
|
||||||
tip("Resource: " + resource.name);
|
tip('Resource: ' + resource.name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (group === "rivers") {
|
if (group === 'rivers') {
|
||||||
const river = +e.target.id.slice(5);
|
const river = +e.target.id.slice(5);
|
||||||
const r = pack.rivers.find(r => r.i === river);
|
const r = pack.rivers.find((r) => r.i === river);
|
||||||
const name = r ? r.name + " " + r.type : "";
|
const name = r ? r.name + ' ' + r.type : '';
|
||||||
tip(name + ". Click to edit");
|
tip(name + '. Click to edit');
|
||||||
if (riversOverview.offsetParent) highlightEditorLine(riversOverview, river, 5000);
|
if (riversOverview.offsetParent) highlightEditorLine(riversOverview, river, 5000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (group === "routes") {tip("Click to edit the Route"); return;}
|
if (group === 'routes') {
|
||||||
if (group === "terrain") {tip("Click to edit the Relief Icon"); return;}
|
tip('Click to edit the Route');
|
||||||
if (subgroup === "burgLabels" || subgroup === "burgIcons") {
|
return;
|
||||||
|
}
|
||||||
|
if (group === 'terrain') {
|
||||||
|
tip('Click to edit the Relief Icon');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (subgroup === 'burgLabels' || subgroup === 'burgIcons') {
|
||||||
const burg = +path[path.length - 10].dataset.id;
|
const burg = +path[path.length - 10].dataset.id;
|
||||||
const b = pack.burgs[burg];
|
const b = pack.burgs[burg];
|
||||||
const population = si(b.population * populationRate.value * urbanization.value);
|
const population = si(b.population * populationRate * urbanization);
|
||||||
tip(`${b.name}. Population: ${population}. Click to edit`);
|
tip(`${b.name}. Population: ${population}. Click to edit`);
|
||||||
if (burgsOverview.offsetParent) highlightEditorLine(burgsOverview, burg, 5000);
|
if (burgsOverview.offsetParent) highlightEditorLine(burgsOverview, burg, 5000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (group === "labels") {tip("Click to edit the Label"); return;}
|
if (group === 'labels') {
|
||||||
if (group === "markers") {tip("Click to edit the Marker"); return;}
|
tip('Click to edit the Label');
|
||||||
if (group === "ruler") {
|
return;
|
||||||
const tag = e.target.tagName;
|
|
||||||
const className = e.target.getAttribute("class");
|
|
||||||
if (tag === "circle" && className === "edge") {tip("Drag to adjust. Hold Ctrl and drag to add a point. Click to remove the point"); return;}
|
|
||||||
if (tag === "circle" && className === "control") {tip("Drag to adjust. Hold Shifta and drag to keep axial direction. Click to remove the point"); return;}
|
|
||||||
if (tag === "circle") {tip("Drag to adjust the measurer"); return;}
|
|
||||||
if (tag === "polyline") {tip("Click on drag to add a control point"); return;}
|
|
||||||
if (tag === "path") {tip("Drag to move the measurer"); return;}
|
|
||||||
if (tag === "text") {tip("Drag to move, click to remove the measurer"); return;}
|
|
||||||
}
|
}
|
||||||
if (subgroup === "burgIcons") {tip("Click to edit the Burg"); return;}
|
if (group === 'markers') {
|
||||||
if (subgroup === "burgLabels") {tip("Click to edit the Burg"); return;}
|
tip('Click to edit the Marker');
|
||||||
if (group === "lakes" && !land) {
|
return;
|
||||||
|
}
|
||||||
|
if (group === 'ruler') {
|
||||||
|
const tag = e.target.tagName;
|
||||||
|
const className = e.target.getAttribute('class');
|
||||||
|
if (tag === 'circle' && className === 'edge') {
|
||||||
|
tip('Drag to adjust. Hold Ctrl and drag to add a point. Click to remove the point');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (tag === 'circle' && className === 'control') {
|
||||||
|
tip('Drag to adjust. Hold Shifta and drag to keep axial direction. Click to remove the point');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (tag === 'circle') {
|
||||||
|
tip('Drag to adjust the measurer');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (tag === 'polyline') {
|
||||||
|
tip('Click on drag to add a control point');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (tag === 'path') {
|
||||||
|
tip('Drag to move the measurer');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (tag === 'text') {
|
||||||
|
tip('Drag to move, click to remove the measurer');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (subgroup === 'burgIcons') {
|
||||||
|
tip('Click to edit the Burg');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (subgroup === 'burgLabels') {
|
||||||
|
tip('Click to edit the Burg');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (group === 'lakes' && !land) {
|
||||||
const lakeId = +e.target.dataset.f;
|
const lakeId = +e.target.dataset.f;
|
||||||
const name = pack.features[lakeId]?.name;
|
const name = pack.features[lakeId]?.name;
|
||||||
const fullName = subgroup === "freshwater" ? name : name + " " + subgroup;
|
const fullName = subgroup === 'freshwater' ? name : name + ' ' + subgroup;
|
||||||
tip(`${fullName} lake. Click to edit`); return;
|
tip(`${fullName} lake. Click to edit`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (group === "coastline") {tip("Click to edit the coastline"); return;}
|
if (group === 'coastline') {
|
||||||
if (group === "zones") {
|
tip('Click to edit the coastline');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (group === 'zones') {
|
||||||
const zone = path[path.length - 8];
|
const zone = path[path.length - 8];
|
||||||
tip(zone.dataset.description);
|
tip(zone.dataset.description);
|
||||||
if (zonesEditor.offsetParent) highlightEditorLine(zonesEditor, zone.id, 5000);
|
if (zonesEditor.offsetParent) highlightEditorLine(zonesEditor, zone.id, 5000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (group === "ice") {tip("Click to edit the Ice"); return;}
|
if (group === 'ice') {
|
||||||
|
tip('Click to edit the Ice');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// covering elements
|
// covering elements
|
||||||
if (layerIsOn("togglePrec") && land) tip("Annual Precipitation: "+ getFriendlyPrecipitation(i)); else
|
if (layerIsOn('togglePrec') && land) tip('Annual Precipitation: ' + getFriendlyPrecipitation(i));
|
||||||
if (layerIsOn("togglePopulation")) tip(getPopulationTip(i)); else
|
else if (layerIsOn('togglePopulation')) tip(getPopulationTip(i));
|
||||||
if (layerIsOn("toggleTemp")) tip("Temperature: " + convertTemperature(grid.cells.temp[g])); else
|
else if (layerIsOn('toggleTemp')) tip('Temperature: ' + convertTemperature(grid.cells.temp[g]));
|
||||||
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]);
|
||||||
if (biomesEditor.offsetParent) highlightEditorLine(biomesEditor, biome);
|
if (biomesEditor.offsetParent) highlightEditorLine(biomesEditor, biome);
|
||||||
} else
|
} else if (layerIsOn('toggleReligions') && pack.cells.religion[i]) {
|
||||||
if (layerIsOn("toggleReligions") && pack.cells.religion[i]) {
|
|
||||||
const religion = pack.cells.religion[i];
|
const religion = pack.cells.religion[i];
|
||||||
const r = pack.religions[religion];
|
const r = pack.religions[religion];
|
||||||
const type = r.type === "Cult" || r.type == "Heresy" ? r.type : r.type + " religion";
|
const type = r.type === 'Cult' || r.type == 'Heresy' ? r.type : r.type + ' religion';
|
||||||
tip(type + ": " + r.name);
|
tip(type + ': ' + r.name);
|
||||||
if (religionsEditor.offsetParent) highlightEditorLine(religionsEditor, religion);
|
if (religionsEditor.offsetParent) highlightEditorLine(religionsEditor, religion);
|
||||||
} else
|
} else if (pack.cells.state[i] && (layerIsOn('toggleProvinces') || layerIsOn('toggleStates'))) {
|
||||||
if (pack.cells.state[i] && (layerIsOn("toggleProvinces") || layerIsOn("toggleStates"))) {
|
|
||||||
const state = pack.cells.state[i];
|
const state = pack.cells.state[i];
|
||||||
const stateName = pack.states[state].fullName;
|
const stateName = pack.states[state].fullName;
|
||||||
const province = pack.cells.province[i];
|
const province = pack.cells.province[i];
|
||||||
const prov = province ? pack.provinces[province].fullName + ", " : "";
|
const prov = province ? pack.provinces[province].fullName + ', ' : '';
|
||||||
tip(prov + stateName);
|
tip(prov + stateName);
|
||||||
if (statesEditor.offsetParent) highlightEditorLine(statesEditor, state);
|
if (statesEditor.offsetParent) highlightEditorLine(statesEditor, state);
|
||||||
if (diplomacyEditor.offsetParent) highlightEditorLine(diplomacyEditor, state);
|
if (diplomacyEditor.offsetParent) highlightEditorLine(diplomacyEditor, state);
|
||||||
if (militaryOverview.offsetParent) highlightEditorLine(militaryOverview, state);
|
if (militaryOverview.offsetParent) highlightEditorLine(militaryOverview, state);
|
||||||
if (provincesEditor.offsetParent) highlightEditorLine(provincesEditor, province);
|
if (provincesEditor.offsetParent) highlightEditorLine(provincesEditor, province);
|
||||||
} else
|
} else if (layerIsOn('toggleCultures') && pack.cells.culture[i]) {
|
||||||
if (layerIsOn("toggleCultures") && pack.cells.culture[i]) {
|
|
||||||
const culture = pack.cells.culture[i];
|
const culture = pack.cells.culture[i];
|
||||||
tip("Culture: " + pack.cultures[culture].name);
|
tip('Culture: ' + pack.cultures[culture].name);
|
||||||
if (culturesEditor.offsetParent) highlightEditorLine(culturesEditor, culture);
|
if (culturesEditor.offsetParent) highlightEditorLine(culturesEditor, culture);
|
||||||
} else
|
} else if (layerIsOn('toggleHeight')) tip('Height: ' + getFriendlyHeight(point));
|
||||||
if (layerIsOn("toggleHeight")) tip("Height: " + getFriendlyHeight(point));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function highlightEditorLine(editor, id, timeout = 15000) {
|
function highlightEditorLine(editor, id, timeout = 15000) {
|
||||||
Array.from(editor.getElementsByClassName("states hovered")).forEach(el => el.classList.remove("hovered")); // clear all hovered
|
Array.from(editor.getElementsByClassName('states hovered')).forEach((el) => el.classList.remove('hovered')); // clear all hovered
|
||||||
const hovered = Array.from(editor.querySelectorAll("div")).find(el => el.dataset.id == id);
|
const hovered = Array.from(editor.querySelectorAll('div')).find((el) => el.dataset.id == id);
|
||||||
if (hovered) hovered.classList.add("hovered"); // add hovered class
|
if (hovered) hovered.classList.add('hovered'); // add hovered class
|
||||||
if (timeout) setTimeout(() => {hovered && hovered.classList.remove("hovered")}, timeout);
|
if (timeout)
|
||||||
|
setTimeout(() => {
|
||||||
|
hovered && hovered.classList.remove('hovered');
|
||||||
|
}, timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
// get cell info on mouse move
|
// get cell info on mouse move
|
||||||
function updateCellInfo(point, i, g) {
|
function updateCellInfo(point, i, g) {
|
||||||
const cells = pack.cells;
|
const cells = pack.cells;
|
||||||
const x = infoX.innerHTML = rn(point[0]);
|
const x = (infoX.innerHTML = rn(point[0]));
|
||||||
const y = infoY.innerHTML = rn(point[1]);
|
const y = (infoY.innerHTML = rn(point[1]));
|
||||||
const f = cells.f[i];
|
const f = cells.f[i];
|
||||||
infoLat.innerHTML = toDMS(mapCoordinates.latN - (y / graphHeight) * mapCoordinates.latT, "lat");
|
infoLat.innerHTML = toDMS(mapCoordinates.latN - (y / graphHeight) * mapCoordinates.latT, 'lat');
|
||||||
infoLon.innerHTML = toDMS(mapCoordinates.lonW + (x / graphWidth) * mapCoordinates.lonT, "lon");
|
infoLon.innerHTML = toDMS(mapCoordinates.lonW + (x / graphWidth) * mapCoordinates.lonT, 'lon');
|
||||||
|
|
||||||
infoCell.innerHTML = i;
|
infoCell.innerHTML = i;
|
||||||
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
|
const unit = areaUnit.value === 'square' ? ' ' + distanceUnitInput.value + '²' : ' ' + areaUnit.value;
|
||||||
infoArea.innerHTML = cells.area[i] ? si(cells.area[i] * distanceScaleInput.value ** 2) + unit : "n/a";
|
infoArea.innerHTML = cells.area[i] ? si(cells.area[i] * distanceScaleInput.value ** 2) + unit : 'n/a';
|
||||||
infoEvelation.innerHTML = getElevation(pack.features[f], pack.cells.h[i]);
|
infoEvelation.innerHTML = getElevation(pack.features[f], pack.cells.h[i]);
|
||||||
infoDepth.innerHTML = getDepth(pack.features[f], pack.cells.h[i], point);
|
infoDepth.innerHTML = getDepth(pack.features[f], pack.cells.h[i], 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';
|
||||||
infoRiver.innerHTML = cells.h[i] >= 20 && cells.r[i] ? getRiverInfo(cells.r[i]) : "no";
|
infoRiver.innerHTML = cells.h[i] >= 20 && cells.r[i] ? getRiverInfo(cells.r[i]) : 'no';
|
||||||
infoState.innerHTML = cells.h[i] >= 20 ? cells.state[i] ? `${pack.states[cells.state[i]].fullName} (${cells.state[i]})` : "neutral lands (0)" : "no";
|
infoState.innerHTML = cells.h[i] >= 20 ? (cells.state[i] ? `${pack.states[cells.state[i]].fullName} (${cells.state[i]})` : 'neutral lands (0)') : 'no';
|
||||||
infoProvince.innerHTML = cells.province[i] ? `${pack.provinces[cells.province[i]].fullName} (${cells.province[i]})` : "no";
|
infoProvince.innerHTML = cells.province[i] ? `${pack.provinces[cells.province[i]].fullName} (${cells.province[i]})` : 'no';
|
||||||
infoCulture.innerHTML = cells.culture[i] ? `${pack.cultures[cells.culture[i]].name} (${cells.culture[i]})` : "no";
|
infoCulture.innerHTML = cells.culture[i] ? `${pack.cultures[cells.culture[i]].name} (${cells.culture[i]})` : 'no';
|
||||||
infoReligion.innerHTML = cells.religion[i] ? `${pack.religions[cells.religion[i]].name} (${cells.religion[i]})` : "no";
|
infoReligion.innerHTML = cells.religion[i] ? `${pack.religions[cells.religion[i]].name} (${cells.religion[i]})` : 'no';
|
||||||
infoPopulation.innerHTML = getFriendlyPopulation(i);
|
infoPopulation.innerHTML = getFriendlyPopulation(i);
|
||||||
infoBurg.innerHTML = cells.burg[i] ? pack.burgs[cells.burg[i]].name + " (" + cells.burg[i] + ")" : "no";
|
infoBurg.innerHTML = cells.burg[i] ? pack.burgs[cells.burg[i]].name + ' (' + cells.burg[i] + ')' : 'no';
|
||||||
infoFeature.innerHTML = f ? pack.features[f].group + " (" + f + ")" : "n/a";
|
infoFeature.innerHTML = f ? pack.features[f].group + ' (' + f + ')' : 'n/a';
|
||||||
infoBiome.innerHTML = biomesData.name[cells.biome[i]];
|
infoBiome.innerHTML = biomesData.name[cells.biome[i]];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -245,23 +286,29 @@ function toDMS(coord, c) {
|
||||||
const minutesNotTruncated = (Math.abs(coord) - degrees) * 60;
|
const minutesNotTruncated = (Math.abs(coord) - degrees) * 60;
|
||||||
const minutes = Math.floor(minutesNotTruncated);
|
const minutes = Math.floor(minutesNotTruncated);
|
||||||
const seconds = Math.floor((minutesNotTruncated - minutes) * 60);
|
const seconds = Math.floor((minutesNotTruncated - minutes) * 60);
|
||||||
const cardinal = c === "lat" ? coord >= 0 ? "N" : "S" : coord >= 0 ? "E" : "W";
|
const cardinal = c === 'lat' ? (coord >= 0 ? 'N' : 'S') : coord >= 0 ? 'E' : 'W';
|
||||||
return degrees + "° " + minutes + "′ " + seconds + "″ " + cardinal;
|
return degrees + '° ' + minutes + '′ ' + seconds + '″ ' + cardinal;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get surface elevation
|
// get surface elevation
|
||||||
function getElevation(f, h) {
|
function getElevation(f, h) {
|
||||||
if (f.land) return getHeight(h) + " (" + h + ")"; // land: usual height
|
if (f.land) return getHeight(h) + ' (' + h + ')'; // land: usual height
|
||||||
if (f.border) return "0 " + heightUnit.value; // ocean: 0
|
if (f.border) return '0 ' + heightUnit.value; // ocean: 0
|
||||||
if (f.type === "lake") return getHeight(f.height) + " (" + f.height + ")"; // lake: defined on river generation
|
if (f.type === 'lake') return getHeight(f.height) + ' (' + f.height + ')'; // lake: defined on river generation
|
||||||
}
|
}
|
||||||
|
|
||||||
// get water depth
|
// get water depth
|
||||||
function getDepth(f, h, p) {
|
function getDepth(f, h, p) {
|
||||||
if (f.land) return "0 " + heightUnit.value; // land: 0
|
if (f.land) return '0 ' + heightUnit.value; // land: 0
|
||||||
if (!f.border) return getHeight(h, "abs"); // lake: pack abs height
|
|
||||||
|
// lake: difference between surface and bottom
|
||||||
const gridH = grid.cells.h[findGridCell(p[0], p[1])];
|
const gridH = grid.cells.h[findGridCell(p[0], p[1])];
|
||||||
return getHeight(gridH, "abs"); // ocean: grig height
|
if (f.type === 'lake') {
|
||||||
|
const depth = gridH === 19 ? f.height / 2 : gridH;
|
||||||
|
return getHeight(depth, 'abs');
|
||||||
|
}
|
||||||
|
|
||||||
|
return getHeight(gridH, 'abs'); // ocean: grid height
|
||||||
}
|
}
|
||||||
|
|
||||||
// get user-friendly (real-world) height value from map data
|
// get user-friendly (real-world) height value from map data
|
||||||
|
|
@ -275,31 +322,32 @@ function getFriendlyHeight(p) {
|
||||||
function getHeight(h, abs) {
|
function getHeight(h, abs) {
|
||||||
const unit = heightUnit.value;
|
const unit = heightUnit.value;
|
||||||
let unitRatio = 3.281; // default calculations are in feet
|
let unitRatio = 3.281; // default calculations are in feet
|
||||||
if (unit === "m") unitRatio = 1; // if meter
|
if (unit === 'm') unitRatio = 1;
|
||||||
else if (unit === "f") unitRatio = 0.5468; // if fathom
|
// if meter
|
||||||
|
else if (unit === 'f') unitRatio = 0.5468; // if fathom
|
||||||
|
|
||||||
let height = -990;
|
let height = -990;
|
||||||
if (h >= 20) height = Math.pow(h - 18, +heightExponentInput.value);
|
if (h >= 20) height = Math.pow(h - 18, +heightExponentInput.value);
|
||||||
else if (h < 20 && h > 0) height = (h - 20) / h * 50;
|
else if (h < 20 && h > 0) height = ((h - 20) / h) * 50;
|
||||||
|
|
||||||
if (abs) height = Math.abs(height);
|
if (abs) height = Math.abs(height);
|
||||||
return rn(height * unitRatio) + " " + unit;
|
return rn(height * unitRatio) + ' ' + unit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get user-friendly (real-world) precipitation value from map data
|
// get user-friendly (real-world) precipitation value from map data
|
||||||
function getFriendlyPrecipitation(i) {
|
function getFriendlyPrecipitation(i) {
|
||||||
const prec = grid.cells.prec[pack.cells.g[i]];
|
const prec = grid.cells.prec[pack.cells.g[i]];
|
||||||
return prec * 100 + " mm";
|
return prec * 100 + ' mm';
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRiverInfo(id) {
|
function getRiverInfo(id) {
|
||||||
const r = pack.rivers.find(r => r.i == id);
|
const r = pack.rivers.find((r) => r.i == id);
|
||||||
return r ? `${r.name} ${r.type} (${id})` : "n/a";
|
return r ? `${r.name} ${r.type} (${id})` : 'n/a';
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCellPopulation(i) {
|
function getCellPopulation(i) {
|
||||||
const rural = pack.cells.pop[i] * populationRate.value;
|
const rural = pack.cells.pop[i] * populationRate;
|
||||||
const urban = pack.cells.burg[i] ? pack.burgs[pack.cells.burg[i]].population * populationRate.value * urbanization.value : 0;
|
const urban = pack.cells.burg[i] ? pack.burgs[pack.cells.burg[i]].population * populationRate * urbanization : 0;
|
||||||
return [rural, urban];
|
return [rural, urban];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -315,41 +363,72 @@ function getPopulationTip(i) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function highlightEmblemElement(type, el) {
|
function highlightEmblemElement(type, el) {
|
||||||
const i = el.i, cells = pack.cells;
|
const i = el.i,
|
||||||
|
cells = pack.cells;
|
||||||
const animation = d3.transition().duration(1000).ease(d3.easeSinIn);
|
const animation = d3.transition().duration(1000).ease(d3.easeSinIn);
|
||||||
|
|
||||||
if (type === "burg") {
|
if (type === 'burg') {
|
||||||
const {x, y} = el;
|
const {x, y} = el;
|
||||||
debug.append("circle").attr("cx", x).attr("cy", y).attr("r", 0)
|
debug
|
||||||
.attr("fill", "none").attr("stroke", "#d0240f").attr("stroke-width", 1).attr("opacity", 1)
|
.append('circle')
|
||||||
.transition(animation).attr("r", 20).attr("opacity", .1).attr("stroke-width", 0).remove();
|
.attr('cx', x)
|
||||||
|
.attr('cy', y)
|
||||||
|
.attr('r', 0)
|
||||||
|
.attr('fill', 'none')
|
||||||
|
.attr('stroke', '#d0240f')
|
||||||
|
.attr('stroke-width', 1)
|
||||||
|
.attr('opacity', 1)
|
||||||
|
.transition(animation)
|
||||||
|
.attr('r', 20)
|
||||||
|
.attr('opacity', 0.1)
|
||||||
|
.attr('stroke-width', 0)
|
||||||
|
.remove();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [x, y] = el.pole || pack.cells.p[el.center];
|
const [x, y] = el.pole || pack.cells.p[el.center];
|
||||||
const obj = type === "state" ? cells.state : cells.province;
|
const obj = type === 'state' ? cells.state : cells.province;
|
||||||
const borderCells = cells.i.filter(id => obj[id] === i && cells.c[id].some(n => obj[n] !== i));
|
const borderCells = cells.i.filter((id) => obj[id] === i && cells.c[id].some((n) => obj[n] !== i));
|
||||||
const data = Array.from(borderCells).filter((c, i) => !(i%2)).map(i => cells.p[i]).map(i => [i[0], i[1], Math.hypot(i[0]-x, i[1]-y)]);
|
const data = Array.from(borderCells)
|
||||||
|
.filter((c, i) => !(i % 2))
|
||||||
|
.map((i) => cells.p[i])
|
||||||
|
.map((i) => [i[0], i[1], Math.hypot(i[0] - x, i[1] - y)]);
|
||||||
|
|
||||||
debug.selectAll("line").data(data).enter().append("line")
|
debug
|
||||||
.attr("x1", x).attr("y1", y).attr("x2", d => d[0]).attr("y2", d => d[1])
|
.selectAll('line')
|
||||||
.attr("stroke", "#d0240f").attr("stroke-width", .5).attr("opacity", .2)
|
.data(data)
|
||||||
.attr("stroke-dashoffset", d => d[2]).attr("stroke-dasharray", d => d[2])
|
.enter()
|
||||||
.transition(animation).attr("stroke-dashoffset", 0).attr("opacity", 1)
|
.append('line')
|
||||||
.transition(animation).delay(1000).attr("stroke-dashoffset", d => d[2]).attr("opacity", 0).remove();
|
.attr('x1', x)
|
||||||
|
.attr('y1', y)
|
||||||
|
.attr('x2', (d) => d[0])
|
||||||
|
.attr('y2', (d) => d[1])
|
||||||
|
.attr('stroke', '#d0240f')
|
||||||
|
.attr('stroke-width', 0.5)
|
||||||
|
.attr('opacity', 0.2)
|
||||||
|
.attr('stroke-dashoffset', (d) => d[2])
|
||||||
|
.attr('stroke-dasharray', (d) => d[2])
|
||||||
|
.transition(animation)
|
||||||
|
.attr('stroke-dashoffset', 0)
|
||||||
|
.attr('opacity', 1)
|
||||||
|
.transition(animation)
|
||||||
|
.delay(1000)
|
||||||
|
.attr('stroke-dashoffset', (d) => d[2])
|
||||||
|
.attr('opacity', 0)
|
||||||
|
.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 (event) {
|
||||||
if (this.className === "icon-lock") tip("Click to unlock the option and allow it to be randomized on new map generation");
|
if (this.className === 'icon-lock') 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();
|
event.stopPropagation();
|
||||||
});
|
});
|
||||||
|
|
||||||
e.addEventListener("click", function() {
|
e.addEventListener('click', function () {
|
||||||
const id = (this.id).slice(5);
|
const id = this.id.slice(5);
|
||||||
if (this.className === "icon-lock") unlock(id);
|
if (this.className === 'icon-lock') unlock(id);
|
||||||
else lock(id);
|
else lock(id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -358,24 +437,24 @@ document.querySelectorAll("[data-locked]").forEach(function(e) {
|
||||||
function lock(id) {
|
function lock(id) {
|
||||||
const input = document.querySelector("[data-stored='" + id + "']");
|
const input = document.querySelector("[data-stored='" + id + "']");
|
||||||
if (input) localStorage.setItem(id, input.value);
|
if (input) localStorage.setItem(id, input.value);
|
||||||
const el = document.getElementById("lock_" + id);
|
const el = document.getElementById('lock_' + id);
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
el.dataset.locked = 1;
|
el.dataset.locked = 1;
|
||||||
el.className = "icon-lock";
|
el.className = 'icon-lock';
|
||||||
}
|
}
|
||||||
|
|
||||||
// unlock option
|
// unlock option
|
||||||
function unlock(id) {
|
function unlock(id) {
|
||||||
localStorage.removeItem(id);
|
localStorage.removeItem(id);
|
||||||
const el = document.getElementById("lock_" + id);
|
const el = document.getElementById('lock_' + id);
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
el.dataset.locked = 0;
|
el.dataset.locked = 0;
|
||||||
el.className = "icon-lock-open";
|
el.className = 'icon-lock-open';
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if option is locked
|
// check if option is locked
|
||||||
function locked(id) {
|
function locked(id) {
|
||||||
const lockEl = document.getElementById("lock_" + id);
|
const lockEl = document.getElementById('lock_' + id);
|
||||||
return lockEl.dataset.locked == 1;
|
return lockEl.dataset.locked == 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -385,16 +464,16 @@ function stored(option) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// assign skeaker behaviour
|
// assign skeaker behaviour
|
||||||
Array.from(document.getElementsByClassName("speaker")).forEach(el => {
|
Array.from(document.getElementsByClassName('speaker')).forEach((el) => {
|
||||||
const input = el.previousElementSibling;
|
const input = el.previousElementSibling;
|
||||||
el.addEventListener("click", () => speak(input.value));
|
el.addEventListener('click', () => speak(input.value));
|
||||||
});
|
});
|
||||||
|
|
||||||
function speak(text) {
|
function speak(text) {
|
||||||
const speaker = new SpeechSynthesisUtterance(text);
|
const speaker = new SpeechSynthesisUtterance(text);
|
||||||
const voices = speechSynthesis.getVoices();
|
const voices = speechSynthesis.getVoices();
|
||||||
if (voices.length) {
|
if (voices.length) {
|
||||||
const voiceId = +document.getElementById("speakerVoice").value;
|
const voiceId = +document.getElementById('speakerVoice').value;
|
||||||
speaker.voice = voices[voiceId];
|
speaker.voice = voices[voiceId];
|
||||||
}
|
}
|
||||||
speechSynthesis.speak(speaker);
|
speechSynthesis.speak(speaker);
|
||||||
|
|
@ -402,21 +481,21 @@ function speak(text) {
|
||||||
|
|
||||||
// apply drop-down menu option. If the value is not in options, add it
|
// apply drop-down menu option. If the value is not in options, add it
|
||||||
function applyOption(select, id, name = id) {
|
function applyOption(select, id, name = id) {
|
||||||
const custom = !Array.from(select.options).some(o => o.value == id);
|
const custom = !Array.from(select.options).some((o) => o.value == id);
|
||||||
if (custom) select.options.add(new Option(name, id));
|
if (custom) select.options.add(new Option(name, id));
|
||||||
select.value = id;
|
select.value = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// show info about the generator in a popup
|
// show info about the generator in a popup
|
||||||
function showInfo() {
|
function showInfo() {
|
||||||
const Discord = link("https://discordapp.com/invite/X7E84HU", "Discord");
|
const Discord = link('https://discordapp.com/invite/X7E84HU', 'Discord');
|
||||||
const Reddit = link("https://www.reddit.com/r/FantasyMapGenerator", "Reddit")
|
const Reddit = link('https://www.reddit.com/r/FantasyMapGenerator', 'Reddit');
|
||||||
const Patreon = link("https://www.patreon.com/azgaar", "Patreon");
|
const Patreon = link('https://www.patreon.com/azgaar', 'Patreon');
|
||||||
const Trello = link("https://trello.com/b/7x832DG4/fantasy-map-generator", "Trello");
|
const Trello = link('https://trello.com/b/7x832DG4/fantasy-map-generator', 'Trello');
|
||||||
const Armoria = link("https://azgaar.github.io/Armoria", "Armoria");
|
const Armoria = link('https://azgaar.github.io/Armoria', 'Armoria');
|
||||||
|
|
||||||
const QuickStart = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Quick-Start-Tutorial", "Quick start tutorial");
|
const QuickStart = link('https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Quick-Start-Tutorial', 'Quick start tutorial');
|
||||||
const QAA = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Q&A", "Q&A page");
|
const QAA = link('https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Q&A', 'Q&A page');
|
||||||
|
|
||||||
alertMessage.innerHTML = `
|
alertMessage.innerHTML = `
|
||||||
<b>Fantasy Map Generator</b> (FMG) is an open-source application, it means the code is published an anyone can use it.
|
<b>Fantasy Map Generator</b> (FMG) is an open-source application, it means the code is published an anyone can use it.
|
||||||
|
|
@ -434,32 +513,39 @@ function showInfo() {
|
||||||
|
|
||||||
<b>Links:</b>
|
<b>Links:</b>
|
||||||
<ul style="columns:2">
|
<ul style="columns:2">
|
||||||
<li>${link("https://github.com/Azgaar/Fantasy-Map-Generator", "GitHub repository")}</li>
|
<li>${link('https://github.com/Azgaar/Fantasy-Map-Generator', 'GitHub repository')}</li>
|
||||||
<li>${link("https://github.com/Azgaar/Fantasy-Map-Generator/blob/master/LICENSE", "License")}</li>
|
<li>${link('https://github.com/Azgaar/Fantasy-Map-Generator/blob/master/LICENSE', 'License')}</li>
|
||||||
<li>${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "Changelog")}</li>
|
<li>${link('https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog', 'Changelog')}</li>
|
||||||
<li>${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys", "Hotkeys")}</li>
|
<li>${link('https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys', 'Hotkeys')}</li>
|
||||||
</ul>`;
|
</ul>`;
|
||||||
|
|
||||||
$("#alert").dialog({resizable: false, title: document.title, width: "28em",
|
$('#alert').dialog({
|
||||||
buttons: {OK: function() {$(this).dialog("close");}},
|
resizable: false,
|
||||||
position: {my: "center", at: "center", of: "svg"}
|
title: document.title,
|
||||||
|
width: '28em',
|
||||||
|
buttons: {
|
||||||
|
OK: function () {
|
||||||
|
$(this).dialog('close');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
position: {my: 'center', at: 'center', of: 'svg'}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// prevent default browser behavior for FMG-used hotkeys
|
// prevent default browser behavior for FMG-used hotkeys
|
||||||
document.addEventListener("keydown", event => {
|
document.addEventListener('keydown', (event) => {
|
||||||
if (event.altKey && event.keyCode !== 18) event.preventDefault(); // disallow alt key combinations
|
if (event.altKey && event.keyCode !== 18) event.preventDefault(); // disallow alt key combinations
|
||||||
if (event.ctrlKey && event.code === "KeyS") event.preventDefault(); // disallow CTRL + C
|
if (event.ctrlKey && event.code === 'KeyS') event.preventDefault(); // disallow CTRL + C
|
||||||
if ([112, 113, 117, 120, 9].includes(event.keyCode)) event.preventDefault(); // F1, F2, F6, F9, Tab
|
if ([112, 113, 117, 120, 9].includes(event.keyCode)) event.preventDefault(); // F1, F2, F6, F9, Tab
|
||||||
});
|
});
|
||||||
|
|
||||||
// Hotkeys, see github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys
|
// Hotkeys, see github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys
|
||||||
document.addEventListener("keyup", event => {
|
document.addEventListener('keyup', (event) => {
|
||||||
if (!window.closeDialogs) return; // not all modules are loaded
|
if (!window.closeDialogs) return; // not all modules are loaded
|
||||||
const canvas3d = document.getElementById("canvas3d"); // check if 3d mode is active
|
const canvas3d = document.getElementById('canvas3d'); // check if 3d mode is active
|
||||||
const active = document.activeElement.tagName;
|
const active = document.activeElement.tagName;
|
||||||
if (active === "INPUT" || active === "SELECT" || active === "TEXTAREA") return; // don't trigger if user inputs a text
|
if (active === 'INPUT' || active === 'SELECT' || active === 'TEXTAREA') return; // don't trigger if user inputs a text
|
||||||
if (active === "DIV" && document.activeElement.contentEditable === "true") return; // don't trigger if user inputs a text
|
if (active === 'DIV' && document.activeElement.contentEditable === 'true') return; // don't trigger if user inputs a text
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
const key = event.keyCode;
|
const key = event.keyCode;
|
||||||
|
|
@ -467,95 +553,174 @@ document.addEventListener("keyup", event => {
|
||||||
const shift = event.shiftKey || key === 16;
|
const shift = event.shiftKey || key === 16;
|
||||||
const alt = event.altKey || key === 18;
|
const alt = event.altKey || key === 18;
|
||||||
|
|
||||||
if (key === 112) showInfo(); // "F1" to show info
|
if (key === 112) showInfo();
|
||||||
else if (key === 113) regeneratePrompt(); // "F2" for new map
|
// "F1" to show info
|
||||||
else if (key === 113) regeneratePrompt(); // "F2" for a new map
|
else if (key === 113) regeneratePrompt();
|
||||||
else if (key === 117) quickSave(); // "F6" for quick save
|
// "F2" for new map
|
||||||
else if (key === 120) quickLoad(); // "F9" for quick load
|
else if (key === 113) regeneratePrompt();
|
||||||
else if (key === 9) toggleOptions(event); // Tab to toggle options
|
// "F2" for a new map
|
||||||
else if (key === 27) {closeDialogs(); hideOptions();} // Escape to close all dialogs
|
else if (key === 117) quickSave();
|
||||||
else if (key === 46) removeElementOnKey(); // "Delete" to remove the selected element
|
// "F6" for quick save
|
||||||
else if (key === 79 && canvas3d) toggle3dOptions(); // "O" to toggle 3d options
|
else if (key === 120) quickLoad();
|
||||||
|
// "F9" for quick load
|
||||||
else if (ctrl && key === 81) toggleSaveReminder(); // Ctrl + "Q" to toggle save reminder
|
else if (key === 9) toggleOptions(event);
|
||||||
else if (ctrl && key === 83) saveMap(); // Ctrl + "S" to save .map file
|
// Tab to toggle options
|
||||||
else if (undo.offsetParent && ctrl && key === 90) undo.click(); // Ctrl + "Z" to undo
|
else if (key === 27) {
|
||||||
else if (redo.offsetParent && ctrl && key === 89) redo.click(); // Ctrl + "Y" to redo
|
closeDialogs();
|
||||||
|
hideOptions();
|
||||||
else if (shift && key === 72) editHeightmap(); // Shift + "H" to edit Heightmap
|
} // Escape to close all dialogs
|
||||||
else if (shift && key === 66) editBiomes(); // Shift + "B" to edit Biomes
|
else if (key === 46) removeElementOnKey();
|
||||||
else if (shift && key === 83) editStates(); // Shift + "S" to edit States
|
// "Delete" to remove the selected element
|
||||||
else if (shift && key === 80) editProvinces(); // Shift + "P" to edit Provinces
|
else if (key === 79 && canvas3d) toggle3dOptions();
|
||||||
else if (shift && key === 68) editDiplomacy(); // Shift + "D" to edit Diplomacy
|
// "O" to toggle 3d options
|
||||||
else if (shift && key === 67) editCultures(); // Shift + "C" to edit Cultures
|
else if (ctrl && key === 81) toggleSaveReminder();
|
||||||
else if (shift && key === 78) editNamesbase(); // Shift + "N" to edit Namesbase
|
// Ctrl + "Q" to toggle save reminder
|
||||||
else if (shift && key === 90) editZones(); // Shift + "Z" to edit Zones
|
else if (ctrl && key === 83) saveMap();
|
||||||
else if (shift && key === 82) editReligions(); // Shift + "R" to edit Religions
|
// Ctrl + "S" to save .map file
|
||||||
else if (shift && key === 81) editResources(); // Shift + "Q" to edit Resources
|
else if (undo.offsetParent && ctrl && key === 90) undo.click();
|
||||||
else if (shift && key === 89) openEmblemEditor(); // Shift + "Y" to edit Emblems
|
// Ctrl + "Z" to undo
|
||||||
else if (shift && key === 87) editUnits(); // Shift + "W" to edit Units
|
else if (redo.offsetParent && ctrl && key === 89) redo.click();
|
||||||
else if (shift && key === 79) editNotes(); // Shift + "O" to edit Notes
|
// Ctrl + "Y" to redo
|
||||||
else if (shift && key === 84) overviewBurgs(); // Shift + "T" to open Burgs overview
|
else if (shift && key === 72) editHeightmap();
|
||||||
else if (shift && key === 86) overviewRivers(); // Shift + "V" to open Rivers overview
|
// Shift + "H" to edit Heightmap
|
||||||
else if (shift && key === 77) overviewMilitary(); // Shift + "M" to open Military overview
|
else if (shift && key === 66) editBiomes();
|
||||||
else if (shift && key === 69) viewCellDetails(); // Shift + "E" to open Cell Details
|
// Shift + "B" to edit Biomes
|
||||||
|
else if (shift && key === 83) editStates();
|
||||||
else if (shift && key === 49) toggleAddBurg(); // Shift + "1" to click to add Burg
|
// Shift + "S" to edit States
|
||||||
else if (shift && key === 50) toggleAddLabel(); // Shift + "2" to click to add Label
|
else if (shift && key === 80) editProvinces();
|
||||||
else if (shift && key === 51) toggleAddRiver(); // Shift + "3" to click to add River
|
// Shift + "P" to edit Provinces
|
||||||
else if (shift && key === 52) toggleAddRoute(); // Shift + "4" to click to add Route
|
else if (shift && key === 68) editDiplomacy();
|
||||||
else if (shift && key === 53) toggleAddMarker(); // Shift + "5" to click to add Marker
|
// Shift + "D" to edit Diplomacy
|
||||||
|
else if (shift && key === 67) editCultures();
|
||||||
else if (alt && key === 66) console.table(pack.burgs); // Alt + "B" to log burgs data
|
// Shift + "C" to edit Cultures
|
||||||
else if (alt && key === 83) console.table(pack.states); // Alt + "S" to log states data
|
else if (shift && key === 78) editNamesbase();
|
||||||
else if (alt && key === 67) console.table(pack.cultures); // Alt + "C" to log cultures data
|
// Shift + "N" to edit Namesbase
|
||||||
else if (alt && key === 82) console.table(pack.religions); // Alt + "R" to log religions data
|
else if (shift && key === 90) editZones();
|
||||||
else if (alt && key === 70) console.table(pack.features); // Alt + "F" to log features data
|
// Shift + "Z" to edit Zones
|
||||||
|
else if (shift && key === 82) editReligions();
|
||||||
else if (key === 88) toggleTexture(); // "X" to toggle Texture layer
|
// Shift + "R" to edit Religions
|
||||||
else if (key === 72) toggleHeight(); // "H" to toggle Heightmap layer
|
else if (shift && key === 81) editResources();
|
||||||
else if (key === 66) toggleBiomes(); // "B" to toggle Biomes layer
|
// Shift + "Q" to edit Resources
|
||||||
else if (key === 69) toggleCells(); // "E" to toggle Cells layer
|
else if (shift && key === 89) openEmblemEditor();
|
||||||
else if (key === 71) toggleGrid(); // "G" to toggle Grid layer
|
// Shift + "Y" to edit Emblems
|
||||||
else if (key === 79) toggleCoordinates(); // "O" to toggle Coordinates layer
|
else if (shift && key === 87) editUnits();
|
||||||
else if (key === 87) toggleCompass(); // "W" to toggle Compass Rose layer
|
// Shift + "W" to edit Units
|
||||||
else if (key === 86) toggleRivers(); // "V" to toggle Rivers layer
|
else if (shift && key === 79) editNotes();
|
||||||
else if (key === 70) toggleRelief(); // "F" to toggle Relief icons layer
|
// Shift + "O" to edit Notes
|
||||||
else if (key === 67) toggleCultures(); // "C" to toggle Cultures layer
|
else if (shift && key === 84) overviewBurgs();
|
||||||
else if (key === 83) toggleStates(); // "S" to toggle States layer
|
// Shift + "T" to open Burgs overview
|
||||||
else if (key === 80) toggleProvinces(); // "P" to toggle Provinces layer
|
else if (shift && key === 86) overviewRivers();
|
||||||
else if (key === 90) toggleZones(); // "Z" to toggle Zones
|
// Shift + "V" to open Rivers overview
|
||||||
else if (key === 68) toggleBorders(); // "D" to toggle Borders layer
|
else if (shift && key === 77) overviewMilitary();
|
||||||
else if (key === 82) toggleReligions(); // "R" to toggle Religions layer
|
// Shift + "M" to open Military overview
|
||||||
else if (key === 85) toggleRoutes(); // "U" to toggle Routes layer
|
else if (shift && key === 69) viewCellDetails();
|
||||||
else if (key === 84) toggleTemp(); // "T" to toggle Temperature layer
|
// Shift + "E" to open Cell Details
|
||||||
else if (key === 78) togglePopulation(); // "N" to toggle Population layer
|
else if (shift && key === 49) toggleAddBurg();
|
||||||
else if (key === 74) toggleIce(); // "J" to toggle Ice layer
|
// Shift + "1" to click to add Burg
|
||||||
else if (key === 65) togglePrec(); // "A" to toggle Precipitation layer
|
else if (shift && key === 50) toggleAddLabel();
|
||||||
else if (key === 81) toggleResources(); // "Q" to toggle Resources layer
|
// Shift + "2" to click to add Label
|
||||||
else if (key === 89) toggleEmblems(); // "Y" to toggle Emblems layer
|
else if (shift && key === 51) toggleAddRiver();
|
||||||
else if (key === 76) toggleLabels(); // "L" to toggle Labels layer
|
// Shift + "3" to click to add River
|
||||||
else if (key === 73) toggleIcons(); // "I" to toggle Icons layer
|
else if (shift && key === 52) toggleAddRoute();
|
||||||
else if (key === 77) toggleMilitary(); // "M" to toggle Military layer
|
// Shift + "4" to click to add Route
|
||||||
else if (key === 75) toggleMarkers(); // "K" to toggle Markers layer
|
else if (shift && key === 53) toggleAddMarker();
|
||||||
else if (key === 187) toggleRulers(); // Equal (=) to toggle Rulers
|
// Shift + "5" to click to add Marker
|
||||||
else if (key === 189) toggleScaleBar(); // Minus (-) to toggle Scale bar
|
else if (alt && key === 66) console.table(pack.burgs);
|
||||||
|
// Alt + "B" to log burgs data
|
||||||
else if (key === 37) zoom.translateBy(svg, 10, 0); // Left to scroll map left
|
else if (alt && key === 83) console.table(pack.states);
|
||||||
else if (key === 39) zoom.translateBy(svg, -10, 0); // Right to scroll map right
|
// Alt + "S" to log states data
|
||||||
else if (key === 38) zoom.translateBy(svg, 0, 10); // Up to scroll map up
|
else if (alt && key === 67) console.table(pack.cultures);
|
||||||
else if (key === 40) zoom.translateBy(svg, 0, -10); // Up to scroll map up
|
// Alt + "C" to log cultures data
|
||||||
else if (key === 107 || key === 109) pressNumpadSign(key); // Numpad Plus/Minus to zoom map or change brush size
|
else if (alt && key === 82) console.table(pack.religions);
|
||||||
else if (key === 48 || key === 96) resetZoom(1000); // 0 to reset zoom
|
// Alt + "R" to log religions data
|
||||||
else if (key === 49 || key === 97) zoom.scaleTo(svg, 1); // 1 to zoom to 1
|
else if (alt && key === 70) console.table(pack.features);
|
||||||
else if (key === 50 || key === 98) zoom.scaleTo(svg, 2); // 2 to zoom to 2
|
// Alt + "F" to log features data
|
||||||
else if (key === 51 || key === 99) zoom.scaleTo(svg, 3); // 3 to zoom to 3
|
else if (key === 88) toggleTexture();
|
||||||
else if (key === 52 || key === 100) zoom.scaleTo(svg, 4); // 4 to zoom to 4
|
// "X" to toggle Texture layer
|
||||||
else if (key === 53 || key === 101) zoom.scaleTo(svg, 5); // 5 to zoom to 5
|
else if (key === 72) toggleHeight();
|
||||||
else if (key === 54 || key === 102) zoom.scaleTo(svg, 6); // 6 to zoom to 6
|
// "H" to toggle Heightmap layer
|
||||||
else if (key === 55 || key === 103) zoom.scaleTo(svg, 7); // 7 to zoom to 7
|
else if (key === 66) toggleBiomes();
|
||||||
else if (key === 56 || key === 104) zoom.scaleTo(svg, 8); // 8 to zoom to 8
|
// "B" to toggle Biomes layer
|
||||||
else if (key === 57 || key === 105) zoom.scaleTo(svg, 9); // 9 to zoom to 9
|
else if (key === 69) toggleCells();
|
||||||
|
// "E" to toggle Cells layer
|
||||||
|
else if (key === 71) toggleGrid();
|
||||||
|
// "G" to toggle Grid layer
|
||||||
|
else if (key === 79) toggleCoordinates();
|
||||||
|
// "O" to toggle Coordinates layer
|
||||||
|
else if (key === 87) toggleCompass();
|
||||||
|
// "W" to toggle Compass Rose layer
|
||||||
|
else if (key === 86) toggleRivers();
|
||||||
|
// "V" to toggle Rivers layer
|
||||||
|
else if (key === 70) toggleRelief();
|
||||||
|
// "F" to toggle Relief icons layer
|
||||||
|
else if (key === 67) toggleCultures();
|
||||||
|
// "C" to toggle Cultures layer
|
||||||
|
else if (key === 83) toggleStates();
|
||||||
|
// "S" to toggle States layer
|
||||||
|
else if (key === 80) toggleProvinces();
|
||||||
|
// "P" to toggle Provinces layer
|
||||||
|
else if (key === 90) toggleZones();
|
||||||
|
// "Z" to toggle Zones
|
||||||
|
else if (key === 68) toggleBorders();
|
||||||
|
// "D" to toggle Borders layer
|
||||||
|
else if (key === 82) toggleReligions();
|
||||||
|
// "R" to toggle Religions layer
|
||||||
|
else if (key === 85) toggleRoutes();
|
||||||
|
// "U" to toggle Routes layer
|
||||||
|
else if (key === 84) toggleTemp();
|
||||||
|
// "T" to toggle Temperature layer
|
||||||
|
else if (key === 78) togglePopulation();
|
||||||
|
// "N" to toggle Population layer
|
||||||
|
else if (key === 74) toggleIce();
|
||||||
|
// "J" to toggle Ice layer
|
||||||
|
else if (key === 65) togglePrec();
|
||||||
|
// "A" to toggle Precipitation layer
|
||||||
|
else if (key === 81) toggleResources();
|
||||||
|
// "Q" to toggle Resources layer
|
||||||
|
else if (key === 89) toggleEmblems();
|
||||||
|
// "Y" to toggle Emblems layer
|
||||||
|
else if (key === 76) toggleLabels();
|
||||||
|
// "L" to toggle Labels layer
|
||||||
|
else if (key === 73) toggleIcons();
|
||||||
|
// "I" to toggle Icons layer
|
||||||
|
else if (key === 77) toggleMilitary();
|
||||||
|
// "M" to toggle Military layer
|
||||||
|
else if (key === 75) toggleMarkers();
|
||||||
|
// "K" to toggle Markers layer
|
||||||
|
else if (key === 187) toggleRulers();
|
||||||
|
// Equal (=) to toggle Rulers
|
||||||
|
else if (key === 189) toggleScaleBar();
|
||||||
|
// Minus (-) to toggle Scale bar
|
||||||
|
else if (key === 37) zoom.translateBy(svg, 10, 0);
|
||||||
|
// Left to scroll map left
|
||||||
|
else if (key === 39) zoom.translateBy(svg, -10, 0);
|
||||||
|
// Right to scroll map right
|
||||||
|
else if (key === 38) zoom.translateBy(svg, 0, 10);
|
||||||
|
// Up to scroll map up
|
||||||
|
else if (key === 40) zoom.translateBy(svg, 0, -10);
|
||||||
|
// Up to scroll map up
|
||||||
|
else if (key === 107 || key === 109) pressNumpadSign(key);
|
||||||
|
// Numpad Plus/Minus to zoom map or change brush size
|
||||||
|
else if (key === 48 || key === 96) resetZoom(1000);
|
||||||
|
// 0 to reset zoom
|
||||||
|
else if (key === 49 || key === 97) zoom.scaleTo(svg, 1);
|
||||||
|
// 1 to zoom to 1
|
||||||
|
else if (key === 50 || key === 98) zoom.scaleTo(svg, 2);
|
||||||
|
// 2 to zoom to 2
|
||||||
|
else if (key === 51 || key === 99) zoom.scaleTo(svg, 3);
|
||||||
|
// 3 to zoom to 3
|
||||||
|
else if (key === 52 || key === 100) zoom.scaleTo(svg, 4);
|
||||||
|
// 4 to zoom to 4
|
||||||
|
else if (key === 53 || key === 101) zoom.scaleTo(svg, 5);
|
||||||
|
// 5 to zoom to 5
|
||||||
|
else if (key === 54 || key === 102) zoom.scaleTo(svg, 6);
|
||||||
|
// 6 to zoom to 6
|
||||||
|
else if (key === 55 || key === 103) zoom.scaleTo(svg, 7);
|
||||||
|
// 7 to zoom to 7
|
||||||
|
else if (key === 56 || key === 104) zoom.scaleTo(svg, 8);
|
||||||
|
// 8 to zoom to 8
|
||||||
|
else if (key === 57 || key === 105) zoom.scaleTo(svg, 9);
|
||||||
|
// 9 to zoom to 9
|
||||||
else if (ctrl) pressControl(); // Control to toggle mode
|
else if (ctrl) pressControl(); // Control to toggle mode
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -564,32 +729,32 @@ function pressNumpadSign(key) {
|
||||||
let brush = null;
|
let brush = null;
|
||||||
const d = key === 107 ? 1 : -1;
|
const d = key === 107 ? 1 : -1;
|
||||||
|
|
||||||
if (brushRadius.offsetParent) brush = document.getElementById("brushRadius"); else
|
if (brushRadius.offsetParent) brush = document.getElementById('brushRadius');
|
||||||
if (biomesManuallyBrush.offsetParent) brush = document.getElementById("biomesManuallyBrush"); else
|
else if (biomesManuallyBrush.offsetParent) brush = document.getElementById('biomesManuallyBrush');
|
||||||
if (statesManuallyBrush.offsetParent) brush = document.getElementById("statesManuallyBrush"); else
|
else if (statesManuallyBrush.offsetParent) brush = document.getElementById('statesManuallyBrush');
|
||||||
if (provincesManuallyBrush.offsetParent) brush = document.getElementById("provincesManuallyBrush"); else
|
else if (provincesManuallyBrush.offsetParent) brush = document.getElementById('provincesManuallyBrush');
|
||||||
if (culturesManuallyBrush.offsetParent) brush = document.getElementById("culturesManuallyBrush"); else
|
else if (culturesManuallyBrush.offsetParent) brush = document.getElementById('culturesManuallyBrush');
|
||||||
if (zonesBrush.offsetParent) brush = document.getElementById("zonesBrush"); else
|
else if (zonesBrush.offsetParent) brush = document.getElementById('zonesBrush');
|
||||||
if (religionsManuallyBrush.offsetParent) brush = document.getElementById("religionsManuallyBrush");
|
else if (religionsManuallyBrush.offsetParent) brush = document.getElementById('religionsManuallyBrush');
|
||||||
|
|
||||||
if (brush) {
|
if (brush) {
|
||||||
const value = Math.max(Math.min(+brush.value + d, +brush.max), +brush.min);
|
const value = Math.max(Math.min(+brush.value + d, +brush.max), +brush.min);
|
||||||
brush.value = document.getElementById(brush.id+"Number").value = value;
|
brush.value = document.getElementById(brush.id + 'Number').value = value;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const scaleBy = key === 107 ? 1.2 : .8;
|
const scaleBy = key === 107 ? 1.2 : 0.8;
|
||||||
zoom.scaleBy(svg, scaleBy); // if no, zoom map
|
zoom.scaleBy(svg, scaleBy); // if no, zoom map
|
||||||
}
|
}
|
||||||
|
|
||||||
function pressControl() {
|
function pressControl() {
|
||||||
if (zonesRemove.offsetParent) {
|
if (zonesRemove.offsetParent) {
|
||||||
zonesRemove.classList.contains("pressed") ? zonesRemove.classList.remove("pressed") : zonesRemove.classList.add("pressed");
|
zonesRemove.classList.contains('pressed') ? zonesRemove.classList.remove('pressed') : zonesRemove.classList.add('pressed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// trigger trash button click on "Delete" keypress
|
// trigger trash button click on "Delete" keypress
|
||||||
function removeElementOnKey() {
|
function removeElementOnKey() {
|
||||||
$(".dialog:visible .fastDelete").click();
|
$('.dialog:visible .fastDelete').click();
|
||||||
$("button:visible:contains('Remove')").click();
|
$("button:visible:contains('Remove')").click();
|
||||||
}
|
}
|
||||||
|
|
@ -16,15 +16,9 @@ function editHeightmap() {
|
||||||
title: 'Edit Heightmap',
|
title: 'Edit Heightmap',
|
||||||
width: '28em',
|
width: '28em',
|
||||||
buttons: {
|
buttons: {
|
||||||
Erase: function () {
|
Erase: () => enterHeightmapEditMode('erase'),
|
||||||
enterHeightmapEditMode('erase');
|
Keep: () => enterHeightmapEditMode('keep'),
|
||||||
},
|
Risk: () => enterHeightmapEditMode('risk'),
|
||||||
Keep: function () {
|
|
||||||
enterHeightmapEditMode('keep');
|
|
||||||
},
|
|
||||||
Risk: function () {
|
|
||||||
enterHeightmapEditMode('risk');
|
|
||||||
},
|
|
||||||
Cancel: function () {
|
Cancel: function () {
|
||||||
$(this).dialog('close');
|
$(this).dialog('close');
|
||||||
}
|
}
|
||||||
|
|
@ -77,7 +71,7 @@ function editHeightmap() {
|
||||||
convertImage.style.display = type === 'erase' ? 'inline-block' : 'none';
|
convertImage.style.display = type === 'erase' ? 'inline-block' : 'none';
|
||||||
|
|
||||||
// hide erosion checkbox if mode is Keep
|
// hide erosion checkbox if mode is Keep
|
||||||
changeHeightsBox.style.display = type === 'keep' ? 'none' : 'inline-block';
|
allowErosionBox.style.display = type === 'keep' ? 'none' : 'inline-block';
|
||||||
|
|
||||||
// show finalize button
|
// show finalize button
|
||||||
if (!sessionStorage.getItem('noExitButtonAnimation')) {
|
if (!sessionStorage.getItem('noExitButtonAnimation')) {
|
||||||
|
|
@ -191,19 +185,22 @@ function editHeightmap() {
|
||||||
INFO && console.group('Edit Heightmap');
|
INFO && console.group('Edit Heightmap');
|
||||||
TIME && console.time('regenerateErasedData');
|
TIME && console.time('regenerateErasedData');
|
||||||
|
|
||||||
const change = changeHeights.checked;
|
const erosionAllowed = allowErosion.checked;
|
||||||
markFeatures();
|
markFeatures();
|
||||||
getSignedDistanceField();
|
markupGridOcean();
|
||||||
if (change) openNearSeaLakes();
|
if (erosionAllowed) {
|
||||||
|
addLakesInDeepDepressions();
|
||||||
|
openNearSeaLakes();
|
||||||
|
}
|
||||||
OceanLayers();
|
OceanLayers();
|
||||||
calculateTemperatures();
|
calculateTemperatures();
|
||||||
generatePrecipitation();
|
generatePrecipitation();
|
||||||
reGraph();
|
reGraph();
|
||||||
drawCoastline();
|
drawCoastline();
|
||||||
|
|
||||||
Rivers.generate(change);
|
Rivers.generate(erosionAllowed);
|
||||||
|
|
||||||
if (!change) {
|
if (!erosionAllowed) {
|
||||||
for (const i of pack.cells.i) {
|
for (const i of pack.cells.i) {
|
||||||
const g = pack.cells.g[i];
|
const g = pack.cells.g[i];
|
||||||
if (pack.cells.h[i] !== grid.cells.h[g] && pack.cells.h[i] >= 20 === grid.cells.h[g] >= 20) pack.cells.h[i] = grid.cells.h[g];
|
if (pack.cells.h[i] !== grid.cells.h[g] && pack.cells.h[i] >= 20 === grid.cells.h[g] >= 20) pack.cells.h[i] = grid.cells.h[g];
|
||||||
|
|
@ -248,6 +245,7 @@ function editHeightmap() {
|
||||||
function restoreRiskedData() {
|
function restoreRiskedData() {
|
||||||
INFO && console.group('Edit Heightmap');
|
INFO && console.group('Edit Heightmap');
|
||||||
TIME && console.time('restoreRiskedData');
|
TIME && console.time('restoreRiskedData');
|
||||||
|
const erosionAllowed = allowErosion.checked;
|
||||||
|
|
||||||
// assign pack data to grid cells
|
// assign pack data to grid cells
|
||||||
const l = grid.cells.i.length;
|
const l = grid.cells.i.length;
|
||||||
|
|
@ -262,7 +260,7 @@ function editHeightmap() {
|
||||||
const culture = new Uint16Array(l);
|
const culture = new Uint16Array(l);
|
||||||
const religion = new Uint16Array(l);
|
const religion = new Uint16Array(l);
|
||||||
|
|
||||||
// rivers data, stored only if changeHeights is unchecked
|
// rivers data, stored only if allowErosion is unchecked
|
||||||
const fl = new Uint16Array(l);
|
const fl = new Uint16Array(l);
|
||||||
const r = new Uint16Array(l);
|
const r = new Uint16Array(l);
|
||||||
const conf = new Uint8Array(l);
|
const conf = new Uint8Array(l);
|
||||||
|
|
@ -280,7 +278,7 @@ function editHeightmap() {
|
||||||
burg[g] = pack.cells.burg[i];
|
burg[g] = pack.cells.burg[i];
|
||||||
religion[g] = pack.cells.religion[i];
|
religion[g] = pack.cells.religion[i];
|
||||||
|
|
||||||
if (!changeHeights.checked) {
|
if (!erosionAllowed) {
|
||||||
fl[g] = pack.cells.fl[i];
|
fl[g] = pack.cells.fl[i];
|
||||||
r[g] = pack.cells.r[i];
|
r[g] = pack.cells.r[i];
|
||||||
conf[g] = pack.cells.conf[i];
|
conf[g] = pack.cells.conf[i];
|
||||||
|
|
@ -312,14 +310,15 @@ function editHeightmap() {
|
||||||
});
|
});
|
||||||
|
|
||||||
markFeatures();
|
markFeatures();
|
||||||
getSignedDistanceField();
|
markupGridOcean();
|
||||||
|
if (erosionAllowed) addLakesInDeepDepressions();
|
||||||
OceanLayers();
|
OceanLayers();
|
||||||
calculateTemperatures();
|
calculateTemperatures();
|
||||||
generatePrecipitation();
|
generatePrecipitation();
|
||||||
reGraph();
|
reGraph();
|
||||||
drawCoastline();
|
drawCoastline();
|
||||||
|
|
||||||
if (changeHeights.checked) Rivers.generate(changeHeights.checked);
|
if (erosionAllowed) Rivers.generate(true);
|
||||||
|
|
||||||
// assign saved pack data from grid back to pack
|
// assign saved pack data from grid back to pack
|
||||||
const n = pack.cells.i.length;
|
const n = pack.cells.i.length;
|
||||||
|
|
@ -334,7 +333,7 @@ function editHeightmap() {
|
||||||
pack.cells.religion = new Uint16Array(n);
|
pack.cells.religion = new Uint16Array(n);
|
||||||
pack.cells.biome = new Uint8Array(n);
|
pack.cells.biome = new Uint8Array(n);
|
||||||
|
|
||||||
if (!changeHeights.checked) {
|
if (!erosionAllowed) {
|
||||||
pack.cells.r = new Uint16Array(n);
|
pack.cells.r = new Uint16Array(n);
|
||||||
pack.cells.conf = new Uint8Array(n);
|
pack.cells.conf = new Uint8Array(n);
|
||||||
pack.cells.fl = new Uint16Array(n);
|
pack.cells.fl = new Uint16Array(n);
|
||||||
|
|
@ -348,7 +347,7 @@ function editHeightmap() {
|
||||||
pack.cells.biome[i] = land && biome[g] ? biome[g] : getBiomeId(grid.cells.prec[g], pack.cells.h[i]);
|
pack.cells.biome[i] = land && biome[g] ? biome[g] : getBiomeId(grid.cells.prec[g], pack.cells.h[i]);
|
||||||
|
|
||||||
// rivers data
|
// rivers data
|
||||||
if (!changeHeights.checked) {
|
if (!erosionAllowed) {
|
||||||
pack.cells.r[i] = r[g];
|
pack.cells.r[i] = r[g];
|
||||||
pack.cells.conf[i] = conf[g];
|
pack.cells.conf[i] = conf[g];
|
||||||
pack.cells.fl[i] = fl[g];
|
pack.cells.fl[i] = fl[g];
|
||||||
|
|
@ -412,7 +411,7 @@ function editHeightmap() {
|
||||||
drawStates();
|
drawStates();
|
||||||
drawBorders();
|
drawBorders();
|
||||||
|
|
||||||
if (changeHeights.checked) {
|
if (erosionAllowed) {
|
||||||
Rivers.specify();
|
Rivers.specify();
|
||||||
Lakes.generateName();
|
Lakes.generateName();
|
||||||
}
|
}
|
||||||
|
|
@ -817,10 +816,25 @@ function editHeightmap() {
|
||||||
const steps = body.querySelectorAll('div').length;
|
const steps = body.querySelectorAll('div').length;
|
||||||
const changed = +body.getAttribute('data-changed');
|
const changed = +body.getAttribute('data-changed');
|
||||||
const template = e.target.value;
|
const template = e.target.value;
|
||||||
if (!steps || !changed) return changeTemplate(template);
|
if (!steps || !changed) {
|
||||||
|
changeTemplate(template);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const message = 'Are you sure you want to select a different template? <br>All changes will be lost';
|
alertMessage.innerHTML = 'Are you sure you want to select a different template? All changes will be lost.';
|
||||||
confirmationDialog({title: 'Change template', message, confirm: 'Change', onConfirm: () => changeTemplate(template)});
|
$('#alert').dialog({
|
||||||
|
resizable: false,
|
||||||
|
title: 'Change Template',
|
||||||
|
buttons: {
|
||||||
|
Change: function () {
|
||||||
|
changeTemplate(template);
|
||||||
|
$(this).dialog('close');
|
||||||
|
},
|
||||||
|
Cancel: function () {
|
||||||
|
$(this).dialog('close');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeTemplate(template) {
|
function changeTemplate(template) {
|
||||||
|
|
@ -952,7 +966,8 @@ function editHeightmap() {
|
||||||
|
|
||||||
for (const s of steps) {
|
for (const s of steps) {
|
||||||
if (s.style.opacity == 0.5) continue;
|
if (s.style.opacity == 0.5) continue;
|
||||||
const type = s.getAttribute('data-type');
|
const type = s.dataset.type;
|
||||||
|
|
||||||
const elCount = s.querySelector('.templateCount') || '';
|
const elCount = s.querySelector('.templateCount') || '';
|
||||||
const elHeight = s.querySelector('.templateHeight') || '';
|
const elHeight = s.querySelector('.templateHeight') || '';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ function getDefaultPresets() {
|
||||||
heightmap: ['toggleHeight', 'toggleRivers'],
|
heightmap: ['toggleHeight', 'toggleRivers'],
|
||||||
physical: ['toggleCoordinates', 'toggleHeight', 'toggleIce', 'toggleRivers', 'toggleScaleBar'],
|
physical: ['toggleCoordinates', 'toggleHeight', 'toggleIce', 'toggleRivers', 'toggleScaleBar'],
|
||||||
poi: ['toggleBorders', 'toggleHeight', 'toggleIce', 'toggleIcons', 'toggleMarkers', 'toggleRivers', 'toggleRoutes', 'toggleScaleBar'],
|
poi: ['toggleBorders', 'toggleHeight', 'toggleIce', 'toggleIcons', 'toggleMarkers', 'toggleRivers', 'toggleRoutes', 'toggleScaleBar'],
|
||||||
economical: ['toggleResources', 'toggleBiomes', 'toggleBorders', 'toggleIcons', 'toggleIce', 'toggleLabels', 'toggleRivers', 'toggleRoutes', 'toggleScaleBar'],
|
|
||||||
military: ['toggleBorders', 'toggleIcons', 'toggleLabels', 'toggleMilitary', 'toggleRivers', 'toggleRoutes', 'toggleScaleBar', 'toggleStates'],
|
military: ['toggleBorders', 'toggleIcons', 'toggleLabels', 'toggleMilitary', 'toggleRivers', 'toggleRoutes', 'toggleScaleBar', 'toggleStates'],
|
||||||
emblems: ['toggleBorders', 'toggleIcons', 'toggleIce', 'toggleEmblems', 'toggleRivers', 'toggleRoutes', 'toggleScaleBar', 'toggleStates'],
|
emblems: ['toggleBorders', 'toggleIcons', 'toggleIce', 'toggleEmblems', 'toggleRivers', 'toggleRoutes', 'toggleScaleBar', 'toggleStates'],
|
||||||
landmass: ['toggleScaleBar']
|
landmass: ['toggleScaleBar']
|
||||||
|
|
@ -547,7 +546,7 @@ function drawPopulation(event) {
|
||||||
.transition(show)
|
.transition(show)
|
||||||
.attr('y2', (d) => d[2]);
|
.attr('y2', (d) => d[2]);
|
||||||
|
|
||||||
const urban = burgs.filter((b) => b.i && !b.removed).map((b) => [b.x, b.y, b.y - (b.population / 8) * urbanization.value]);
|
const urban = burgs.filter((b) => b.i && !b.removed).map((b) => [b.x, b.y, b.y - (b.population / 8) * urbanization]);
|
||||||
population
|
population
|
||||||
.select('#urban')
|
.select('#urban')
|
||||||
.selectAll('line')
|
.selectAll('line')
|
||||||
|
|
@ -919,7 +918,9 @@ function drawStates() {
|
||||||
const bodyString = bodyData.map((d) => `<path id="state${d[1]}" d="${d[0]}" fill="${d[2]}" stroke="none"/>`).join('');
|
const bodyString = bodyData.map((d) => `<path id="state${d[1]}" d="${d[0]}" fill="${d[2]}" stroke="none"/>`).join('');
|
||||||
const gapString = gapData.map((d) => `<path id="state-gap${d[1]}" d="${d[0]}" fill="none" stroke="${d[2]}"/>`).join('');
|
const gapString = gapData.map((d) => `<path id="state-gap${d[1]}" d="${d[0]}" fill="none" stroke="${d[2]}"/>`).join('');
|
||||||
const clipString = bodyData.map((d) => `<clipPath id="state-clip${d[1]}"><use href="#state${d[1]}"/></clipPath>`).join('');
|
const clipString = bodyData.map((d) => `<clipPath id="state-clip${d[1]}"><use href="#state${d[1]}"/></clipPath>`).join('');
|
||||||
const haloString = bodyData.map((d) => `<path id="state-border${d[1]}" d="${d[0]}" clip-path="url(#state-clip${d[1]})" stroke="${d3.color(d[2]) ? d3.color(d[2]).darker().hex() : '#666666'}"/>`).join('');
|
const haloString = bodyData
|
||||||
|
.map((d) => `<path id="state-border${d[1]}" d="${d[0]}" clip-path="url(#state-clip${d[1]})" stroke="${d3.color(d[2]) ? d3.color(d[2]).darker().hex() : '#666666'}"/>`)
|
||||||
|
.join('');
|
||||||
|
|
||||||
statesBody.html(bodyString + gapString);
|
statesBody.html(bodyString + gapString);
|
||||||
defs.select('#statePaths').html(clipString);
|
defs.select('#statePaths').html(clipString);
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,7 @@ class Rulers {
|
||||||
for (const rulerString of rulers) {
|
for (const rulerString of rulers) {
|
||||||
const [type, pointsString] = rulerString.split(": ");
|
const [type, pointsString] = rulerString.split(": ");
|
||||||
const points = pointsString.split(" ").map(el => el.split(",").map(n => +n));
|
const points = pointsString.split(" ").map(el => el.split(",").map(n => +n));
|
||||||
const Type = type === "Ruler" ? Ruler :
|
const Type = type === "Ruler" ? Ruler : type === "Opisometer" ? Opisometer : type === "RouteOpisometer" ? RouteOpisometer : type === "Planimeter" ? Planimeter : null;
|
||||||
type === "Opisometer" ? Opisometer :
|
|
||||||
type === "RouteOpisometer" ? RouteOpisometer :
|
|
||||||
type === "Planimeter" ? Planimeter : null;
|
|
||||||
this.create(Type, points);
|
this.create(Type, points);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -57,7 +54,7 @@ class Measurer {
|
||||||
}
|
}
|
||||||
|
|
||||||
getSize() {
|
getSize() {
|
||||||
return rn(1 / scale ** .3 * 2, 2);
|
return rn((1 / scale ** 0.3) * 2, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
getDash() {
|
getDash() {
|
||||||
|
|
@ -66,10 +63,11 @@ class Measurer {
|
||||||
|
|
||||||
drag() {
|
drag() {
|
||||||
const tr = parseTransform(this.getAttribute("transform"));
|
const tr = parseTransform(this.getAttribute("transform"));
|
||||||
const x = +tr[0] - d3.event.x, y = +tr[1] - d3.event.y;
|
const x = +tr[0] - d3.event.x,
|
||||||
|
y = +tr[1] - d3.event.y;
|
||||||
|
|
||||||
d3.event.on("drag", function () {
|
d3.event.on("drag", function () {
|
||||||
const transform = `translate(${(x + d3.event.x)},${(y + d3.event.y)})`;
|
const transform = `translate(${x + d3.event.x},${y + d3.event.y})`;
|
||||||
this.setAttribute("transform", transform);
|
this.setAttribute("transform", transform);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -105,7 +103,6 @@ class Measurer {
|
||||||
undraw() {
|
undraw() {
|
||||||
this.el?.remove();
|
this.el?.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Ruler extends Measurer {
|
class Ruler extends Measurer {
|
||||||
|
|
@ -136,12 +133,29 @@ class Ruler extends Measurer {
|
||||||
const size = this.getSize();
|
const size = this.getSize();
|
||||||
const dash = this.getDash();
|
const dash = this.getDash();
|
||||||
|
|
||||||
const el = this.el = ruler.append("g").attr("class", "ruler").call(d3.drag().on("start", this.drag)).attr("font-size", 10 * size);
|
const el = (this.el = ruler
|
||||||
el.append("polyline").attr("points", points).attr("class", "white").attr("stroke-width", size)
|
.append("g")
|
||||||
|
.attr("class", "ruler")
|
||||||
|
.call(d3.drag().on("start", this.drag))
|
||||||
|
.attr("font-size", 10 * size));
|
||||||
|
el.append("polyline")
|
||||||
|
.attr("points", points)
|
||||||
|
.attr("class", "white")
|
||||||
|
.attr("stroke-width", size)
|
||||||
.call(d3.drag().on("start", () => this.addControl(this)));
|
.call(d3.drag().on("start", () => this.addControl(this)));
|
||||||
el.append("polyline").attr("points", points).attr("class", "gray").attr("stroke-width", rn(size * 1.2, 2)).attr("stroke-dasharray", dash);
|
el.append("polyline")
|
||||||
el.append("g").attr("class", "rulerPoints").attr("stroke-width", .5 * size).attr("font-size", 2 * size);
|
.attr("points", points)
|
||||||
el.append("text").attr("dx", ".35em").attr("dy", "-.45em").on("click", () => rulers.remove(this.id));
|
.attr("class", "gray")
|
||||||
|
.attr("stroke-width", rn(size * 1.2, 2))
|
||||||
|
.attr("stroke-dasharray", dash);
|
||||||
|
el.append("g")
|
||||||
|
.attr("class", "rulerPoints")
|
||||||
|
.attr("stroke-width", 0.5 * size)
|
||||||
|
.attr("font-size", 2 * size);
|
||||||
|
el.append("text")
|
||||||
|
.attr("dx", ".35em")
|
||||||
|
.attr("dy", "-.45em")
|
||||||
|
.on("click", () => rulers.remove(this.id));
|
||||||
this.drawPoints(el);
|
this.drawPoints(el);
|
||||||
this.updateLabel();
|
this.updateLabel();
|
||||||
return this;
|
return this;
|
||||||
|
|
@ -160,10 +174,21 @@ class Ruler extends Measurer {
|
||||||
drawPoint(el, x, y, i) {
|
drawPoint(el, x, y, i) {
|
||||||
const context = this;
|
const context = this;
|
||||||
el.append("circle")
|
el.append("circle")
|
||||||
.attr("r", "1em").attr("cx", x).attr("cy", y)
|
.attr("r", "1em")
|
||||||
|
.attr("cx", x)
|
||||||
|
.attr("cy", y)
|
||||||
.attr("class", this.isEdge(i) ? "edge" : "control")
|
.attr("class", this.isEdge(i) ? "edge" : "control")
|
||||||
.on("click", function() {context.removePoint(context, i)})
|
.on("click", function () {
|
||||||
.call(d3.drag().clickDistance(3).on("start", function() {context.dragControl(context, i)}));
|
context.removePoint(context, i);
|
||||||
|
})
|
||||||
|
.call(
|
||||||
|
d3
|
||||||
|
.drag()
|
||||||
|
.clickDistance(3)
|
||||||
|
.on("start", function () {
|
||||||
|
context.dragControl(context, i);
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isEdge(i) {
|
isEdge(i) {
|
||||||
|
|
@ -198,7 +223,7 @@ class Ruler extends Measurer {
|
||||||
|
|
||||||
d3.event.on("drag", function () {
|
d3.event.on("drag", function () {
|
||||||
if (addPoint) {
|
if (addPoint) {
|
||||||
if (d3.event.dx < .1 && d3.event.dy < .1) return;
|
if (d3.event.dx < 0.1 && d3.event.dy < 0.1) return;
|
||||||
context.pushPoint(pointId);
|
context.pushPoint(pointId);
|
||||||
context.drawPoints(context.el);
|
context.drawPoints(context.el);
|
||||||
if (pointId) pointId++;
|
if (pointId) pointId++;
|
||||||
|
|
@ -253,13 +278,38 @@ class Opisometer extends Measurer {
|
||||||
const dash = this.getDash();
|
const dash = this.getDash();
|
||||||
const context = this;
|
const context = this;
|
||||||
|
|
||||||
const el = this.el = ruler.append("g").attr("class", "opisometer").call(d3.drag().on("start", this.drag)).attr("font-size", 10 * size);
|
const el = (this.el = ruler
|
||||||
|
.append("g")
|
||||||
|
.attr("class", "opisometer")
|
||||||
|
.call(d3.drag().on("start", this.drag))
|
||||||
|
.attr("font-size", 10 * size));
|
||||||
el.append("path").attr("class", "white").attr("stroke-width", size);
|
el.append("path").attr("class", "white").attr("stroke-width", size);
|
||||||
el.append("path").attr("class", "gray").attr("stroke-width", size).attr("stroke-dasharray", dash);
|
el.append("path").attr("class", "gray").attr("stroke-width", size).attr("stroke-dasharray", dash);
|
||||||
const rulerPoints = el.append("g").attr("class", "rulerPoints").attr("stroke-width", .5 * size).attr("font-size", 2 * size);
|
const rulerPoints = el
|
||||||
rulerPoints.append("circle").attr("r", "1em").call(d3.drag().on("start", function() {context.dragControl(context, 0)}));
|
.append("g")
|
||||||
rulerPoints.append("circle").attr("r", "1em").call(d3.drag().on("start", function() {context.dragControl(context, 1)}));
|
.attr("class", "rulerPoints")
|
||||||
el.append("text").attr("dx", ".35em").attr("dy", "-.45em").on("click", () => rulers.remove(this.id));
|
.attr("stroke-width", 0.5 * size)
|
||||||
|
.attr("font-size", 2 * size);
|
||||||
|
rulerPoints
|
||||||
|
.append("circle")
|
||||||
|
.attr("r", "1em")
|
||||||
|
.call(
|
||||||
|
d3.drag().on("start", function () {
|
||||||
|
context.dragControl(context, 0);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
rulerPoints
|
||||||
|
.append("circle")
|
||||||
|
.attr("r", "1em")
|
||||||
|
.call(
|
||||||
|
d3.drag().on("start", function () {
|
||||||
|
context.dragControl(context, 1);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
el.append("text")
|
||||||
|
.attr("dx", ".35em")
|
||||||
|
.attr("dy", "-.45em")
|
||||||
|
.on("click", () => rulers.remove(this.id));
|
||||||
|
|
||||||
this.updateCurve();
|
this.updateCurve();
|
||||||
this.updateLabel();
|
this.updateLabel();
|
||||||
|
|
@ -267,7 +317,7 @@ class Opisometer extends Measurer {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCurve() {
|
updateCurve() {
|
||||||
lineGen.curve(d3.curveCatmullRom.alpha(.5));
|
lineGen.curve(d3.curveCatmullRom.alpha(0.5));
|
||||||
const path = round(lineGen(this.points));
|
const path = round(lineGen(this.points));
|
||||||
this.el.selectAll("path").attr("d", path);
|
this.el.selectAll("path").attr("d", path);
|
||||||
|
|
||||||
|
|
@ -367,13 +417,37 @@ class RouteOpisometer extends Measurer {
|
||||||
const dash = this.getDash();
|
const dash = this.getDash();
|
||||||
const context = this;
|
const context = this;
|
||||||
|
|
||||||
const el = this.el = ruler.append("g").attr("class", "opisometer").attr("font-size", 10 * size);
|
const el = (this.el = ruler
|
||||||
|
.append("g")
|
||||||
|
.attr("class", "opisometer")
|
||||||
|
.attr("font-size", 10 * size));
|
||||||
el.append("path").attr("class", "white").attr("stroke-width", size);
|
el.append("path").attr("class", "white").attr("stroke-width", size);
|
||||||
el.append("path").attr("class", "gray").attr("stroke-width", size).attr("stroke-dasharray", dash);
|
el.append("path").attr("class", "gray").attr("stroke-width", size).attr("stroke-dasharray", dash);
|
||||||
const rulerPoints = el.append("g").attr("class", "rulerPoints").attr("stroke-width", .5 * size).attr("font-size", 2 * size);
|
const rulerPoints = el
|
||||||
rulerPoints.append("circle").attr("r", "1em").call(d3.drag().on("start", function() {context.dragControl(context, 0)}));
|
.append("g")
|
||||||
rulerPoints.append("circle").attr("r", "1em").call(d3.drag().on("start", function() {context.dragControl(context, 1)}));
|
.attr("class", "rulerPoints")
|
||||||
el.append("text").attr("dx", ".35em").attr("dy", "-.45em").on("click", () => rulers.remove(this.id));
|
.attr("stroke-width", 0.5 * size)
|
||||||
|
.attr("font-size", 2 * size);
|
||||||
|
rulerPoints
|
||||||
|
.append("circle")
|
||||||
|
.attr("r", "1em")
|
||||||
|
.call(
|
||||||
|
d3.drag().on("start", function () {
|
||||||
|
context.dragControl(context, 0);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
rulerPoints
|
||||||
|
.append("circle")
|
||||||
|
.attr("r", "1em")
|
||||||
|
.call(
|
||||||
|
d3.drag().on("start", function () {
|
||||||
|
context.dragControl(context, 1);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
el.append("text")
|
||||||
|
.attr("dx", ".35em")
|
||||||
|
.attr("dy", "-.45em")
|
||||||
|
.on("click", () => rulers.remove(this.id));
|
||||||
|
|
||||||
this.updateCurve();
|
this.updateCurve();
|
||||||
this.updateLabel();
|
this.updateLabel();
|
||||||
|
|
@ -381,7 +455,7 @@ class RouteOpisometer extends Measurer {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCurve() {
|
updateCurve() {
|
||||||
lineGen.curve(d3.curveCatmullRom.alpha(.5));
|
lineGen.curve(d3.curveCatmullRom.alpha(0.5));
|
||||||
const path = round(lineGen(this.points));
|
const path = round(lineGen(this.points));
|
||||||
this.el.selectAll("path").attr("d", path);
|
this.el.selectAll("path").attr("d", path);
|
||||||
|
|
||||||
|
|
@ -422,7 +496,11 @@ class Planimeter extends Measurer {
|
||||||
if (this.el) this.el.selectAll("*").remove();
|
if (this.el) this.el.selectAll("*").remove();
|
||||||
const size = this.getSize();
|
const size = this.getSize();
|
||||||
|
|
||||||
const el = this.el = ruler.append("g").attr("class", "planimeter").call(d3.drag().on("start", this.drag)).attr("font-size", 10 * size);
|
const el = (this.el = ruler
|
||||||
|
.append("g")
|
||||||
|
.attr("class", "planimeter")
|
||||||
|
.call(d3.drag().on("start", this.drag))
|
||||||
|
.attr("font-size", 10 * size));
|
||||||
el.append("path").attr("class", "planimeter").attr("stroke-width", size);
|
el.append("path").attr("class", "planimeter").attr("stroke-width", size);
|
||||||
el.append("text").on("click", () => rulers.remove(this.id));
|
el.append("text").on("click", () => rulers.remove(this.id));
|
||||||
|
|
||||||
|
|
@ -432,7 +510,7 @@ class Planimeter extends Measurer {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCurve() {
|
updateCurve() {
|
||||||
lineGen.curve(d3.curveCatmullRomClosed.alpha(.5));
|
lineGen.curve(d3.curveCatmullRomClosed.alpha(0.5));
|
||||||
const path = round(lineGen(this.points));
|
const path = round(lineGen(this.points));
|
||||||
this.el.selectAll("path").attr("d", path);
|
this.el.selectAll("path").attr("d", path);
|
||||||
}
|
}
|
||||||
|
|
@ -458,36 +536,79 @@ function drawScaleBar() {
|
||||||
|
|
||||||
// calculate size
|
// calculate size
|
||||||
const init = 100; // actual length in pixels if scale, dScale and size = 1;
|
const init = 100; // actual length in pixels if scale, dScale and size = 1;
|
||||||
const size = +barSize.value;
|
const size = +barSizeInput.value;
|
||||||
let val = init * size * dScale / scale; // bar length in distance unit
|
let val = (init * size * dScale) / scale; // bar length in distance unit
|
||||||
if (val > 900) val = rn(val, -3); // round to 1000
|
if (val > 900) val = rn(val, -3);
|
||||||
else if (val > 90) val = rn(val, -2); // round to 100
|
// round to 1000
|
||||||
else if (val > 9) val = rn(val, -1); // round to 10
|
else if (val > 90) val = rn(val, -2);
|
||||||
else val = rn(val) // round to 1
|
// round to 100
|
||||||
const l = val * scale / dScale; // actual length in pixels on this scale
|
else if (val > 9) val = rn(val, -1);
|
||||||
|
// round to 10
|
||||||
|
else val = rn(val); // round to 1
|
||||||
|
const l = (val * scale) / dScale; // actual length in pixels on this scale
|
||||||
|
|
||||||
scaleBar.append("line").attr("x1", 0.5).attr("y1", 0).attr("x2", l+size-0.5).attr("y2", 0).attr("stroke-width", size).attr("stroke", "white");
|
scaleBar
|
||||||
scaleBar.append("line").attr("x1", 0).attr("y1", size).attr("x2", l+size).attr("y2", size).attr("stroke-width", size).attr("stroke", "#3d3d3d");
|
.append("line")
|
||||||
|
.attr("x1", 0.5)
|
||||||
|
.attr("y1", 0)
|
||||||
|
.attr("x2", l + size - 0.5)
|
||||||
|
.attr("y2", 0)
|
||||||
|
.attr("stroke-width", size)
|
||||||
|
.attr("stroke", "white");
|
||||||
|
scaleBar
|
||||||
|
.append("line")
|
||||||
|
.attr("x1", 0)
|
||||||
|
.attr("y1", size)
|
||||||
|
.attr("x2", l + size)
|
||||||
|
.attr("y2", size)
|
||||||
|
.attr("stroke-width", size)
|
||||||
|
.attr("stroke", "#3d3d3d");
|
||||||
const dash = size + " " + rn(l / 5 - size, 2);
|
const dash = size + " " + rn(l / 5 - size, 2);
|
||||||
scaleBar.append("line").attr("x1", 0).attr("y1", 0).attr("x2", l+size).attr("y2", 0)
|
scaleBar
|
||||||
.attr("stroke-width", rn(size * 3, 2)).attr("stroke-dasharray", dash).attr("stroke", "#3d3d3d");
|
.append("line")
|
||||||
|
.attr("x1", 0)
|
||||||
|
.attr("y1", 0)
|
||||||
|
.attr("x2", l + size)
|
||||||
|
.attr("y2", 0)
|
||||||
|
.attr("stroke-width", rn(size * 3, 2))
|
||||||
|
.attr("stroke-dasharray", dash)
|
||||||
|
.attr("stroke", "#3d3d3d");
|
||||||
|
|
||||||
const fontSize = rn(5 * size, 1);
|
const fontSize = rn(5 * size, 1);
|
||||||
scaleBar.selectAll("text").data(d3.range(0,6)).enter().append("text")
|
scaleBar
|
||||||
.attr("x", d => rn(d * l/5, 2)).attr("y", 0).attr("dy", "-.5em")
|
.selectAll("text")
|
||||||
.attr("font-size", fontSize).text(d => rn(d * l/5 * dScale / scale) + (d<5 ? "" : " " + unit));
|
.data(d3.range(0, 6))
|
||||||
|
.enter()
|
||||||
|
.append("text")
|
||||||
|
.attr("x", d => rn((d * l) / 5, 2))
|
||||||
|
.attr("y", 0)
|
||||||
|
.attr("dy", "-.5em")
|
||||||
|
.attr("font-size", fontSize)
|
||||||
|
.text(d => rn((((d * l) / 5) * dScale) / scale) + (d < 5 ? "" : " " + unit));
|
||||||
|
|
||||||
if (barLabel.value !== "") {
|
if (barLabel.value !== "") {
|
||||||
scaleBar.append("text").attr("x", (l+1) / 2).attr("y", 2 * size)
|
scaleBar
|
||||||
|
.append("text")
|
||||||
|
.attr("x", (l + 1) / 2)
|
||||||
|
.attr("y", 2 * size)
|
||||||
.attr("dominant-baseline", "text-before-edge")
|
.attr("dominant-baseline", "text-before-edge")
|
||||||
.attr("font-size", fontSize).text(barLabel.value);
|
.attr("font-size", fontSize)
|
||||||
|
.text(barLabel.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const bbox = scaleBar.node().getBBox();
|
const bbox = scaleBar.node().getBBox();
|
||||||
// append backbround rectangle
|
// append backbround rectangle
|
||||||
scaleBar.insert("rect", ":first-child").attr("x", -10).attr("y", -20).attr("width", bbox.width + 10).attr("height", bbox.height + 15)
|
scaleBar
|
||||||
.attr("stroke-width", size).attr("stroke", "none").attr("filter", "url(#blur5)")
|
.insert("rect", ":first-child")
|
||||||
.attr("fill", barBackColor.value).attr("opacity", +barBackOpacity.value);
|
.attr("x", -10)
|
||||||
|
.attr("y", -20)
|
||||||
|
.attr("width", bbox.width + 10)
|
||||||
|
.attr("height", bbox.height + 15)
|
||||||
|
.attr("stroke-width", size)
|
||||||
|
.attr("stroke", "none")
|
||||||
|
.attr("filter", "url(#blur5)")
|
||||||
|
.attr("fill", barBackColor.value)
|
||||||
|
.attr("opacity", +barBackOpacity.value);
|
||||||
|
|
||||||
fitScaleBar();
|
fitScaleBar();
|
||||||
}
|
}
|
||||||
|
|
@ -495,9 +616,10 @@ function drawScaleBar() {
|
||||||
// fit ScaleBar to canvas size
|
// fit ScaleBar to canvas size
|
||||||
function fitScaleBar() {
|
function fitScaleBar() {
|
||||||
if (!scaleBar.select("rect").size() || scaleBar.style("display") === "none") return;
|
if (!scaleBar.select("rect").size() || scaleBar.style("display") === "none") return;
|
||||||
const px = isNaN(+barPosX.value) ? .99 : barPosX.value / 100;
|
const px = isNaN(+barPosX.value) ? 0.99 : barPosX.value / 100;
|
||||||
const py = isNaN(+barPosY.value) ? .99 : barPosY.value / 100;
|
const py = isNaN(+barPosY.value) ? 0.99 : barPosY.value / 100;
|
||||||
const bbox = scaleBar.select("rect").node().getBBox();
|
const bbox = scaleBar.select("rect").node().getBBox();
|
||||||
const x = rn(svgWidth * px - bbox.width + 10), y = rn(svgHeight * py - bbox.height + 20);
|
const x = rn(svgWidth * px - bbox.width + 10),
|
||||||
|
y = rn(svgHeight * py - bbox.height + 20);
|
||||||
scaleBar.attr("transform", `translate(${x},${y})`);
|
scaleBar.attr("transform", `translate(${x},${y})`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ function overviewMilitary() {
|
||||||
const states = pack.states.filter((s) => s.i && !s.removed);
|
const states = pack.states.filter((s) => s.i && !s.removed);
|
||||||
|
|
||||||
for (const s of states) {
|
for (const s of states) {
|
||||||
const population = rn((s.rural + s.urban * urbanization.value) * populationRate.value);
|
const population = rn((s.rural + s.urban * urbanization) * populationRate);
|
||||||
const getForces = (u) => s.military.reduce((s, r) => s + (r.u[u.name] || 0), 0);
|
const getForces = (u) => s.military.reduce((s, r) => s + (r.u[u.name] || 0), 0);
|
||||||
const total = options.military.reduce((s, u) => s + getForces(u) * u.crew, 0);
|
const total = options.military.reduce((s, u) => s + getForces(u) * u.crew, 0);
|
||||||
const rate = (total / population) * 100;
|
const rate = (total / population) * 100;
|
||||||
|
|
@ -114,7 +114,7 @@ function overviewMilitary() {
|
||||||
const getForces = (u) => s.military.reduce((s, r) => s + (r.u[u.name] || 0), 0);
|
const getForces = (u) => s.military.reduce((s, r) => s + (r.u[u.name] || 0), 0);
|
||||||
options.military.forEach((u) => (line.dataset[u.name] = line.querySelector(`div[data-type='${u.name}']`).innerHTML = getForces(u)));
|
options.military.forEach((u) => (line.dataset[u.name] = line.querySelector(`div[data-type='${u.name}']`).innerHTML = getForces(u)));
|
||||||
|
|
||||||
const population = rn((s.rural + s.urban * urbanization.value) * populationRate.value);
|
const population = rn((s.rural + s.urban * urbanization) * populationRate);
|
||||||
const total = (line.dataset.total = options.military.reduce((s, u) => s + getForces(u) * u.crew, 0));
|
const total = (line.dataset.total = options.military.reduce((s, u) => s + getForces(u) * u.crew, 0));
|
||||||
const rate = (line.dataset.rate = (total / population) * 100);
|
const rate = (line.dataset.rate = (total / population) * 100);
|
||||||
line.querySelector("div[data-type='total']").innerHTML = si(total);
|
line.querySelector("div[data-type='total']").innerHTML = si(total);
|
||||||
|
|
@ -282,12 +282,21 @@ function overviewMilitary() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function militaryRecalculate() {
|
function militaryRecalculate() {
|
||||||
const message = 'Are you sure you want to recalculate military forces for all states?<br>Regiments for all states will be regenerated';
|
alertMessage.innerHTML = 'Are you sure you want to recalculate military forces for all states?<br>Regiments for all states will be regenerated';
|
||||||
const onConfirm = () => {
|
$('#alert').dialog({
|
||||||
|
resizable: false,
|
||||||
|
title: 'Remove regiment',
|
||||||
|
buttons: {
|
||||||
|
Recalculate: function () {
|
||||||
|
$(this).dialog('close');
|
||||||
Military.generate();
|
Military.generate();
|
||||||
addLines();
|
addLines();
|
||||||
};
|
},
|
||||||
confirmationDialog({title: 'Remove regiment', message, confirm: 'Remove', onConfirm});
|
Cancel: function () {
|
||||||
|
$(this).dialog('close');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function downloadMilitaryData() {
|
function downloadMilitaryData() {
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ function editNotes(id, name) {
|
||||||
document.getElementById('legendsToLoad').addEventListener('change', function () {
|
document.getElementById('legendsToLoad').addEventListener('change', function () {
|
||||||
uploadFile(this, uploadLegends);
|
uploadFile(this, uploadLegends);
|
||||||
});
|
});
|
||||||
|
document.getElementById('notesClearStyle').addEventListener('click', clearStyle);
|
||||||
document.getElementById('notesRemove').addEventListener('click', triggerNotesRemove);
|
document.getElementById('notesRemove').addEventListener('click', triggerNotesRemove);
|
||||||
|
|
||||||
function showNote(note) {
|
function showNote(note) {
|
||||||
|
|
@ -89,8 +90,20 @@ function editNotes(id, name) {
|
||||||
const element = document.getElementById(select.value);
|
const element = document.getElementById(select.value);
|
||||||
|
|
||||||
if (element === null) {
|
if (element === null) {
|
||||||
const message = 'Related element is not found. Would you like to remove the note?';
|
alertMessage.innerHTML = 'Related element is not found. Would you like to remove the note?';
|
||||||
confirmationDialog({title: 'Element not found', message, confirm: 'Remove', onConfirm: removeLegend});
|
$('#alert').dialog({
|
||||||
|
resizable: false,
|
||||||
|
title: 'Element not found',
|
||||||
|
buttons: {
|
||||||
|
Remove: function () {
|
||||||
|
$(this).dialog('close');
|
||||||
|
removeLegend();
|
||||||
|
},
|
||||||
|
Keep: function () {
|
||||||
|
$(this).dialog('close');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -104,15 +117,34 @@ function editNotes(id, name) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function uploadLegends(dataLoaded) {
|
function uploadLegends(dataLoaded) {
|
||||||
if (!dataLoaded) return tip('Cannot load the file. Please check the data format', false, 'error');
|
if (!dataLoaded) {
|
||||||
|
tip('Cannot load the file. Please check the data format', false, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
notes = JSON.parse(dataLoaded);
|
notes = JSON.parse(dataLoaded);
|
||||||
document.getElementById('notesSelect').options.length = 0;
|
document.getElementById('notesSelect').options.length = 0;
|
||||||
editNotes(notes[0].id, notes[0].name);
|
editNotes(notes[0].id, notes[0].name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearStyle() {
|
||||||
|
editor.content.innerHTML = editor.content.textContent;
|
||||||
|
}
|
||||||
|
|
||||||
function triggerNotesRemove() {
|
function triggerNotesRemove() {
|
||||||
const message = 'Are you sure you want to remove the selected note? <br>This action cannot be reverted';
|
alertMessage.innerHTML = 'Are you sure you want to remove the selected note?';
|
||||||
confirmationDialog({title: 'Remove note', message, confirm: 'Remove', onConfirm: removeLegend});
|
$('#alert').dialog({
|
||||||
|
resizable: false,
|
||||||
|
title: 'Remove note',
|
||||||
|
buttons: {
|
||||||
|
Remove: function () {
|
||||||
|
$(this).dialog('close');
|
||||||
|
removeLegend();
|
||||||
|
},
|
||||||
|
Keep: function () {
|
||||||
|
$(this).dialog('close');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeLegend() {
|
function removeLegend() {
|
||||||
|
|
@ -120,7 +152,10 @@ function editNotes(id, name) {
|
||||||
const index = notes.findIndex((n) => n.id === select.value);
|
const index = notes.findIndex((n) => n.id === select.value);
|
||||||
notes.splice(index, 1);
|
notes.splice(index, 1);
|
||||||
select.options.length = 0;
|
select.options.length = 0;
|
||||||
if (!notes.length) return $('#notesEditor').dialog('close');
|
if (!notes.length) {
|
||||||
|
$('#notesEditor').dialog('close');
|
||||||
|
return;
|
||||||
|
}
|
||||||
notesText.innerHTML = '';
|
notesText.innerHTML = '';
|
||||||
editNotes(notes[0].id, notes[0].name);
|
editNotes(notes[0].id, notes[0].name);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,10 @@ function showSupporters() {
|
||||||
Kyle S,Eric Moore,Dean Dunakin,Uniquenameosaurus,WarWizardGames,Chance Mena,Jan Ka,Miguel Alejandro,Dalton Clark,Simon Drapeau,Radovan Zapletal,Jmmat6,
|
Kyle S,Eric Moore,Dean Dunakin,Uniquenameosaurus,WarWizardGames,Chance Mena,Jan Ka,Miguel Alejandro,Dalton Clark,Simon Drapeau,Radovan Zapletal,Jmmat6,
|
||||||
Justa Badge,Blargh Blarghmoomoo,Vanessa Anjos,Grant A. Murray,Akirsop,Rikard Wolff,Jake Fish,teco 47,Antiroo,Jakob Siegel,Guilherme Aguiar,Jarno Hallikainen,
|
Justa Badge,Blargh Blarghmoomoo,Vanessa Anjos,Grant A. Murray,Akirsop,Rikard Wolff,Jake Fish,teco 47,Antiroo,Jakob Siegel,Guilherme Aguiar,Jarno Hallikainen,
|
||||||
Justin Mcclain,Kristin Chernoff,Rowland Kingman,Esther Busch,Grayson McClead,Austin,Hakon the Viking,Chad Riley,Cooper Counts,Patrick Jones,Clonetone,
|
Justin Mcclain,Kristin Chernoff,Rowland Kingman,Esther Busch,Grayson McClead,Austin,Hakon the Viking,Chad Riley,Cooper Counts,Patrick Jones,Clonetone,
|
||||||
PlayByMail.Net,Brad Wardell,Lance Saba,Egoensis,Brea Richards,Tiber,Chris Bloom,Maxim Lowe,Aquelion,Page One Project,Spencer Morris,Paul Ingram`;
|
PlayByMail.Net,Brad Wardell,Lance Saba,Egoensis,Brea Richards,Tiber,Chris Bloom,Maxim Lowe,Aquelion,Page One Project,Spencer Morris,Paul Ingram,
|
||||||
|
Dust Bunny,Adrian Wright,Eric Alexander Cartaya,GameNight,Thomas Mortensen Hansen,Zklaus,Drinarius,Ed Wright,Lon Varnadore,Crys Cain,Heaven N Lee,
|
||||||
|
Jeffrey Henning,Lazer Elf,Jordan Bellah,Alex Beard,Kass Frisson,Petro Lombaard,Emanuel Pietri,Rox,PinkEvil,Gavin Madrigal,Martin Lorber,Prince of Morgoth,
|
||||||
|
Jaryd Armstrong,Andrew Pirkola,ThyHolyDevil,Gary Smith,Tyshaun Wise,Ethan Cook,Jon Stroman,Nobody679,良义 金,Chris Gray`;
|
||||||
|
|
||||||
const array = supporters
|
const array = supporters
|
||||||
.replace(/(?:\r\n|\r|\n)/g, '')
|
.replace(/(?:\r\n|\r|\n)/g, '')
|
||||||
|
|
@ -106,35 +109,51 @@ function showSupporters() {
|
||||||
$('#alert').dialog({resizable: false, title: 'Patreon Supporters', width: '54vw', position: {my: 'center', at: 'center', of: 'svg'}});
|
$('#alert').dialog({resizable: false, title: 'Patreon Supporters', width: '54vw', position: {my: 'center', at: 'center', of: 'svg'}});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// on any option or dialog change
|
||||||
|
document.getElementById('options').addEventListener('change', checkIfStored);
|
||||||
|
document.getElementById('dialogs').addEventListener('change', checkIfStored);
|
||||||
|
document.getElementById('options').addEventListener('input', updateOutputToFollowInput);
|
||||||
|
document.getElementById('dialogs').addEventListener('input', updateOutputToFollowInput);
|
||||||
|
|
||||||
|
function checkIfStored(ev) {
|
||||||
|
if (ev.target.dataset.stored) lock(ev.target.dataset.stored);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateOutputToFollowInput(ev) {
|
||||||
|
const id = ev.target.id;
|
||||||
|
const value = ev.target.value;
|
||||||
|
|
||||||
|
// specific cases
|
||||||
|
if (id === 'manorsInput') return (manorsOutput.value = value == 1000 ? 'auto' : value);
|
||||||
|
|
||||||
|
// generic case
|
||||||
|
if (id.slice(-5) === 'Input') {
|
||||||
|
const output = document.getElementById(id.slice(0, -5) + 'Output');
|
||||||
|
if (output) output.value = value;
|
||||||
|
} else if (id.slice(-6) === 'Output') {
|
||||||
|
const input = document.getElementById(id.slice(0, -6) + 'Input');
|
||||||
|
if (input) input.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Option listeners
|
// Option listeners
|
||||||
const optionsContent = document.getElementById('optionsContent');
|
const optionsContent = document.getElementById('optionsContent');
|
||||||
optionsContent.addEventListener('input', function (event) {
|
optionsContent.addEventListener('input', function (event) {
|
||||||
const id = event.target.id,
|
const id = event.target.id;
|
||||||
value = event.target.value;
|
const value = event.target.value;
|
||||||
if (id === 'mapWidthInput' || id === 'mapHeightInput') mapSizeInputChange();
|
if (id === 'mapWidthInput' || id === 'mapHeightInput') mapSizeInputChange();
|
||||||
else if (id === 'pointsInput') changeCellsDensity(+value);
|
else if (id === 'pointsInput') changeCellsDensity(+value);
|
||||||
else if (id === 'culturesInput') culturesOutput.value = value;
|
|
||||||
else if (id === 'culturesOutput') culturesInput.value = value;
|
|
||||||
else if (id === 'culturesSet') changeCultureSet();
|
else if (id === 'culturesSet') changeCultureSet();
|
||||||
else if (id === 'regionsInput' || id === 'regionsOutput') changeStatesNumber(value);
|
else if (id === 'regionsInput' || id === 'regionsOutput') changeStatesNumber(value);
|
||||||
else if (id === 'provincesInput') provincesOutput.value = value;
|
|
||||||
else if (id === 'provincesOutput') provincesOutput.value = value;
|
|
||||||
else if (id === 'provincesOutput') powerOutput.value = value;
|
|
||||||
else if (id === 'powerInput') powerOutput.value = value;
|
|
||||||
else if (id === 'powerOutput') powerInput.value = value;
|
|
||||||
else if (id === 'neutralInput') neutralOutput.value = value;
|
|
||||||
else if (id === 'neutralOutput') neutralInput.value = value;
|
|
||||||
else if (id === 'manorsInput') changeBurgsNumberSlider(value);
|
|
||||||
else if (id === 'religionsInput') religionsOutput.value = value;
|
|
||||||
else if (id === 'emblemShape') changeEmblemShape(value);
|
else if (id === 'emblemShape') changeEmblemShape(value);
|
||||||
else if (id === 'tooltipSizeInput' || id === 'tooltipSizeOutput') changeTooltipSize(value);
|
else if (id === 'tooltipSizeInput' || id === 'tooltipSizeOutput') changeTooltipSize(value);
|
||||||
else if (id === 'transparencyInput') changeDialogsTransparency(value);
|
else if (id === 'transparencyInput') changeDialogsTransparency(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
optionsContent.addEventListener('change', function (event) {
|
optionsContent.addEventListener('change', function (event) {
|
||||||
if (event.target.dataset.stored) lock(event.target.dataset.stored);
|
const id = event.target.id;
|
||||||
const id = event.target.id,
|
const value = event.target.value;
|
||||||
value = event.target.value;
|
|
||||||
if (id === 'zoomExtentMin' || id === 'zoomExtentMax') changeZoomExtent(value);
|
if (id === 'zoomExtentMin' || id === 'zoomExtentMax') changeZoomExtent(value);
|
||||||
else if (id === 'optionsSeed') generateMapWithSeed();
|
else if (id === 'optionsSeed') generateMapWithSeed();
|
||||||
else if (id === 'uiSizeInput' || id === 'uiSizeOutput') changeUIsize(value);
|
else if (id === 'uiSizeInput' || id === 'uiSizeOutput') changeUIsize(value);
|
||||||
|
|
@ -330,8 +349,8 @@ function changeCellsDensity(value) {
|
||||||
const cells = convert(value);
|
const cells = convert(value);
|
||||||
|
|
||||||
pointsInput.setAttribute('data-cells', cells);
|
pointsInput.setAttribute('data-cells', cells);
|
||||||
pointsOutput.value = cells / 1000 + 'K';
|
pointsOutput_formatted.value = cells / 1000 + 'K';
|
||||||
pointsOutput.style.color = cells > 50000 ? '#b12117' : cells !== 10000 ? '#dfdf12' : '#053305';
|
pointsOutput_formatted.style.color = cells > 50000 ? '#b12117' : cells !== 10000 ? '#dfdf12' : '#053305';
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeCultureSet() {
|
function changeCultureSet() {
|
||||||
|
|
@ -382,16 +401,11 @@ function changeEmblemShape(emblemShape) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeStatesNumber(value) {
|
function changeStatesNumber(value) {
|
||||||
regionsInput.value = regionsOutput.value = value;
|
|
||||||
regionsOutput.style.color = +value ? null : '#b12117';
|
regionsOutput.style.color = +value ? null : '#b12117';
|
||||||
burgLabels.select('#capitals').attr('data-size', Math.max(rn(6 - value / 20), 3));
|
burgLabels.select('#capitals').attr('data-size', Math.max(rn(6 - value / 20), 3));
|
||||||
labels.select('#countries').attr('data-size', Math.max(rn(18 - value / 6), 4));
|
labels.select('#countries').attr('data-size', Math.max(rn(18 - value / 6), 4));
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeBurgsNumberSlider(value) {
|
|
||||||
manorsOutput.value = value == 1000 ? 'auto' : value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function changeUIsize(value) {
|
function changeUIsize(value) {
|
||||||
if (isNaN(+value) || +value < 0.5) return;
|
if (isNaN(+value) || +value < 0.5) return;
|
||||||
|
|
||||||
|
|
@ -408,7 +422,6 @@ function getUImaxSize() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeTooltipSize(value) {
|
function changeTooltipSize(value) {
|
||||||
tooltipSizeInput.value = tooltipSizeOutput.value = value;
|
|
||||||
tooltip.style.fontSize = `calc(${value}px + 0.5vw)`;
|
tooltip.style.fontSize = `calc(${value}px + 0.5vw)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -446,8 +459,9 @@ function applyStoredOptions() {
|
||||||
if (localStorage.getItem('heightUnit')) applyOption(heightUnit, localStorage.getItem('heightUnit'));
|
if (localStorage.getItem('heightUnit')) applyOption(heightUnit, localStorage.getItem('heightUnit'));
|
||||||
|
|
||||||
for (let i = 0; i < localStorage.length; i++) {
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
const stored = localStorage.key(i),
|
const stored = localStorage.key(i);
|
||||||
value = localStorage.getItem(stored);
|
const value = localStorage.getItem(stored);
|
||||||
|
|
||||||
if (stored === 'speakerVoice') continue;
|
if (stored === 'speakerVoice') continue;
|
||||||
const input = document.getElementById(stored + 'Input') || document.getElementById(stored);
|
const input = document.getElementById(stored + 'Input') || document.getElementById(stored);
|
||||||
const output = document.getElementById(stored + 'Output');
|
const output = document.getElementById(stored + 'Output');
|
||||||
|
|
@ -606,23 +620,40 @@ document.getElementById('sticked').addEventListener('click', function (event) {
|
||||||
});
|
});
|
||||||
|
|
||||||
function regeneratePrompt() {
|
function regeneratePrompt() {
|
||||||
if (customization) return tip('New map cannot be generated when edit mode is active, please exit the mode and retry', false, 'error');
|
if (customization) {
|
||||||
const workingMinutes = (Date.now() - last(mapHistory).created) / 60000;
|
tip('New map cannot be generated when edit mode is active, please exit the mode and retry', false, 'error');
|
||||||
if (workingMinutes < 5) return regenerateMap();
|
return;
|
||||||
|
}
|
||||||
|
const workingTime = (Date.now() - last(mapHistory).created) / 60000; // minutes
|
||||||
|
if (workingTime < 5) {
|
||||||
|
regenerateMap();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const message = 'Are you sure you want to generate a new map? <br>All unsaved changes made to the current map will be lost';
|
alertMessage.innerHTML = `Are you sure you want to generate a new map?<br>
|
||||||
const onConfirm = () => {
|
All unsaved changes made to the current map will be lost`;
|
||||||
|
$('#alert').dialog({
|
||||||
|
resizable: false,
|
||||||
|
title: 'Generate new map',
|
||||||
|
buttons: {
|
||||||
|
Cancel: function () {
|
||||||
|
$(this).dialog('close');
|
||||||
|
},
|
||||||
|
Generate: function () {
|
||||||
closeDialogs();
|
closeDialogs();
|
||||||
regenerateMap();
|
regenerateMap();
|
||||||
};
|
}
|
||||||
confirmationDialog({title: 'Generate new map', message, confirm: 'Generate', onConfirm});
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function showSavePane() {
|
function showSavePane() {
|
||||||
|
document.getElementById('showLabels').checked = !hideLabels.checked;
|
||||||
|
|
||||||
$('#saveMapData').dialog({
|
$('#saveMapData').dialog({
|
||||||
title: 'Save map',
|
title: 'Save map',
|
||||||
resizable: false,
|
resizable: false,
|
||||||
width: '27em',
|
width: '30em',
|
||||||
position: {my: 'center', at: 'center', of: 'svg'},
|
position: {my: 'center', at: 'center', of: 'svg'},
|
||||||
buttons: {
|
buttons: {
|
||||||
Close: function () {
|
Close: function () {
|
||||||
|
|
@ -703,6 +734,74 @@ document.getElementById('mapToLoad').addEventListener('change', function () {
|
||||||
uploadMap(fileToLoad);
|
uploadMap(fileToLoad);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function openSaveTiles() {
|
||||||
|
closeDialogs();
|
||||||
|
updateTilesOptions();
|
||||||
|
const status = document.getElementById('tileStatus');
|
||||||
|
status.innerHTML = '';
|
||||||
|
let loading = null;
|
||||||
|
|
||||||
|
$('#saveTilesScreen').dialog({
|
||||||
|
resizable: false,
|
||||||
|
title: 'Download tiles',
|
||||||
|
width: '23em',
|
||||||
|
buttons: {
|
||||||
|
Download: function () {
|
||||||
|
status.innerHTML = 'Preparing for download...';
|
||||||
|
setTimeout(() => (status.innerHTML = 'Downloading. It may take some time.'), 1000);
|
||||||
|
loading = setInterval(() => (status.innerHTML += '.'), 1000);
|
||||||
|
saveTiles().then(() => {
|
||||||
|
clearInterval(loading);
|
||||||
|
status.innerHTML = `Done. Check file in "Downloads" (crtl + J)`;
|
||||||
|
setTimeout(() => (status.innerHTML = ''), 8000);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
Cancel: function () {
|
||||||
|
$(this).dialog('close');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
close: () => {
|
||||||
|
debug.selectAll('*').remove();
|
||||||
|
clearInterval(loading);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document
|
||||||
|
.getElementById('saveTilesScreen')
|
||||||
|
.querySelectorAll('input')
|
||||||
|
.forEach((el) => el.addEventListener('input', updateTilesOptions));
|
||||||
|
|
||||||
|
function updateTilesOptions() {
|
||||||
|
const tileSize = document.getElementById('tileSize');
|
||||||
|
const tilesX = +document.getElementById('tileColsOutput').value;
|
||||||
|
const tilesY = +document.getElementById('tileRowsOutput').value;
|
||||||
|
const scale = +document.getElementById('tileScaleOutput').value;
|
||||||
|
|
||||||
|
// calculate size
|
||||||
|
const sizeX = graphWidth * scale * tilesX;
|
||||||
|
const sizeY = graphHeight * scale * tilesY;
|
||||||
|
const totalSize = sizeX * sizeY;
|
||||||
|
|
||||||
|
tileSize.innerHTML = `${sizeX} x ${sizeY} px`;
|
||||||
|
tileSize.style.color = totalSize > 1e9 ? '#d00b0b' : totalSize > 1e8 ? '#9e6409' : '#1a941a';
|
||||||
|
|
||||||
|
// draw tiles
|
||||||
|
const rects = [];
|
||||||
|
const labels = [];
|
||||||
|
const tileW = (graphWidth / tilesX) | 0;
|
||||||
|
const tileH = (graphHeight / tilesY) | 0;
|
||||||
|
for (let y = 0, i = 0; y + tileH <= graphHeight; y += tileH) {
|
||||||
|
for (let x = 0; x + tileW <= graphWidth; x += tileW, i++) {
|
||||||
|
rects.push(`<rect x=${x} y=${y} width=${tileW} height=${tileH} />`);
|
||||||
|
labels.push(`<text x=${x + tileW / 2} y=${y + tileH / 2}>${i}</text>`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const rectsG = "<g fill='none' stroke='#000'>" + rects.join('') + '</g>';
|
||||||
|
const labelsG = "<g fill='#000' stroke='none' text-anchor='middle' dominant-baseline='central' font-size='24px'>" + labels.join('') + '</g>';
|
||||||
|
debug.html(rectsG + labelsG);
|
||||||
|
}
|
||||||
|
|
||||||
// View mode
|
// View mode
|
||||||
viewMode.addEventListener('click', changeViewMode);
|
viewMode.addEventListener('click', changeViewMode);
|
||||||
function changeViewMode(event) {
|
function changeViewMode(event) {
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ function editProvinces() {
|
||||||
else if (cl.contains('name')) editProvinceName(p);
|
else if (cl.contains('name')) editProvinceName(p);
|
||||||
else if (cl.contains('coaIcon')) editEmblem('province', 'provinceCOA' + p, pack.provinces[p]);
|
else if (cl.contains('coaIcon')) editEmblem('province', 'provinceCOA' + p, pack.provinces[p]);
|
||||||
else if (cl.contains('icon-star-empty')) capitalZoomIn(p);
|
else if (cl.contains('icon-star-empty')) capitalZoomIn(p);
|
||||||
else if (cl.contains('icon-flag-empty')) triggerIndependence(p);
|
else if (cl.contains('icon-flag-empty')) triggerIndependencePromps(p);
|
||||||
else if (cl.contains('culturePopulation')) changePopulation(p);
|
else if (cl.contains('culturePopulation')) changePopulation(p);
|
||||||
else if (cl.contains('icon-pin')) toggleFog(p, cl);
|
else if (cl.contains('icon-pin')) toggleFog(p, cl);
|
||||||
else if (cl.contains('icon-trash-empty')) removeProvince(p);
|
else if (cl.contains('icon-trash-empty')) removeProvince(p);
|
||||||
|
|
@ -118,8 +118,8 @@ function editProvinces() {
|
||||||
for (const p of filtered) {
|
for (const p of filtered) {
|
||||||
const area = p.area * distanceScaleInput.value ** 2;
|
const area = p.area * distanceScaleInput.value ** 2;
|
||||||
totalArea += area;
|
totalArea += area;
|
||||||
const rural = p.rural * populationRate.value;
|
const rural = p.rural * populationRate;
|
||||||
const urban = p.urban * populationRate.value * urbanization.value;
|
const urban = p.urban * populationRate * urbanization;
|
||||||
const population = rn(rural + urban);
|
const population = rn(rural + urban);
|
||||||
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
|
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
|
||||||
totalPopulation += population;
|
totalPopulation += population;
|
||||||
|
|
@ -233,9 +233,21 @@ function editProvinces() {
|
||||||
zoomTo(x, y, 8, 2000);
|
zoomTo(x, y, 8, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function triggerIndependence(p) {
|
function triggerIndependencePromps(p) {
|
||||||
const message = 'Are you sure you want to declare province independence? <br>It will turn province into a new state';
|
alertMessage.innerHTML = 'Are you sure you want to declare province independence? <br>It will turn province into a new state';
|
||||||
confirmationDialog({title: 'Declare independence', message, confirm: 'Declare', onConfirm: () => declareProvinceIndependence(p)});
|
$('#alert').dialog({
|
||||||
|
resizable: false,
|
||||||
|
title: 'Declare independence',
|
||||||
|
buttons: {
|
||||||
|
Declare: function () {
|
||||||
|
declareProvinceIndependence(p);
|
||||||
|
$(this).dialog('close');
|
||||||
|
},
|
||||||
|
Cancel: function () {
|
||||||
|
$(this).dialog('close');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function declareProvinceIndependence(p) {
|
function declareProvinceIndependence(p) {
|
||||||
|
|
@ -328,8 +340,8 @@ function editProvinces() {
|
||||||
tip('Province does not have any cells, cannot change population', false, 'error');
|
tip('Province does not have any cells, cannot change population', false, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const rural = rn(p.rural * populationRate.value);
|
const rural = rn(p.rural * populationRate);
|
||||||
const urban = rn(p.urban * populationRate.value * urbanization.value);
|
const urban = rn(p.urban * populationRate * urbanization);
|
||||||
const total = rural + urban;
|
const total = rural + urban;
|
||||||
const l = (n) => Number(n).toLocaleString();
|
const l = (n) => Number(n).toLocaleString();
|
||||||
|
|
||||||
|
|
@ -370,7 +382,7 @@ function editProvinces() {
|
||||||
cells.forEach((i) => (pack.cells.pop[i] *= ruralChange));
|
cells.forEach((i) => (pack.cells.pop[i] *= ruralChange));
|
||||||
}
|
}
|
||||||
if (!isFinite(ruralChange) && +ruralPop.value > 0) {
|
if (!isFinite(ruralChange) && +ruralPop.value > 0) {
|
||||||
const points = ruralPop.value / populationRate.value;
|
const points = ruralPop.value / populationRate;
|
||||||
const pop = rn(points / cells.length);
|
const pop = rn(points / cells.length);
|
||||||
cells.forEach((i) => (pack.cells.pop[i] = pop));
|
cells.forEach((i) => (pack.cells.pop[i] = pop));
|
||||||
}
|
}
|
||||||
|
|
@ -380,7 +392,7 @@ function editProvinces() {
|
||||||
p.burgs.forEach((b) => (pack.burgs[b].population = rn(pack.burgs[b].population * urbanChange, 4)));
|
p.burgs.forEach((b) => (pack.burgs[b].population = rn(pack.burgs[b].population * urbanChange, 4)));
|
||||||
}
|
}
|
||||||
if (!isFinite(urbanChange) && +urbanPop.value > 0) {
|
if (!isFinite(urbanChange) && +urbanPop.value > 0) {
|
||||||
const points = urbanPop.value / populationRate.value / urbanization.value;
|
const points = urbanPop.value / populationRate / urbanization;
|
||||||
const population = rn(points / burgs.length, 4);
|
const population = rn(points / burgs.length, 4);
|
||||||
p.burgs.forEach((b) => (pack.burgs[b].population = population));
|
p.burgs.forEach((b) => (pack.burgs[b].population = population));
|
||||||
}
|
}
|
||||||
|
|
@ -397,13 +409,17 @@ function editProvinces() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeProvince(p) {
|
function removeProvince(p) {
|
||||||
const message = 'Are you sure you want to remove the province? <br>This action cannot be reverted';
|
alertMessage.innerHTML = `Are you sure you want to remove the province? <br>This action cannot be reverted`;
|
||||||
const onConfirm = () => {
|
$('#alert').dialog({
|
||||||
|
resizable: false,
|
||||||
|
title: 'Remove province',
|
||||||
|
buttons: {
|
||||||
|
Remove: function () {
|
||||||
pack.cells.province.forEach((province, i) => {
|
pack.cells.province.forEach((province, i) => {
|
||||||
if (province === p) pack.cells.province[i] = 0;
|
if (province === p) pack.cells.province[i] = 0;
|
||||||
});
|
});
|
||||||
const s = pack.provinces[p].state;
|
const s = pack.provinces[p].state,
|
||||||
const state = pack.states[s];
|
state = pack.states[s];
|
||||||
if (state.provinces.includes(p)) state.provinces.splice(state.provinces.indexOf(p), 1);
|
if (state.provinces.includes(p)) state.provinces.splice(state.provinces.indexOf(p), 1);
|
||||||
|
|
||||||
unfog('focusProvince' + p);
|
unfog('focusProvince' + p);
|
||||||
|
|
@ -420,8 +436,13 @@ function editProvinces() {
|
||||||
if (!layerIsOn('toggleBorders')) toggleBorders();
|
if (!layerIsOn('toggleBorders')) toggleBorders();
|
||||||
else drawBorders();
|
else drawBorders();
|
||||||
refreshProvincesEditor();
|
refreshProvincesEditor();
|
||||||
};
|
$(this).dialog('close');
|
||||||
confirmationDialog({title: 'Remove province', message, confirm: 'Remove', onConfirm});
|
},
|
||||||
|
Cancel: function () {
|
||||||
|
$(this).dialog('close');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function editProvinceName(province) {
|
function editProvinceName(province) {
|
||||||
|
|
@ -571,8 +592,8 @@ function editProvinces() {
|
||||||
|
|
||||||
const unit = areaUnit.value === 'square' ? ' ' + distanceUnitInput.value + '²' : ' ' + areaUnit.value;
|
const unit = areaUnit.value === 'square' ? ' ' + distanceUnitInput.value + '²' : ' ' + areaUnit.value;
|
||||||
const area = d.data.area * distanceScaleInput.value ** 2 + unit;
|
const area = d.data.area * distanceScaleInput.value ** 2 + unit;
|
||||||
const rural = rn(d.data.rural * populationRate.value);
|
const rural = rn(d.data.rural * populationRate);
|
||||||
const urban = rn(d.data.urban * populationRate.value * urbanization.value);
|
const urban = rn(d.data.urban * populationRate * urbanization);
|
||||||
|
|
||||||
const value =
|
const value =
|
||||||
provincesTreeType.value === 'area'
|
provincesTreeType.value === 'area'
|
||||||
|
|
@ -938,8 +959,8 @@ function editProvinces() {
|
||||||
data += el.dataset.capital + ',';
|
data += el.dataset.capital + ',';
|
||||||
data += el.dataset.area + ',';
|
data += el.dataset.area + ',';
|
||||||
data += el.dataset.population + ',';
|
data += el.dataset.population + ',';
|
||||||
data += `${Math.round(pack.provinces[key].rural * populationRate.value)},`;
|
data += `${Math.round(pack.provinces[key].rural * populationRate)},`;
|
||||||
data += `${Math.round(pack.provinces[key].urban * populationRate.value * urbanization.value)}\n`;
|
data += `${Math.round(pack.provinces[key].urban * populationRate * urbanization)}\n`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const name = getFileName('Provinces') + '.csv';
|
const name = getFileName('Provinces') + '.csv';
|
||||||
|
|
@ -947,8 +968,14 @@ function editProvinces() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeAllProvinces() {
|
function removeAllProvinces() {
|
||||||
const message = `Are you sure you want to remove all provinces? <br>This action cannot be reverted`;
|
alertMessage.innerHTML = `Are you sure you want to remove all provinces? <br>This action cannot be reverted`;
|
||||||
const onConfirm = () => {
|
$('#alert').dialog({
|
||||||
|
resizable: false,
|
||||||
|
title: 'Remove all provinces',
|
||||||
|
buttons: {
|
||||||
|
Remove: function () {
|
||||||
|
$(this).dialog('close');
|
||||||
|
|
||||||
// remove emblems
|
// remove emblems
|
||||||
document.querySelectorAll("[id^='provinceCOA']").forEach((el) => el.remove());
|
document.querySelectorAll("[id^='provinceCOA']").forEach((el) => el.remove());
|
||||||
emblems.select('#provinceEmblems').selectAll('*').remove();
|
emblems.select('#provinceEmblems').selectAll('*').remove();
|
||||||
|
|
@ -965,8 +992,12 @@ function editProvinces() {
|
||||||
turnButtonOff('toggleProvinces');
|
turnButtonOff('toggleProvinces');
|
||||||
|
|
||||||
provincesEditorAddLines();
|
provincesEditorAddLines();
|
||||||
};
|
},
|
||||||
confirmationDialog({title: 'Remove all provinces', message, confirm: 'Remove', onConfirm});
|
Cancel: function () {
|
||||||
|
$(this).dialog('close');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function dragLabel() {
|
function dragLabel() {
|
||||||
|
|
|
||||||
|
|
@ -68,8 +68,8 @@ function editReligions() {
|
||||||
if (r.removed) continue;
|
if (r.removed) continue;
|
||||||
|
|
||||||
const area = r.area * distanceScaleInput.value ** 2;
|
const area = r.area * distanceScaleInput.value ** 2;
|
||||||
const rural = r.rural * populationRate.value;
|
const rural = r.rural * populationRate;
|
||||||
const urban = r.urban * populationRate.value * urbanization.value;
|
const urban = r.urban * populationRate * urbanization;
|
||||||
const population = rn(rural + urban);
|
const population = rn(rural + urban);
|
||||||
if (r.i && !r.cells && body.dataset.extinct !== 'show') continue; // hide extinct religions
|
if (r.i && !r.cells && body.dataset.extinct !== 'show') continue; // hide extinct religions
|
||||||
const populationTip = `Believers: ${si(population)}; Rural areas: ${si(rural)}; Urban areas: ${si(urban)}. Click to change`;
|
const populationTip = `Believers: ${si(population)}; Rural areas: ${si(rural)}; Urban areas: ${si(urban)}. Click to change`;
|
||||||
|
|
@ -160,8 +160,8 @@ function editReligions() {
|
||||||
const r = pack.religions[religion];
|
const r = pack.religions[religion];
|
||||||
const type = r.name.includes(r.type) ? '' : r.type === 'Folk' || r.type === 'Organized' ? '. ' + r.type + ' religion' : '. ' + r.type;
|
const type = r.name.includes(r.type) ? '' : r.type === 'Folk' || r.type === 'Organized' ? '. ' + r.type + ' religion' : '. ' + r.type;
|
||||||
const form = r.form === r.type || r.name.includes(r.form) ? '' : '. ' + r.form;
|
const form = r.form === r.type || r.name.includes(r.form) ? '' : '. ' + r.form;
|
||||||
const rural = r.rural * populationRate.value;
|
const rural = r.rural * populationRate;
|
||||||
const urban = r.urban * populationRate.value * urbanization.value;
|
const urban = r.urban * populationRate * urbanization;
|
||||||
const population = rural + urban > 0 ? '. ' + si(rn(rural + urban)) + ' believers' : '. Extinct';
|
const population = rural + urban > 0 ? '. ' + si(rn(rural + urban)) + ' believers' : '. Extinct';
|
||||||
info.innerHTML = `${r.name}${type}${form}${population}`;
|
info.innerHTML = `${r.name}${type}${form}${population}`;
|
||||||
tip('Drag to change parent, drag to itself to move to the top level. Hold CTRL and click to change abbreviation');
|
tip('Drag to change parent, drag to itself to move to the top level. Hold CTRL and click to change abbreviation');
|
||||||
|
|
@ -273,8 +273,8 @@ function editReligions() {
|
||||||
tip('Religion does not have any cells, cannot change population', false, 'error');
|
tip('Religion does not have any cells, cannot change population', false, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const rural = rn(r.rural * populationRate.value);
|
const rural = rn(r.rural * populationRate);
|
||||||
const urban = rn(r.urban * populationRate.value * urbanization.value);
|
const urban = rn(r.urban * populationRate * urbanization);
|
||||||
const total = rural + urban;
|
const total = rural + urban;
|
||||||
const l = (n) => Number(n).toLocaleString();
|
const l = (n) => Number(n).toLocaleString();
|
||||||
const burgs = pack.burgs.filter((b) => !b.removed && pack.cells.religion[b.cell] === religion);
|
const burgs = pack.burgs.filter((b) => !b.removed && pack.cells.religion[b.cell] === religion);
|
||||||
|
|
@ -318,7 +318,7 @@ function editReligions() {
|
||||||
cells.forEach((i) => (pack.cells.pop[i] *= ruralChange));
|
cells.forEach((i) => (pack.cells.pop[i] *= ruralChange));
|
||||||
}
|
}
|
||||||
if (!isFinite(ruralChange) && +ruralPop.value > 0) {
|
if (!isFinite(ruralChange) && +ruralPop.value > 0) {
|
||||||
const points = ruralPop.value / populationRate.value;
|
const points = ruralPop.value / populationRate;
|
||||||
const cells = pack.cells.i.filter((i) => pack.cells.religion[i] === religion);
|
const cells = pack.cells.i.filter((i) => pack.cells.religion[i] === religion);
|
||||||
const pop = rn(points / cells.length);
|
const pop = rn(points / cells.length);
|
||||||
cells.forEach((i) => (pack.cells.pop[i] = pop));
|
cells.forEach((i) => (pack.cells.pop[i] = pop));
|
||||||
|
|
@ -329,7 +329,7 @@ function editReligions() {
|
||||||
burgs.forEach((b) => (b.population = rn(b.population * urbanChange, 4)));
|
burgs.forEach((b) => (b.population = rn(b.population * urbanChange, 4)));
|
||||||
}
|
}
|
||||||
if (!isFinite(urbanChange) && +urbanPop.value > 0) {
|
if (!isFinite(urbanChange) && +urbanPop.value > 0) {
|
||||||
const points = urbanPop.value / populationRate.value / urbanization.value;
|
const points = urbanPop.value / populationRate / urbanization;
|
||||||
const population = rn(points / burgs.length, 4);
|
const population = rn(points / burgs.length, 4);
|
||||||
burgs.forEach((b) => (b.population = population));
|
burgs.forEach((b) => (b.population = population));
|
||||||
}
|
}
|
||||||
|
|
@ -342,8 +342,12 @@ function editReligions() {
|
||||||
if (customization) return;
|
if (customization) return;
|
||||||
const religion = +this.parentNode.dataset.id;
|
const religion = +this.parentNode.dataset.id;
|
||||||
|
|
||||||
const message = 'Are you sure you want to remove the religion? <br>This action cannot be reverted';
|
alertMessage.innerHTML = 'Are you sure you want to remove the religion? <br>This action cannot be reverted';
|
||||||
const onConfirm = () => {
|
$('#alert').dialog({
|
||||||
|
resizable: false,
|
||||||
|
title: 'Remove religion',
|
||||||
|
buttons: {
|
||||||
|
Remove: function () {
|
||||||
relig.select('#religion' + religion).remove();
|
relig.select('#religion' + religion).remove();
|
||||||
relig.select('#religion-gap' + religion).remove();
|
relig.select('#religion-gap' + religion).remove();
|
||||||
debug.select('#religionsCenter' + religion).remove();
|
debug.select('#religionsCenter' + religion).remove();
|
||||||
|
|
@ -358,8 +362,13 @@ function editReligions() {
|
||||||
});
|
});
|
||||||
|
|
||||||
refreshReligionsEditor();
|
refreshReligionsEditor();
|
||||||
};
|
$(this).dialog('close');
|
||||||
confirmationDialog({title: 'Remove religion', message, confirm: 'Remove', onConfirm});
|
},
|
||||||
|
Cancel: function () {
|
||||||
|
$(this).dialog('close');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawReligionCenters() {
|
function drawReligionCenters() {
|
||||||
|
|
|
||||||
|
|
@ -89,8 +89,8 @@ function editStates() {
|
||||||
for (const s of pack.states) {
|
for (const s of pack.states) {
|
||||||
if (s.removed) continue;
|
if (s.removed) continue;
|
||||||
const area = s.area * distanceScaleInput.value ** 2;
|
const area = s.area * distanceScaleInput.value ** 2;
|
||||||
const rural = s.rural * populationRate.value;
|
const rural = s.rural * populationRate;
|
||||||
const urban = s.urban * populationRate.value * urbanization.value;
|
const urban = s.urban * populationRate * urbanization;
|
||||||
const population = rn(rural + urban);
|
const population = rn(rural + urban);
|
||||||
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}. Click to change`;
|
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}. Click to change`;
|
||||||
totalArea += area;
|
totalArea += area;
|
||||||
|
|
@ -362,8 +362,8 @@ function editStates() {
|
||||||
tip('State does not have any cells, cannot change population', false, 'error');
|
tip('State does not have any cells, cannot change population', false, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const rural = rn(s.rural * populationRate.value);
|
const rural = rn(s.rural * populationRate);
|
||||||
const urban = rn(s.urban * populationRate.value * urbanization.value);
|
const urban = rn(s.urban * populationRate * urbanization);
|
||||||
const total = rural + urban;
|
const total = rural + urban;
|
||||||
const l = (n) => Number(n).toLocaleString();
|
const l = (n) => Number(n).toLocaleString();
|
||||||
|
|
||||||
|
|
@ -405,7 +405,7 @@ function editStates() {
|
||||||
cells.forEach((i) => (pack.cells.pop[i] *= ruralChange));
|
cells.forEach((i) => (pack.cells.pop[i] *= ruralChange));
|
||||||
}
|
}
|
||||||
if (!isFinite(ruralChange) && +ruralPop.value > 0) {
|
if (!isFinite(ruralChange) && +ruralPop.value > 0) {
|
||||||
const points = ruralPop.value / populationRate.value;
|
const points = ruralPop.value / populationRate;
|
||||||
const cells = pack.cells.i.filter((i) => pack.cells.state[i] === state);
|
const cells = pack.cells.i.filter((i) => pack.cells.state[i] === state);
|
||||||
const pop = points / cells.length;
|
const pop = points / cells.length;
|
||||||
cells.forEach((i) => (pack.cells.pop[i] = pop));
|
cells.forEach((i) => (pack.cells.pop[i] = pop));
|
||||||
|
|
@ -417,7 +417,7 @@ function editStates() {
|
||||||
burgs.forEach((b) => (b.population = rn(b.population * urbanChange, 4)));
|
burgs.forEach((b) => (b.population = rn(b.population * urbanChange, 4)));
|
||||||
}
|
}
|
||||||
if (!isFinite(urbanChange) && +urbanPop.value > 0) {
|
if (!isFinite(urbanChange) && +urbanPop.value > 0) {
|
||||||
const points = urbanPop.value / populationRate.value / urbanization.value;
|
const points = urbanPop.value / populationRate / urbanization;
|
||||||
const burgs = pack.burgs.filter((b) => !b.removed && b.state === state);
|
const burgs = pack.burgs.filter((b) => !b.removed && b.state === state);
|
||||||
const population = rn(points / burgs.length, 4);
|
const population = rn(points / burgs.length, 4);
|
||||||
burgs.forEach((b) => (b.population = population));
|
burgs.forEach((b) => (b.population = population));
|
||||||
|
|
@ -459,8 +459,21 @@ function editStates() {
|
||||||
|
|
||||||
function stateRemovePrompt(state) {
|
function stateRemovePrompt(state) {
|
||||||
if (customization) return;
|
if (customization) return;
|
||||||
const message = 'Are you sure you want to remove the state? <br>This action cannot be reverted';
|
|
||||||
confirmationDialog({title: 'Remove state', message, confirm: 'Remove', onConfirm: () => stateRemove(state)});
|
alertMessage.innerHTML = 'Are you sure you want to remove the state? <br>This action cannot be reverted';
|
||||||
|
$('#alert').dialog({
|
||||||
|
resizable: false,
|
||||||
|
title: 'Remove state',
|
||||||
|
buttons: {
|
||||||
|
Remove: function () {
|
||||||
|
$(this).dialog('close');
|
||||||
|
stateRemove(state);
|
||||||
|
},
|
||||||
|
Cancel: function () {
|
||||||
|
$(this).dialog('close');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function stateRemove(state) {
|
function stateRemove(state) {
|
||||||
|
|
@ -627,8 +640,8 @@ function editStates() {
|
||||||
|
|
||||||
const unit = areaUnit.value === 'square' ? ' ' + distanceUnitInput.value + '²' : ' ' + areaUnit.value;
|
const unit = areaUnit.value === 'square' ? ' ' + distanceUnitInput.value + '²' : ' ' + areaUnit.value;
|
||||||
const area = d.data.area * distanceScaleInput.value ** 2 + unit;
|
const area = d.data.area * distanceScaleInput.value ** 2 + unit;
|
||||||
const rural = rn(d.data.rural * populationRate.value);
|
const rural = rn(d.data.rural * populationRate);
|
||||||
const urban = rn(d.data.urban * populationRate.value * urbanization.value);
|
const urban = rn(d.data.urban * populationRate * urbanization);
|
||||||
|
|
||||||
const option = statesTreeType.value;
|
const option = statesTreeType.value;
|
||||||
const value =
|
const value =
|
||||||
|
|
@ -1056,8 +1069,8 @@ function editStates() {
|
||||||
data += el.dataset.burgs + ',';
|
data += el.dataset.burgs + ',';
|
||||||
data += el.dataset.area + ',';
|
data += el.dataset.area + ',';
|
||||||
data += el.dataset.population + ',';
|
data += el.dataset.population + ',';
|
||||||
data += `${Math.round(pack.states[key].rural * populationRate.value)},`;
|
data += `${Math.round(pack.states[key].rural * populationRate)},`;
|
||||||
data += `${Math.round(pack.states[key].urban * populationRate.value * urbanization.value)}\n`;
|
data += `${Math.round(pack.states[key].urban * populationRate * urbanization)}\n`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const name = getFileName('States') + '.csv';
|
const name = getFileName('States') + '.csv';
|
||||||
|
|
|
||||||
1003
modules/ui/style.js
1003
modules/ui/style.js
File diff suppressed because one or more lines are too long
|
|
@ -1,93 +1,110 @@
|
||||||
// module to control the Tools options (click to edit, to re-geenerate, tp add)
|
// module to control the Tools options (click to edit, to re-geenerate, tp add)
|
||||||
"use strict";
|
'use strict';
|
||||||
|
|
||||||
toolsContent.addEventListener("click", function(event) {
|
toolsContent.addEventListener('click', function (event) {
|
||||||
if (customization) {tip("Please exit the customization mode first", false, "warning"); return;}
|
if (customization) {
|
||||||
if (event.target.tagName !== "BUTTON") return;
|
tip('Please exit the customization mode first', false, 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.target.tagName !== 'BUTTON') return;
|
||||||
const button = event.target.id;
|
const button = event.target.id;
|
||||||
|
|
||||||
// Click to open Editor buttons
|
// Click to open Editor buttons
|
||||||
if (button === "editHeightmapButton") editHeightmap(); else
|
if (button === 'editHeightmapButton') editHeightmap();
|
||||||
if (button === "editBiomesButton") editBiomes(); else
|
else if (button === 'editBiomesButton') editBiomes();
|
||||||
if (button === "editStatesButton") editStates(); else
|
else if (button === 'editStatesButton') editStates();
|
||||||
if (button === "editProvincesButton") editProvinces(); else
|
else if (button === 'editProvincesButton') editProvinces();
|
||||||
if (button === "editDiplomacyButton") editDiplomacy(); else
|
else if (button === 'editDiplomacyButton') editDiplomacy();
|
||||||
if (button === "editCulturesButton") editCultures(); else
|
else if (button === 'editCulturesButton') editCultures();
|
||||||
if (button === "editReligions") editReligions(); else
|
else if (button === 'editReligions') editReligions();
|
||||||
if (button === "editResources") editResources(); else
|
else if (button === 'editEmblemButton') openEmblemEditor();
|
||||||
if (button === "editEmblemButton") openEmblemEditor(); else
|
else if (button === 'editNamesBaseButton') editNamesbase();
|
||||||
if (button === "editNamesBaseButton") editNamesbase(); else
|
else if (button === 'editUnitsButton') editUnits();
|
||||||
if (button === "editUnitsButton") editUnits(); else
|
else if (button === 'editNotesButton') editNotes();
|
||||||
if (button === "editNotesButton") editNotes(); else
|
else if (button === 'editZonesButton') editZones();
|
||||||
if (button === "editZonesButton") editZones(); else
|
else if (button === 'overviewBurgsButton') overviewBurgs();
|
||||||
if (button === "overviewBurgsButton") overviewBurgs(); else
|
else if (button === 'overviewRiversButton') overviewRivers();
|
||||||
if (button === "overviewRiversButton") overviewRivers(); else
|
else if (button === 'overviewMilitaryButton') overviewMilitary();
|
||||||
if (button === "overviewMilitaryButton") overviewMilitary(); else
|
else if (button === 'overviewCellsButton') viewCellDetails();
|
||||||
if (button === "overviewCellsButton") viewCellDetails();
|
|
||||||
|
|
||||||
// Click to Regenerate buttons
|
// Click to Regenerate buttons
|
||||||
if (event.target.parentNode.id === "regenerateFeature") {
|
if (event.target.parentNode.id === 'regenerateFeature') {
|
||||||
if (sessionStorage.getItem("regenerateFeatureDontAsk")) {processFeatureRegeneration(event, button); return;}
|
if (sessionStorage.getItem('regenerateFeatureDontAsk')) {
|
||||||
|
processFeatureRegeneration(event, button);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
alertMessage.innerHTML = `Regeneration will remove all the custom changes for the element.<br><br>Are you sure you want to proceed?`
|
alertMessage.innerHTML = `Regeneration will remove all the custom changes for the element.<br><br>Are you sure you want to proceed?`;
|
||||||
$("#alert").dialog({resizable: false, title: "Regenerate element",
|
$('#alert').dialog({
|
||||||
|
resizable: false,
|
||||||
|
title: 'Regenerate element',
|
||||||
buttons: {
|
buttons: {
|
||||||
Proceed: function() {processFeatureRegeneration(event, button); $(this).dialog("close");},
|
Proceed: function () {
|
||||||
Cancel: function() {$(this).dialog("close");}
|
processFeatureRegeneration(event, button);
|
||||||
|
$(this).dialog('close');
|
||||||
|
},
|
||||||
|
Cancel: function () {
|
||||||
|
$(this).dialog('close');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
open: function () {
|
open: function () {
|
||||||
const pane = $(this).dialog("widget").find(".ui-dialog-buttonpane");
|
const pane = $(this).dialog('widget').find('.ui-dialog-buttonpane');
|
||||||
$('<span><input id="dontAsk" class="checkbox" type="checkbox"><label for="dontAsk" class="checkbox-label dontAsk"><i>do not ask again</i></label><span>').prependTo(pane);
|
$('<span><input id="dontAsk" class="checkbox" type="checkbox"><label for="dontAsk" class="checkbox-label dontAsk"><i>do not ask again</i></label><span>').prependTo(pane);
|
||||||
},
|
},
|
||||||
close: function () {
|
close: function () {
|
||||||
const box = $(this).dialog("widget").find(".checkbox")[0];
|
const box = $(this).dialog('widget').find('.checkbox')[0];
|
||||||
if (!box) return;
|
if (!box) return;
|
||||||
if (box.checked) sessionStorage.setItem("regenerateFeatureDontAsk", true);
|
if (box.checked) sessionStorage.setItem('regenerateFeatureDontAsk', true);
|
||||||
$(this).dialog("destroy");
|
$(this).dialog('destroy');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Click to Add buttons
|
// Click to Add buttons
|
||||||
if (button === "addLabel") toggleAddLabel(); else
|
if (button === 'addLabel') toggleAddLabel();
|
||||||
if (button === "addBurgTool") toggleAddBurg(); else
|
else if (button === 'addBurgTool') toggleAddBurg();
|
||||||
if (button === "addRiver") toggleAddRiver(); else
|
else if (button === 'addRiver') toggleAddRiver();
|
||||||
if (button === "addRoute") toggleAddRoute(); else
|
else if (button === 'addRoute') toggleAddRoute();
|
||||||
if (button === "addMarker") toggleAddMarker();
|
else if (button === 'addMarker') toggleAddMarker();
|
||||||
});
|
});
|
||||||
|
|
||||||
function processFeatureRegeneration(event, button) {
|
function processFeatureRegeneration(event, button) {
|
||||||
if (button === "regenerateStateLabels") {BurgsAndStates.drawStateLabels(); if (!layerIsOn("toggleLabels")) toggleLabels();} else
|
if (button === 'regenerateStateLabels') {
|
||||||
if (button === "regenerateReliefIcons") {ReliefIcons(); if (!layerIsOn("toggleRelief")) toggleRelief();} else
|
BurgsAndStates.drawStateLabels();
|
||||||
if (button === "regenerateRoutes") {Routes.regenerate(); if (!layerIsOn("toggleRoutes")) toggleRoutes();} else
|
if (!layerIsOn('toggleLabels')) toggleLabels();
|
||||||
if (button === "regenerateRivers") regenerateRivers(); else
|
} else if (button === 'regenerateReliefIcons') {
|
||||||
if (button === "regeneratePopulation") recalculatePopulation(); else
|
ReliefIcons();
|
||||||
if (button === "regenerateStates") regenerateStates(); else
|
if (!layerIsOn('toggleRelief')) toggleRelief();
|
||||||
if (button === "regenerateProvinces") regenerateProvinces(); else
|
} else if (button === 'regenerateRoutes') {
|
||||||
if (button === "regenerateBurgs") regenerateBurgs(); else
|
Routes.regenerate();
|
||||||
if (button === "regenerateResources") regenerateResources(); else
|
if (!layerIsOn('toggleRoutes')) toggleRoutes();
|
||||||
if (button === "regenerateEmblems") regenerateEmblems(); else
|
} else if (button === 'regenerateRivers') regenerateRivers();
|
||||||
if (button === "regenerateReligions") regenerateReligions(); else
|
else if (button === 'regeneratePopulation') recalculatePopulation();
|
||||||
if (button === "regenerateCultures") regenerateCultures(); else
|
else if (button === 'regenerateStates') regenerateStates();
|
||||||
if (button === "regenerateMilitary") regenerateMilitary(); else
|
else if (button === 'regenerateProvinces') regenerateProvinces();
|
||||||
if (button === "regenerateIce") regenerateIce(); else
|
else if (button === 'regenerateBurgs') regenerateBurgs();
|
||||||
if (button === "regenerateMarkers") regenerateMarkers(event); else
|
else if (button === 'regenerateEmblems') regenerateEmblems();
|
||||||
if (button === "regenerateZones") regenerateZones(event);
|
else if (button === 'regenerateReligions') regenerateReligions();
|
||||||
|
else if (button === 'regenerateCultures') regenerateCultures();
|
||||||
|
else if (button === 'regenerateMilitary') regenerateMilitary();
|
||||||
|
else if (button === 'regenerateIce') regenerateIce();
|
||||||
|
else if (button === 'regenerateMarkers') regenerateMarkers(event);
|
||||||
|
else if (button === 'regenerateZones') regenerateZones(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openEmblemEditor() {
|
async function openEmblemEditor() {
|
||||||
let type, id, el;
|
let type, id, el;
|
||||||
|
|
||||||
if (pack.states[1]?.coa) {
|
if (pack.states[1]?.coa) {
|
||||||
type = "state";
|
type = 'state';
|
||||||
id = "stateCOA1";
|
id = 'stateCOA1';
|
||||||
el = pack.states[1];
|
el = pack.states[1];
|
||||||
} else if (pack.burgs[1]?.coa) {
|
} else if (pack.burgs[1]?.coa) {
|
||||||
type = "burg";
|
type = 'burg';
|
||||||
id = "burgCOA1";
|
id = 'burgCOA1';
|
||||||
el = pack.burgs[1];
|
el = pack.burgs[1];
|
||||||
} else {
|
} else {
|
||||||
tip("No emblems to edit, please generate states and burgs first", false, "error");
|
tip('No emblems to edit, please generate states and burgs first', false, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,95 +116,102 @@ function regenerateRivers() {
|
||||||
Rivers.generate();
|
Rivers.generate();
|
||||||
Lakes.defineGroup();
|
Lakes.defineGroup();
|
||||||
Rivers.specify();
|
Rivers.specify();
|
||||||
if (!layerIsOn("toggleRivers")) toggleRivers();
|
if (!layerIsOn('toggleRivers')) toggleRivers();
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
b.population = rn(Math.max((pack.cells.s[i] + pack.cells.road[i] / 2) / 8 + b.i / 1000 + i % 100 / 1000, .1), 3);
|
b.population = rn(Math.max((pack.cells.s[i] + pack.cells.road[i] / 2) / 8 + b.i / 1000 + (i % 100) / 1000, 0.1), 3);
|
||||||
if (b.capital) b.population = b.population * 1.3; // increase capital population
|
if (b.capital) b.population = b.population * 1.3; // increase capital population
|
||||||
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,.6,20,3), 3);
|
b.population = rn(b.population * gauss(2, 3, 0.6, 20, 3), 3);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function regenerateStates() {
|
function regenerateStates() {
|
||||||
const localSeed = Math.floor(Math.random() * 1e9); // new random seed
|
const localSeed = Math.floor(Math.random() * 1e9); // new random seed
|
||||||
Math.random = aleaPRNG(localSeed);
|
Math.random = aleaPRNG(localSeed);
|
||||||
const burgs = pack.burgs.filter(b => b.i && !b.removed);
|
const burgs = pack.burgs.filter((b) => b.i && !b.removed);
|
||||||
if (!burgs.length) {
|
if (!burgs.length) {
|
||||||
tip("No burgs to generate states. Please create burgs first", false, "error");
|
tip('No burgs to generate states. Please create burgs first', false, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (burgs.length < +regionsInput.value) {
|
if (burgs.length < +regionsInput.value) {
|
||||||
tip(`Not enough burgs to generate ${regionsInput.value} states. Will generate only ${burgs.length} states`, false, "warn");
|
tip(`Not enough burgs to generate ${regionsInput.value} states. Will generate only ${burgs.length} states`, false, 'warn');
|
||||||
}
|
}
|
||||||
|
|
||||||
// burg local ids sorted by a bit randomized population:
|
// burg local ids sorted by a bit randomized population:
|
||||||
const sorted = burgs.map((b, i) => [i, b.population * Math.random()]).sort((a, b) => b[1] - a[1]).map(b => b[0]);
|
const sorted = burgs
|
||||||
|
.map((b, i) => [i, b.population * Math.random()])
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.map((b) => b[0]);
|
||||||
const capitalsTree = d3.quadtree();
|
const capitalsTree = d3.quadtree();
|
||||||
|
|
||||||
// turn all old capitals into towns
|
// turn all old capitals into towns
|
||||||
burgs.filter(b => b.capital).forEach(b => {
|
burgs
|
||||||
moveBurgToGroup(b.i, "towns");
|
.filter((b) => b.capital)
|
||||||
|
.forEach((b) => {
|
||||||
|
moveBurgToGroup(b.i, 'towns');
|
||||||
b.capital = 0;
|
b.capital = 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
// remove emblems
|
// remove emblems
|
||||||
document.querySelectorAll("[id^=stateCOA]").forEach(el => el.remove());
|
document.querySelectorAll('[id^=stateCOA]').forEach((el) => el.remove());
|
||||||
document.querySelectorAll("[id^=provinceCOA]").forEach(el => el.remove());
|
document.querySelectorAll('[id^=provinceCOA]').forEach((el) => el.remove());
|
||||||
emblems.selectAll("use").remove();
|
emblems.selectAll('use').remove();
|
||||||
|
|
||||||
unfog();
|
unfog();
|
||||||
|
|
||||||
// if desired states number is 0
|
// if desired states number is 0
|
||||||
if (regionsInput.value == 0) {
|
if (regionsInput.value == 0) {
|
||||||
tip(`Cannot generate zero states. Please check the <i>States Number</i> option`, false, "warn");
|
tip(`Cannot generate zero states. Please check the <i>States Number</i> option`, false, 'warn');
|
||||||
pack.states = pack.states.slice(0, 1); // remove all except of neutrals
|
pack.states = pack.states.slice(0, 1); // remove all except of neutrals
|
||||||
pack.states[0].diplomacy = []; // clear diplomacy
|
pack.states[0].diplomacy = []; // clear diplomacy
|
||||||
pack.provinces = [0]; // remove all provinces
|
pack.provinces = [0]; // remove all provinces
|
||||||
pack.cells.state = new Uint16Array(pack.cells.i.length); // reset cells data
|
pack.cells.state = new Uint16Array(pack.cells.i.length); // reset cells data
|
||||||
borders.selectAll("path").remove(); // remove borders
|
borders.selectAll('path').remove(); // remove borders
|
||||||
regions.selectAll("path").remove(); // remove states fill
|
regions.selectAll('path').remove(); // remove states fill
|
||||||
labels.select("#states").selectAll("text"); // remove state labels
|
labels.select('#states').selectAll('text'); // remove state labels
|
||||||
defs.select("#textPaths").selectAll("path[id*='stateLabel']").remove(); // remove state labels paths
|
defs.select('#textPaths').selectAll("path[id*='stateLabel']").remove(); // remove state labels paths
|
||||||
|
|
||||||
if (document.getElementById("burgsOverviewRefresh").offsetParent) burgsOverviewRefresh.click();
|
if (document.getElementById('burgsOverviewRefresh').offsetParent) burgsOverviewRefresh.click();
|
||||||
if (document.getElementById("statesEditorRefresh").offsetParent) statesEditorRefresh.click();
|
if (document.getElementById('statesEditorRefresh').offsetParent) statesEditorRefresh.click();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const neutral = pack.states[0].name;
|
const neutral = pack.states[0].name;
|
||||||
const count = Math.min(+regionsInput.value, burgs.length);
|
const count = Math.min(+regionsInput.value, burgs.length);
|
||||||
let spacing = (graphWidth + graphHeight) / 2 / count; // min distance between capitals
|
let spacing = (graphWidth + graphHeight) / 2 / count; // min distance between capitals
|
||||||
pack.states = d3.range(count).map(i => {
|
pack.states = d3.range(count).map((i) => {
|
||||||
if (!i) return {i, name: neutral};
|
if (!i) return {i, name: neutral};
|
||||||
|
|
||||||
let capital = null, x = 0, y = 0;
|
let capital = null,
|
||||||
|
x = 0,
|
||||||
|
y = 0;
|
||||||
for (const i of sorted) {
|
for (const i of sorted) {
|
||||||
capital = burgs[i];
|
capital = burgs[i];
|
||||||
x = capital.x, y = capital.y;
|
(x = capital.x), (y = capital.y);
|
||||||
if (capitalsTree.find(x, y, spacing) === undefined) break;
|
if (capitalsTree.find(x, y, spacing) === undefined) break;
|
||||||
spacing = Math.max(spacing - 1, 1);
|
spacing = Math.max(spacing - 1, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
capitalsTree.add([x, y]);
|
capitalsTree.add([x, y]);
|
||||||
capital.capital = 1;
|
capital.capital = 1;
|
||||||
moveBurgToGroup(capital.i, "cities");
|
moveBurgToGroup(capital.i, 'cities');
|
||||||
|
|
||||||
const culture = capital.culture;
|
const culture = capital.culture;
|
||||||
const basename = capital.name.length < 9 && capital.cell%5 === 0 ? capital.name : Names.getCulture(culture, 3, 6, "", 0);
|
const basename = capital.name.length < 9 && capital.cell % 5 === 0 ? capital.name : Names.getCulture(culture, 3, 6, '', 0);
|
||||||
const name = Names.getState(basename, culture);
|
const name = Names.getState(basename, culture);
|
||||||
const nomadic = [1, 2, 3, 4].includes(pack.cells.biome[capital.cell]);
|
const nomadic = [1, 2, 3, 4].includes(pack.cells.biome[capital.cell]);
|
||||||
const type = nomadic ? "Nomadic" : pack.cultures[culture].type === "Nomadic" ? "Generic" : pack.cultures[culture].type;
|
const type = nomadic ? 'Nomadic' : pack.cultures[culture].type === 'Nomadic' ? 'Generic' : pack.cultures[culture].type;
|
||||||
const expansionism = rn(Math.random() * powerInput.value + 1, 1);
|
const expansionism = rn(Math.random() * powerInput.value + 1, 1);
|
||||||
|
|
||||||
const cultureType = pack.cultures[culture].type;
|
const cultureType = pack.cultures[culture].type;
|
||||||
const coa = COA.generate(capital.coa, .3, null, cultureType);
|
const coa = COA.generate(capital.coa, 0.3, null, cultureType);
|
||||||
coa.shield = capital.coa.shield;
|
coa.shield = capital.coa.shield;
|
||||||
|
|
||||||
return {i, name, type, capital: capital.i, center: capital.cell, culture, expansionism, coa};
|
return {i, name, type, capital: capital.i, center: capital.cell, culture, expansionism, coa};
|
||||||
|
|
@ -201,15 +225,17 @@ function regenerateStates() {
|
||||||
BurgsAndStates.generateDiplomacy();
|
BurgsAndStates.generateDiplomacy();
|
||||||
BurgsAndStates.defineStateForms();
|
BurgsAndStates.defineStateForms();
|
||||||
BurgsAndStates.generateProvinces(true);
|
BurgsAndStates.generateProvinces(true);
|
||||||
if (!layerIsOn("toggleStates")) toggleStates(); else drawStates();
|
if (!layerIsOn('toggleStates')) toggleStates();
|
||||||
if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders();
|
else drawStates();
|
||||||
|
if (!layerIsOn('toggleBorders')) toggleBorders();
|
||||||
|
else drawBorders();
|
||||||
BurgsAndStates.drawStateLabels();
|
BurgsAndStates.drawStateLabels();
|
||||||
Military.generate();
|
Military.generate();
|
||||||
if (layerIsOn("toggleEmblems")) drawEmblems(); // redrawEmblems
|
if (layerIsOn('toggleEmblems')) drawEmblems(); // redrawEmblems
|
||||||
|
|
||||||
if (document.getElementById("burgsOverviewRefresh").offsetParent) burgsOverviewRefresh.click();
|
if (document.getElementById('burgsOverviewRefresh').offsetParent) burgsOverviewRefresh.click();
|
||||||
if (document.getElementById("statesEditorRefresh").offsetParent) statesEditorRefresh.click();
|
if (document.getElementById('statesEditorRefresh').offsetParent) statesEditorRefresh.click();
|
||||||
if (document.getElementById("militaryOverviewRefresh").offsetParent) militaryOverviewRefresh.click();
|
if (document.getElementById('militaryOverviewRefresh').offsetParent) militaryOverviewRefresh.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
function regenerateProvinces() {
|
function regenerateProvinces() {
|
||||||
|
|
@ -217,28 +243,32 @@ function regenerateProvinces() {
|
||||||
|
|
||||||
BurgsAndStates.generateProvinces(true);
|
BurgsAndStates.generateProvinces(true);
|
||||||
drawBorders();
|
drawBorders();
|
||||||
if (layerIsOn("toggleProvinces")) drawProvinces();
|
if (layerIsOn('toggleProvinces')) drawProvinces();
|
||||||
|
|
||||||
// remove emblems
|
// remove emblems
|
||||||
document.querySelectorAll("[id^=provinceCOA]").forEach(el => el.remove());
|
document.querySelectorAll('[id^=provinceCOA]').forEach((el) => el.remove());
|
||||||
emblems.selectAll("use").remove();
|
emblems.selectAll('use').remove();
|
||||||
if (layerIsOn("toggleEmblems")) drawEmblems();
|
if (layerIsOn('toggleEmblems')) drawEmblems();
|
||||||
}
|
}
|
||||||
|
|
||||||
function regenerateBurgs() {
|
function regenerateBurgs() {
|
||||||
const cells = pack.cells, states = pack.states, Lockedburgs = pack.burgs.filter(b =>b.lock);
|
const cells = pack.cells,
|
||||||
|
states = pack.states,
|
||||||
|
Lockedburgs = pack.burgs.filter((b) => b.lock);
|
||||||
rankCells();
|
rankCells();
|
||||||
cells.burg = new Uint16Array(cells.i.length);
|
cells.burg = new Uint16Array(cells.i.length);
|
||||||
const burgs = pack.burgs = [0]; // clear burgs array
|
const burgs = (pack.burgs = [0]); // clear burgs array
|
||||||
states.filter(s => s.i).forEach(s => s.capital = 0); // clear state capitals
|
states.filter((s) => s.i).forEach((s) => (s.capital = 0)); // clear state capitals
|
||||||
pack.provinces.filter(p => p.i).forEach(p => p.burg = 0); // clear province capitals
|
pack.provinces.filter((p) => p.i).forEach((p) => (p.burg = 0)); // clear province capitals
|
||||||
const burgsTree = d3.quadtree();
|
const burgsTree = d3.quadtree();
|
||||||
|
|
||||||
const score = new Int16Array(cells.s.map(s => s * Math.random())); // cell score for capitals placement
|
const score = new Int16Array(cells.s.map((s) => s * Math.random())); // cell score for capitals placement
|
||||||
const sorted = cells.i.filter(i => score[i] > 0 && cells.culture[i]).sort((a, b) => score[b] - score[a]); // filtered and sorted array of indexes
|
const sorted = cells.i.filter((i) => score[i] > 0 && cells.culture[i]).sort((a, b) => score[b] - score[a]); // filtered and sorted array of indexes
|
||||||
const burgsCount = manorsInput.value == 1000 ? rn(sorted.length / 5 / (grid.points.length / 10000) ** .8) + states.length : +manorsInput.value + states.length;
|
const burgsCount = manorsInput.value == 1000 ? rn(sorted.length / 5 / (grid.points.length / 10000) ** 0.8) + states.length : +manorsInput.value + states.length;
|
||||||
const spacing = (graphWidth + graphHeight) / 150 / (burgsCount ** .7 / 66); // base min distance between towns
|
const spacing = (graphWidth + graphHeight) / 150 / (burgsCount ** 0.7 / 66); // base min distance between towns
|
||||||
|
|
||||||
|
//clear locked list since ids will change
|
||||||
|
//burglock.selectAll("text").remove();
|
||||||
for (let j = 0; j < Lockedburgs.length; j++) {
|
for (let j = 0; j < Lockedburgs.length; j++) {
|
||||||
const id = burgs.length;
|
const id = burgs.length;
|
||||||
const oldBurg = Lockedburgs[j];
|
const oldBurg = Lockedburgs[j];
|
||||||
|
|
@ -246,20 +276,28 @@ function regenerateBurgs() {
|
||||||
burgs.push(oldBurg);
|
burgs.push(oldBurg);
|
||||||
burgsTree.add([oldBurg.x, oldBurg.y]);
|
burgsTree.add([oldBurg.x, oldBurg.y]);
|
||||||
cells.burg[oldBurg.cell] = id;
|
cells.burg[oldBurg.cell] = id;
|
||||||
if (oldBurg.capital) {states[oldBurg.state].capital = id; states[oldBurg.state].center = oldBurg.cell;}
|
if (oldBurg.capital) {
|
||||||
|
states[oldBurg.state].capital = id;
|
||||||
|
states[oldBurg.state].center = oldBurg.cell;
|
||||||
|
}
|
||||||
|
//burglock.append("text").attr("data-id", id);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < sorted.length && burgs.length < burgsCount; i++) {
|
for (let i = 0; i < sorted.length && burgs.length < burgsCount; i++) {
|
||||||
const id = burgs.length;
|
const id = burgs.length;
|
||||||
const cell = sorted[i];
|
const cell = sorted[i];
|
||||||
const x = cells.p[cell][0], y = cells.p[cell][1];
|
const x = cells.p[cell][0],
|
||||||
|
y = cells.p[cell][1];
|
||||||
|
|
||||||
const s = spacing * gauss(1, .3, .2, 2, 2); // randomize to make the placement not uniform
|
const s = spacing * gauss(1, 0.3, 0.2, 2, 2); // randomize to make the placement not uniform
|
||||||
if (burgsTree.find(x, y, s) !== undefined) continue; // to close to existing burg
|
if (burgsTree.find(x, y, s) !== undefined) continue; // to close to existing burg
|
||||||
|
|
||||||
const state = cells.state[cell];
|
const state = cells.state[cell];
|
||||||
const capital = state && !states[state].capital; // if state doesn't have capital, make this burg a capital, no capital for neutral lands
|
const capital = state && !states[state].capital; // if state doesn't have capital, make this burg a capital, no capital for neutral lands
|
||||||
if (capital) {states[state].capital = id; states[state].center = cell;}
|
if (capital) {
|
||||||
|
states[state].capital = id;
|
||||||
|
states[state].center = cell;
|
||||||
|
}
|
||||||
|
|
||||||
const culture = cells.culture[cell];
|
const culture = cells.culture[cell];
|
||||||
const name = Names.getCulture(culture);
|
const name = Names.getCulture(culture);
|
||||||
|
|
@ -269,92 +307,97 @@ function regenerateBurgs() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// add a capital at former place for states without added capitals
|
// add a capital at former place for states without added capitals
|
||||||
states.filter(s => s.i && !s.removed && !s.capital).forEach(s => {
|
states
|
||||||
|
.filter((s) => s.i && !s.removed && !s.capital)
|
||||||
|
.forEach((s) => {
|
||||||
const burg = addBurg([cells.p[s.center][0], cells.p[s.center][1]]); // add new burg
|
const burg = addBurg([cells.p[s.center][0], cells.p[s.center][1]]); // add new burg
|
||||||
s.capital = burg;
|
s.capital = burg;
|
||||||
s.center = pack.burgs[burg].cell;
|
s.center = pack.burgs[burg].cell;
|
||||||
pack.burgs[burg].capital = 1;
|
pack.burgs[burg].capital = 1;
|
||||||
pack.burgs[burg].state = s.i;
|
pack.burgs[burg].state = s.i;
|
||||||
moveBurgToGroup(burg, "cities");
|
moveBurgToGroup(burg, 'cities');
|
||||||
});
|
});
|
||||||
|
|
||||||
pack.features.forEach(f => {if (f.port) f.port = 0}); // reset features ports counter
|
pack.features.forEach((f) => {
|
||||||
|
if (f.port) f.port = 0;
|
||||||
|
}); // reset features ports counter
|
||||||
BurgsAndStates.specifyBurgs();
|
BurgsAndStates.specifyBurgs();
|
||||||
BurgsAndStates.defineBurgFeatures();
|
BurgsAndStates.defineBurgFeatures();
|
||||||
BurgsAndStates.drawBurgs();
|
BurgsAndStates.drawBurgs();
|
||||||
Routes.regenerate();
|
Routes.regenerate();
|
||||||
|
|
||||||
// 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();
|
||||||
if (layerIsOn("toggleEmblems")) drawEmblems();
|
if (layerIsOn('toggleEmblems')) drawEmblems();
|
||||||
|
|
||||||
if (document.getElementById("burgsOverviewRefresh").offsetParent) burgsOverviewRefresh.click();
|
if (document.getElementById('burgsOverviewRefresh').offsetParent) burgsOverviewRefresh.click();
|
||||||
if (document.getElementById("statesEditorRefresh").offsetParent) statesEditorRefresh.click();
|
if (document.getElementById('statesEditorRefresh').offsetParent) statesEditorRefresh.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
function regenerateResources() {
|
function regenerateResources() {
|
||||||
Resources.generate();
|
Resources.generate();
|
||||||
goods.selectAll("*").remove();
|
goods.selectAll('*').remove();
|
||||||
if (layerIsOn("toggleResources")) drawResources();
|
if (layerIsOn('toggleResources')) drawResources();
|
||||||
refreshAllEditors();
|
refreshAllEditors();
|
||||||
}
|
}
|
||||||
|
|
||||||
function regenerateEmblems() {
|
function regenerateEmblems() {
|
||||||
// remove old emblems
|
// remove old emblems
|
||||||
document.querySelectorAll("[id^=stateCOA]").forEach(el => el.remove());
|
document.querySelectorAll('[id^=stateCOA]').forEach((el) => el.remove());
|
||||||
document.querySelectorAll("[id^=provinceCOA]").forEach(el => el.remove());
|
document.querySelectorAll('[id^=provinceCOA]').forEach((el) => el.remove());
|
||||||
document.querySelectorAll("[id^=burgCOA]").forEach(el => el.remove());
|
document.querySelectorAll('[id^=burgCOA]').forEach((el) => el.remove());
|
||||||
emblems.selectAll("use").remove();
|
emblems.selectAll('use').remove();
|
||||||
|
|
||||||
// generate new emblems
|
// generate new emblems
|
||||||
pack.states.forEach(state => {
|
pack.states.forEach((state) => {
|
||||||
if (!state.i || state.removed) return;
|
if (!state.i || state.removed) return;
|
||||||
const cultureType = pack.cultures[state.culture].type;
|
const cultureType = pack.cultures[state.culture].type;
|
||||||
state.coa = COA.generate(null, null, null, cultureType);
|
state.coa = COA.generate(null, null, null, cultureType);
|
||||||
state.coa.shield = COA.getShield(state.culture, null);
|
state.coa.shield = COA.getShield(state.culture, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
pack.burgs.forEach(burg => {
|
pack.burgs.forEach((burg) => {
|
||||||
if (!burg.i || burg.removed) return;
|
if (!burg.i || burg.removed) return;
|
||||||
const state = pack.states[burg.state];
|
const state = pack.states[burg.state];
|
||||||
|
|
||||||
let kinship = state ? .25 : 0;
|
let kinship = state ? 0.25 : 0;
|
||||||
if (burg.capital) kinship += .1;
|
if (burg.capital) kinship += 0.1;
|
||||||
else if (burg.port) kinship -= .1;
|
else if (burg.port) kinship -= 0.1;
|
||||||
if (state && burg.culture !== state.culture) kinship -= .25;
|
if (state && burg.culture !== state.culture) kinship -= 0.25;
|
||||||
burg.coa = COA.generate(state ? state.coa : null, kinship, null, burg.type);
|
burg.coa = COA.generate(state ? state.coa : null, kinship, null, burg.type);
|
||||||
burg.coa.shield = COA.getShield(burg.culture, state ? burg.state : 0);
|
burg.coa.shield = COA.getShield(burg.culture, state ? burg.state : 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
pack.provinces.forEach(province => {
|
pack.provinces.forEach((province) => {
|
||||||
if (!province.i || province.removed) return;
|
if (!province.i || province.removed) return;
|
||||||
const parent = province.burg ? pack.burgs[province.burg] : pack.states[province.state];
|
const parent = province.burg ? pack.burgs[province.burg] : pack.states[province.state];
|
||||||
|
|
||||||
let dominion = false;
|
let dominion = false;
|
||||||
if (!province.burg) {
|
if (!province.burg) {
|
||||||
dominion = P(.2);
|
dominion = P(0.2);
|
||||||
if (province.formName === "Colony") dominion = P(.95); else
|
if (province.formName === 'Colony') dominion = P(0.95);
|
||||||
if (province.formName === "Island") dominion = P(.6); else
|
else if (province.formName === 'Island') dominion = P(0.6);
|
||||||
if (province.formName === "Islands") dominion = P(.5); else
|
else if (province.formName === 'Islands') dominion = P(0.5);
|
||||||
if (province.formName === "Territory") dominion = P(.4); else
|
else if (province.formName === 'Territory') dominion = P(0.4);
|
||||||
if (province.formName === "Land") dominion = P(.3);
|
else if (province.formName === 'Land') dominion = P(0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nameByBurg = province.burg && province.name.slice(0, 3) === parent.name.slice(0, 3);
|
const nameByBurg = province.burg && province.name.slice(0, 3) === parent.name.slice(0, 3);
|
||||||
const kinship = dominion ? 0 : nameByBurg ? .8 : .4;
|
const kinship = dominion ? 0 : nameByBurg ? 0.8 : 0.4;
|
||||||
const culture = pack.cells.culture[province.center];
|
const culture = pack.cells.culture[province.center];
|
||||||
const type = BurgsAndStates.getType(province.center, parent.port);
|
const type = BurgsAndStates.getType(province.center, parent.port);
|
||||||
province.coa = COA.generate(parent.coa, kinship, dominion, type);
|
province.coa = COA.generate(parent.coa, kinship, dominion, type);
|
||||||
province.coa.shield = COA.getShield(culture, province.state);
|
province.coa.shield = COA.getShield(culture, province.state);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (layerIsOn("toggleEmblems")) drawEmblems(); // redrawEmblems
|
if (layerIsOn('toggleEmblems')) drawEmblems(); // redrawEmblems
|
||||||
}
|
}
|
||||||
|
|
||||||
function regenerateReligions() {
|
function regenerateReligions() {
|
||||||
Religions.generate();
|
Religions.generate();
|
||||||
if (!layerIsOn("toggleReligions")) toggleReligions(); else drawReligions();
|
if (!layerIsOn('toggleReligions')) toggleReligions();
|
||||||
|
else drawReligions();
|
||||||
}
|
}
|
||||||
|
|
||||||
function regenerateCultures() {
|
function regenerateCultures() {
|
||||||
|
|
@ -362,66 +405,73 @@ function regenerateCultures() {
|
||||||
Cultures.expand();
|
Cultures.expand();
|
||||||
BurgsAndStates.updateCultures();
|
BurgsAndStates.updateCultures();
|
||||||
Religions.updateCultures();
|
Religions.updateCultures();
|
||||||
if (!layerIsOn("toggleCultures")) toggleCultures(); else drawCultures();
|
if (!layerIsOn('toggleCultures')) toggleCultures();
|
||||||
|
else drawCultures();
|
||||||
refreshAllEditors();
|
refreshAllEditors();
|
||||||
}
|
}
|
||||||
|
|
||||||
function regenerateMilitary() {
|
function regenerateMilitary() {
|
||||||
Military.generate();
|
Military.generate();
|
||||||
if (!layerIsOn("toggleMilitary")) toggleMilitary();
|
if (!layerIsOn('toggleMilitary')) toggleMilitary();
|
||||||
if (document.getElementById("militaryOverviewRefresh").offsetParent) militaryOverviewRefresh.click();
|
if (document.getElementById('militaryOverviewRefresh').offsetParent) militaryOverviewRefresh.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
function regenerateIce() {
|
function regenerateIce() {
|
||||||
if (!layerIsOn("toggleIce")) toggleIce();
|
if (!layerIsOn('toggleIce')) toggleIce();
|
||||||
ice.selectAll("*").remove();
|
ice.selectAll('*').remove();
|
||||||
drawIce();
|
drawIce();
|
||||||
}
|
}
|
||||||
|
|
||||||
function regenerateMarkers(event) {
|
function regenerateMarkers(event) {
|
||||||
if (isCtrlClick(event)) prompt("Please provide markers number multiplier", {default:1, step:.01, min:0, max:100}, v => addNumberOfMarkers(v));
|
if (isCtrlClick(event)) prompt('Please provide markers number multiplier', {default: 1, step: 0.01, min: 0, max: 100}, (v) => addNumberOfMarkers(v));
|
||||||
else addNumberOfMarkers(gauss(1, .5, .3, 5, 2));
|
else addNumberOfMarkers(gauss(1, 0.5, 0.3, 5, 2));
|
||||||
|
|
||||||
function addNumberOfMarkers(number) {
|
function addNumberOfMarkers(number) {
|
||||||
// remove existing markers and assigned notes
|
// remove existing markers and assigned notes
|
||||||
markers.selectAll("use").each(function() {
|
markers
|
||||||
const index = notes.findIndex(n => n.id === this.id);
|
.selectAll('use')
|
||||||
|
.each(function () {
|
||||||
|
const index = notes.findIndex((n) => n.id === this.id);
|
||||||
if (index != -1) notes.splice(index, 1);
|
if (index != -1) notes.splice(index, 1);
|
||||||
}).remove();
|
})
|
||||||
|
.remove();
|
||||||
|
|
||||||
addMarkers(number);
|
addMarkers(number);
|
||||||
if (!layerIsOn("toggleMarkers")) toggleMarkers();
|
if (!layerIsOn('toggleMarkers')) toggleMarkers();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function regenerateZones(event) {
|
function regenerateZones(event) {
|
||||||
if (isCtrlClick(event)) prompt("Please provide zones number multiplier", {default:1, step:.01, min:0, max:100}, v => addNumberOfZones(v));
|
if (isCtrlClick(event)) prompt('Please provide zones number multiplier', {default: 1, step: 0.01, min: 0, max: 100}, (v) => addNumberOfZones(v));
|
||||||
else addNumberOfZones(gauss(1, .5, .6, 5, 2));
|
else addNumberOfZones(gauss(1, 0.5, 0.6, 5, 2));
|
||||||
|
|
||||||
function addNumberOfZones(number) {
|
function addNumberOfZones(number) {
|
||||||
zones.selectAll("g").remove(); // remove existing zones
|
zones.selectAll('g').remove(); // remove existing zones
|
||||||
addZones(number);
|
addZones(number);
|
||||||
if (document.getElementById("zonesEditorRefresh").offsetParent) zonesEditorRefresh.click();
|
if (document.getElementById('zonesEditorRefresh').offsetParent) zonesEditorRefresh.click();
|
||||||
if (!layerIsOn("toggleZones")) toggleZones();
|
if (!layerIsOn('toggleZones')) toggleZones();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function unpressClickToAddButton() {
|
function unpressClickToAddButton() {
|
||||||
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
|
addFeature.querySelectorAll('button.pressed').forEach((b) => b.classList.remove('pressed'));
|
||||||
restoreDefaultEvents();
|
restoreDefaultEvents();
|
||||||
clearMainTip();
|
clearMainTip();
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleAddLabel() {
|
function toggleAddLabel() {
|
||||||
const pressed = document.getElementById("addLabel").classList.contains("pressed");
|
const pressed = document.getElementById('addLabel').classList.contains('pressed');
|
||||||
if (pressed) {unpressClickToAddButton(); return;}
|
if (pressed) {
|
||||||
|
unpressClickToAddButton();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
|
addFeature.querySelectorAll('button.pressed').forEach((b) => b.classList.remove('pressed'));
|
||||||
addLabel.classList.add('pressed');
|
addLabel.classList.add('pressed');
|
||||||
closeDialogs(".stable");
|
closeDialogs('.stable');
|
||||||
viewbox.style("cursor", "crosshair").on("click", addLabelOnClick);
|
viewbox.style('cursor', 'crosshair').on('click', addLabelOnClick);
|
||||||
tip("Click on map to place label. Hold Shift to add multiple", true);
|
tip('Click on map to place label. Hold Shift to add multiple', true);
|
||||||
if (!layerIsOn("toggleLabels")) toggleLabels();
|
if (!layerIsOn('toggleLabels')) toggleLabels();
|
||||||
}
|
}
|
||||||
|
|
||||||
function addLabelOnClick() {
|
function addLabelOnClick() {
|
||||||
|
|
@ -431,53 +481,71 @@ function addLabelOnClick() {
|
||||||
const cell = findCell(point[0], point[1]);
|
const cell = findCell(point[0], point[1]);
|
||||||
const culture = pack.cells.culture[cell];
|
const culture = pack.cells.culture[cell];
|
||||||
const name = Names.getCulture(culture);
|
const name = Names.getCulture(culture);
|
||||||
const id = getNextId("label");
|
const id = getNextId('label');
|
||||||
|
|
||||||
let group = labels.select("#addedLabels");
|
let group = labels.select('#addedLabels');
|
||||||
if (!group.size()) group = labels.append("g").attr("id", "addedLabels")
|
if (!group.size())
|
||||||
.attr("fill", "#3e3e4b").attr("opacity", 1).attr("stroke", "#3a3a3a")
|
group = labels
|
||||||
.attr("stroke-width", 0).attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC")
|
.append('g')
|
||||||
.attr("font-size", 18).attr("data-size", 18).attr("filter", null);
|
.attr('id', 'addedLabels')
|
||||||
|
.attr('fill', '#3e3e4b')
|
||||||
|
.attr('opacity', 1)
|
||||||
|
.attr('stroke', '#3a3a3a')
|
||||||
|
.attr('stroke-width', 0)
|
||||||
|
.attr('font-family', 'Almendra SC')
|
||||||
|
.attr('data-font', 'Almendra+SC')
|
||||||
|
.attr('font-size', 18)
|
||||||
|
.attr('data-size', 18)
|
||||||
|
.attr('filter', null);
|
||||||
|
|
||||||
const example = group.append("text").attr("x", 0).attr("x", 0).text(name);
|
const example = group.append('text').attr('x', 0).attr('x', 0).text(name);
|
||||||
const width = example.node().getBBox().width;
|
const width = example.node().getBBox().width;
|
||||||
const x = width / -2; // x offset;
|
const x = width / -2; // x offset;
|
||||||
example.remove();
|
example.remove();
|
||||||
|
|
||||||
group.classed("hidden", false);
|
group.classed('hidden', false);
|
||||||
group.append("text").attr("id", id)
|
group
|
||||||
.append("textPath").attr("xlink:href", "#textPath_"+id).attr("startOffset", "50%").attr("font-size", "100%")
|
.append('text')
|
||||||
.append("tspan").attr("x", x).text(name);
|
.attr('id', id)
|
||||||
|
.append('textPath')
|
||||||
|
.attr('xlink:href', '#textPath_' + id)
|
||||||
|
.attr('startOffset', '50%')
|
||||||
|
.attr('font-size', '100%')
|
||||||
|
.append('tspan')
|
||||||
|
.attr('x', x)
|
||||||
|
.text(name);
|
||||||
|
|
||||||
defs.select("#textPaths")
|
defs
|
||||||
.append("path").attr("id", "textPath_"+id)
|
.select('#textPaths')
|
||||||
.attr("d", `M${point[0]-width},${point[1]} h${width*2}`);
|
.append('path')
|
||||||
|
.attr('id', 'textPath_' + id)
|
||||||
|
.attr('d', `M${point[0] - width},${point[1]} h${width * 2}`);
|
||||||
|
|
||||||
if (d3.event.shiftKey === false) unpressClickToAddButton();
|
if (d3.event.shiftKey === false) unpressClickToAddButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleAddBurg() {
|
function toggleAddBurg() {
|
||||||
unpressClickToAddButton();
|
unpressClickToAddButton();
|
||||||
document.getElementById("addBurgTool").classList.add("pressed");
|
document.getElementById('addBurgTool').classList.add('pressed');
|
||||||
overviewBurgs();
|
overviewBurgs();
|
||||||
document.getElementById("addNewBurg").click();
|
document.getElementById('addNewBurg').click();
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleAddRiver() {
|
function toggleAddRiver() {
|
||||||
const pressed = document.getElementById("addRiver").classList.contains("pressed");
|
const pressed = document.getElementById('addRiver').classList.contains('pressed');
|
||||||
if (pressed) {
|
if (pressed) {
|
||||||
unpressClickToAddButton();
|
unpressClickToAddButton();
|
||||||
document.getElementById("addNewRiver").classList.remove("pressed");
|
document.getElementById('addNewRiver').classList.remove('pressed');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
|
addFeature.querySelectorAll('button.pressed').forEach((b) => b.classList.remove('pressed'));
|
||||||
addRiver.classList.add('pressed');
|
addRiver.classList.add('pressed');
|
||||||
document.getElementById("addNewRiver").classList.add("pressed");
|
document.getElementById('addNewRiver').classList.add('pressed');
|
||||||
closeDialogs(".stable");
|
closeDialogs('.stable');
|
||||||
viewbox.style("cursor", "crosshair").on("click", addRiverOnClick);
|
viewbox.style('cursor', 'crosshair').on('click', addRiverOnClick);
|
||||||
tip("Click on map to place new river or extend an existing one. Hold Shift to place multiple rivers", true, "warn");
|
tip('Click on map to place new river or extend an existing one. Hold Shift to place multiple rivers', true, 'warn');
|
||||||
if (!layerIsOn("toggleRivers")) toggleRivers();
|
if (!layerIsOn('toggleRivers')) toggleRivers();
|
||||||
}
|
}
|
||||||
|
|
||||||
function addRiverOnClick() {
|
function addRiverOnClick() {
|
||||||
|
|
@ -487,23 +555,22 @@ function addRiverOnClick() {
|
||||||
if (cells.r[i] || cells.h[i] < 20 || cells.b[i]) return;
|
if (cells.r[i] || cells.h[i] < 20 || cells.b[i]) return;
|
||||||
|
|
||||||
const dataRiver = []; // to store river points
|
const dataRiver = []; // to store river points
|
||||||
let river = +getNextId("river").slice(5); // river id
|
let river = +getNextId('river').slice(5); // river id
|
||||||
cells.fl[i] = grid.cells.prec[cells.g[i]]; // initial flux
|
cells.fl[i] = grid.cells.prec[cells.g[i]]; // initial flux
|
||||||
|
|
||||||
// height with added t value to make map less depressed
|
const h = Rivers.alterHeights();
|
||||||
const h = Array.from(cells.h)
|
Lakes.prepareLakeData(h);
|
||||||
.map((h, i) => h < 20 || cells.t[i] < 1 ? h : h + cells.t[i] / 100)
|
|
||||||
.map((h, i) => h < 20 || cells.t[i] < 1 ? h : h + d3.mean(cells.c[i].map(c => cells.t[c])) / 10000);
|
|
||||||
Rivers.resolveDepressions(h);
|
Rivers.resolveDepressions(h);
|
||||||
|
|
||||||
while (i) {
|
while (i) {
|
||||||
cells.r[i] = river;
|
cells.r[i] = river;
|
||||||
const x = cells.p[i][0], y = cells.p[i][1];
|
const [x, y] = cells.p[i];
|
||||||
dataRiver.push({x, y, cell: i});
|
dataRiver.push({x, y, cell: i});
|
||||||
|
|
||||||
const min = cells.c[i][d3.scan(cells.c[i], (a, b) => h[a] - h[b])]; // downhill cell
|
const min = cells.c[i].sort((a, b) => h[a] - h[b])[0]; // downhill cell
|
||||||
if (h[i] <= h[min]) {tip(`Cell ${i} is depressed, river cannot flow further`, false, "error"); return;}
|
if (h[i] <= h[min]) return tip(`Cell ${i} is depressed, river cannot flow further`, false, 'error');
|
||||||
const tx = cells.p[min][0], ty = cells.p[min][1];
|
|
||||||
|
const [tx, ty] = cells.p[min];
|
||||||
|
|
||||||
if (h[min] < 20) {
|
if (h[min] < 20) {
|
||||||
// pour to water body
|
// pour to water body
|
||||||
|
|
@ -518,10 +585,10 @@ function addRiverOnClick() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// hadnle case when lowest cell already has a river
|
// handle case when lowest cell already has a river
|
||||||
const r = cells.r[min];
|
const r = cells.r[min];
|
||||||
const riverCells = cells.i.filter(i => cells.r[i] === r);
|
const riverCells = cells.i.filter((i) => cells.r[i] === r);
|
||||||
const riverCellsUpper = riverCells.filter(i => h[i] > h[min]);
|
const riverCellsUpper = riverCells.filter((i) => h[i] > h[min]);
|
||||||
|
|
||||||
// finish new river if old river is longer
|
// finish new river if old river is longer
|
||||||
if (dataRiver.length <= riverCellsUpper.length) {
|
if (dataRiver.length <= riverCellsUpper.length) {
|
||||||
|
|
@ -532,22 +599,25 @@ function addRiverOnClick() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// extend old river
|
// extend old river
|
||||||
rivers.select("#river"+r).remove();
|
rivers.select('#river' + r).remove();
|
||||||
cells.i.filter(i => cells.r[i] === river).forEach(i => cells.r[i] = r);
|
cells.i.filter((i) => cells.r[i] === river).forEach((i) => (cells.r[i] = r));
|
||||||
riverCells.forEach(i => cells.r[i] = 0);
|
riverCells.forEach((i) => (cells.r[i] = 0));
|
||||||
river = r;
|
river = r;
|
||||||
cells.fl[min] = cells.fl[i] + grid.cells.prec[cells.g[min]];
|
cells.fl[min] = cells.fl[i] + grid.cells.prec[cells.g[min]];
|
||||||
i = min;
|
i = min;
|
||||||
}
|
}
|
||||||
|
|
||||||
const points = Rivers.addMeandering(dataRiver, 1, .5);
|
const points = Rivers.addMeandering(dataRiver, 1, 0.5);
|
||||||
const widthFactor = rn(.8 + Math.random() * .4, 1); // river width modifier [.8, 1.2]
|
const widthFactor = rn(0.8 + Math.random() * 0.4, 1); // river width modifier [.8, 1.2]
|
||||||
const sourceWidth = .1;
|
const sourceWidth = 0.1;
|
||||||
const [path, length, offset] = Rivers.getPath(points, widthFactor, sourceWidth);
|
const [path, length, offset] = Rivers.getPath(points, widthFactor, sourceWidth);
|
||||||
rivers.append("path").attr("d", path).attr("id", "river"+river);
|
rivers
|
||||||
|
.append('path')
|
||||||
|
.attr('d', path)
|
||||||
|
.attr('id', 'river' + river);
|
||||||
|
|
||||||
// add new river to data or change extended river attributes
|
// add new river to data or change extended river attributes
|
||||||
const r = pack.rivers.find(r => r.i === river);
|
const r = pack.rivers.find((r) => r.i === river);
|
||||||
const mouth = last(dataRiver).cell;
|
const mouth = last(dataRiver).cell;
|
||||||
const discharge = cells.fl[mouth]; // in m3/s
|
const discharge = cells.fl[mouth]; // in m3/s
|
||||||
|
|
||||||
|
|
@ -561,74 +631,98 @@ function addRiverOnClick() {
|
||||||
const source = dataRiver[0].cell;
|
const source = dataRiver[0].cell;
|
||||||
const width = rn(offset ** 2, 2); // mounth width in km
|
const width = rn(offset ** 2, 2); // mounth width in km
|
||||||
const name = Rivers.getName(mouth);
|
const name = Rivers.getName(mouth);
|
||||||
const smallLength = pack.rivers.map(r => r.length||0).sort((a,b) => a-b)[Math.ceil(pack.rivers.length * .15)];
|
const smallLength = pack.rivers.map((r) => r.length || 0).sort((a, b) => a - b)[Math.ceil(pack.rivers.length * 0.15)];
|
||||||
const type = length < smallLength ? rw({"Creek":9, "River":3, "Brook":3, "Stream":1}) : "River";
|
const type = length < smallLength ? rw({Creek: 9, River: 3, Brook: 3, Stream: 1}) : 'River';
|
||||||
|
|
||||||
pack.rivers.push({i: river, source, mouth, discharge, length, width, widthFactor, sourceWidth, parent, basin, name, type});
|
pack.rivers.push({i: river, source, mouth, discharge, length, width, widthFactor, sourceWidth, parent, basin, name, type});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (d3.event.shiftKey === false) {
|
if (d3.event.shiftKey === false) {
|
||||||
|
Lakes.cleanupLakeData();
|
||||||
unpressClickToAddButton();
|
unpressClickToAddButton();
|
||||||
document.getElementById("addNewRiver").classList.remove("pressed");
|
document.getElementById('addNewRiver').classList.remove('pressed');
|
||||||
if (addNewRiver.offsetParent) riversOverviewRefresh.click();
|
if (addNewRiver.offsetParent) riversOverviewRefresh.click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleAddRoute() {
|
function toggleAddRoute() {
|
||||||
const pressed = document.getElementById("addRoute").classList.contains("pressed");
|
const pressed = document.getElementById('addRoute').classList.contains('pressed');
|
||||||
if (pressed) {unpressClickToAddButton(); return;}
|
if (pressed) {
|
||||||
|
unpressClickToAddButton();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
|
addFeature.querySelectorAll('button.pressed').forEach((b) => b.classList.remove('pressed'));
|
||||||
addRoute.classList.add('pressed');
|
addRoute.classList.add('pressed');
|
||||||
closeDialogs(".stable");
|
closeDialogs('.stable');
|
||||||
viewbox.style("cursor", "crosshair").on("click", addRouteOnClick);
|
viewbox.style('cursor', 'crosshair').on('click', addRouteOnClick);
|
||||||
tip("Click on map to add a first control point", true);
|
tip('Click on map to add a first control point', true);
|
||||||
if (!layerIsOn("toggleRoutes")) toggleRoutes();
|
if (!layerIsOn('toggleRoutes')) toggleRoutes();
|
||||||
}
|
}
|
||||||
|
|
||||||
function addRouteOnClick() {
|
function addRouteOnClick() {
|
||||||
unpressClickToAddButton();
|
unpressClickToAddButton();
|
||||||
const point = d3.mouse(this);
|
const point = d3.mouse(this);
|
||||||
const id = getNextId("route");
|
const id = getNextId('route');
|
||||||
elSelected = routes.select("g").append("path").attr("id", id).attr("data-new", 1).attr("d", `M${point[0]},${point[1]}`);
|
elSelected = routes.select('g').append('path').attr('id', id).attr('data-new', 1).attr('d', `M${point[0]},${point[1]}`);
|
||||||
editRoute(true);
|
editRoute(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleAddMarker() {
|
function toggleAddMarker() {
|
||||||
const pressed = document.getElementById("addMarker").classList.contains("pressed");
|
const pressed = document.getElementById('addMarker').classList.contains('pressed');
|
||||||
if (pressed) {unpressClickToAddButton(); return;}
|
if (pressed) {
|
||||||
|
unpressClickToAddButton();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
|
addFeature.querySelectorAll('button.pressed').forEach((b) => b.classList.remove('pressed'));
|
||||||
addMarker.classList.add('pressed');
|
addMarker.classList.add('pressed');
|
||||||
closeDialogs(".stable");
|
closeDialogs('.stable');
|
||||||
viewbox.style("cursor", "crosshair").on("click", addMarkerOnClick);
|
viewbox.style('cursor', 'crosshair').on('click', addMarkerOnClick);
|
||||||
tip("Click on map to add a marker. Hold Shift to add multiple", true);
|
tip('Click on map to add a marker. Hold Shift to add multiple', true);
|
||||||
if (!layerIsOn("toggleMarkers")) toggleMarkers();
|
if (!layerIsOn('toggleMarkers')) toggleMarkers();
|
||||||
}
|
}
|
||||||
|
|
||||||
function addMarkerOnClick() {
|
function addMarkerOnClick() {
|
||||||
const point = d3.mouse(this);
|
const point = d3.mouse(this);
|
||||||
const x = rn(point[0], 2), y = rn(point[1], 2);
|
const x = rn(point[0], 2),
|
||||||
const id = getNextId("markerElement");
|
y = rn(point[1], 2);
|
||||||
|
const id = getNextId('markerElement');
|
||||||
|
|
||||||
const selected = markerSelectGroup.value;
|
const selected = markerSelectGroup.value;
|
||||||
const valid = selected && d3.select("#defs-markers").select("#"+selected).size();
|
const valid =
|
||||||
const symbol = valid ? "#"+selected : "#marker0";
|
selected &&
|
||||||
|
d3
|
||||||
|
.select('#defs-markers')
|
||||||
|
.select('#' + selected)
|
||||||
|
.size();
|
||||||
|
const symbol = valid ? '#' + selected : '#marker0';
|
||||||
const added = markers.select("[data-id='" + symbol + "']").size();
|
const added = markers.select("[data-id='" + symbol + "']").size();
|
||||||
let desired = valid && added ? markers.select("[data-id='" + symbol + "']").attr("data-size") : 1;
|
let desired = valid && added ? markers.select("[data-id='" + symbol + "']").attr('data-size') : 1;
|
||||||
if (isNaN(desired)) desired = 1;
|
if (isNaN(desired)) desired = 1;
|
||||||
const size = desired * 5 + 25 / scale;
|
const size = desired * 5 + 25 / scale;
|
||||||
|
|
||||||
markers.append("use").attr("id", id).attr("xlink:href", symbol).attr("data-id", symbol)
|
markers
|
||||||
.attr("data-x", x).attr("data-y", y).attr("x", x - size / 2).attr("y", y - size)
|
.append('use')
|
||||||
.attr("data-size", desired).attr("width", size).attr("height", size);
|
.attr('id', id)
|
||||||
|
.attr('xlink:href', symbol)
|
||||||
|
.attr('data-id', symbol)
|
||||||
|
.attr('data-x', x)
|
||||||
|
.attr('data-y', y)
|
||||||
|
.attr('x', x - size / 2)
|
||||||
|
.attr('y', y - size)
|
||||||
|
.attr('data-size', desired)
|
||||||
|
.attr('width', size)
|
||||||
|
.attr('height', size);
|
||||||
|
|
||||||
if (d3.event.shiftKey === false) unpressClickToAddButton();
|
if (d3.event.shiftKey === false) unpressClickToAddButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
function viewCellDetails() {
|
function viewCellDetails() {
|
||||||
$("#cellInfo").dialog({
|
$('#cellInfo').dialog({
|
||||||
resizable: false, width: "22em", title: "Cell Details",
|
resizable: false,
|
||||||
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
|
width: '22em',
|
||||||
|
title: 'Cell Details',
|
||||||
|
position: {my: 'right top', at: 'right-10 top+10', of: 'svg', collision: 'fit'}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,23 +15,22 @@ function editUnits() {
|
||||||
document.getElementById('distanceUnitInput').addEventListener('change', changeDistanceUnit);
|
document.getElementById('distanceUnitInput').addEventListener('change', changeDistanceUnit);
|
||||||
document.getElementById('distanceScaleOutput').addEventListener('input', changeDistanceScale);
|
document.getElementById('distanceScaleOutput').addEventListener('input', changeDistanceScale);
|
||||||
document.getElementById('distanceScaleInput').addEventListener('change', changeDistanceScale);
|
document.getElementById('distanceScaleInput').addEventListener('change', changeDistanceScale);
|
||||||
document.getElementById('areaUnit').addEventListener('change', () => lock('areaUnit'));
|
|
||||||
document.getElementById('heightUnit').addEventListener('change', changeHeightUnit);
|
document.getElementById('heightUnit').addEventListener('change', changeHeightUnit);
|
||||||
document.getElementById('heightExponentInput').addEventListener('input', changeHeightExponent);
|
document.getElementById('heightExponentInput').addEventListener('input', changeHeightExponent);
|
||||||
document.getElementById('heightExponentOutput').addEventListener('input', changeHeightExponent);
|
document.getElementById('heightExponentOutput').addEventListener('input', changeHeightExponent);
|
||||||
document.getElementById('temperatureScale').addEventListener('change', changeTemperatureScale);
|
document.getElementById('temperatureScale').addEventListener('change', changeTemperatureScale);
|
||||||
document.getElementById('barSizeOutput').addEventListener('input', changeScaleBarSize);
|
document.getElementById('barSizeOutput').addEventListener('input', drawScaleBar);
|
||||||
document.getElementById('barSize').addEventListener('input', changeScaleBarSize);
|
document.getElementById('barSizeInput').addEventListener('input', drawScaleBar);
|
||||||
document.getElementById('barLabel').addEventListener('input', changeScaleBarLabel);
|
document.getElementById('barLabel').addEventListener('input', drawScaleBar);
|
||||||
document.getElementById('barPosX').addEventListener('input', changeScaleBarPosition);
|
document.getElementById('barPosX').addEventListener('input', fitScaleBar);
|
||||||
document.getElementById('barPosY').addEventListener('input', changeScaleBarPosition);
|
document.getElementById('barPosY').addEventListener('input', fitScaleBar);
|
||||||
document.getElementById('barBackOpacity').addEventListener('input', changeScaleBarOpacity);
|
document.getElementById('barBackOpacity').addEventListener('input', changeScaleBarOpacity);
|
||||||
document.getElementById('barBackColor').addEventListener('input', changeScaleBarColor);
|
document.getElementById('barBackColor').addEventListener('input', changeScaleBarColor);
|
||||||
|
|
||||||
document.getElementById('populationRateOutput').addEventListener('input', changePopulationRate);
|
document.getElementById('populationRateOutput').addEventListener('input', changePopulationRate);
|
||||||
document.getElementById('populationRate').addEventListener('change', changePopulationRate);
|
document.getElementById('populationRateInput').addEventListener('change', changePopulationRate);
|
||||||
document.getElementById('urbanizationOutput').addEventListener('input', changeUrbanizationRate);
|
document.getElementById('urbanizationOutput').addEventListener('input', changeUrbanizationRate);
|
||||||
document.getElementById('urbanization').addEventListener('change', changeUrbanizationRate);
|
document.getElementById('urbanizationInput').addEventListener('change', changeUrbanizationRate);
|
||||||
|
|
||||||
document.getElementById('addLinearRuler').addEventListener('click', addRuler);
|
document.getElementById('addLinearRuler').addEventListener('click', addRuler);
|
||||||
document.getElementById('addOpisometer').addEventListener('click', toggleOpisometerMode);
|
document.getElementById('addOpisometer').addEventListener('click', toggleOpisometerMode);
|
||||||
|
|
@ -51,114 +50,53 @@ function editUnits() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
lock('distanceUnit');
|
|
||||||
drawScaleBar();
|
drawScaleBar();
|
||||||
calculateFriendlyGridSize();
|
calculateFriendlyGridSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeDistanceScale() {
|
function changeDistanceScale() {
|
||||||
const scale = +this.value;
|
|
||||||
if (!scale || isNaN(scale) || scale < 0) {
|
|
||||||
tip('Distance scale should be a positive number', false, 'error');
|
|
||||||
this.value = document.getElementById('distanceScaleInput').dataset.value;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('distanceScaleOutput').value = scale;
|
|
||||||
document.getElementById('distanceScaleInput').value = scale;
|
|
||||||
document.getElementById('distanceScaleInput').dataset.value = scale;
|
|
||||||
lock('distanceScale');
|
|
||||||
|
|
||||||
drawScaleBar();
|
drawScaleBar();
|
||||||
calculateFriendlyGridSize();
|
calculateFriendlyGridSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeHeightUnit() {
|
function changeHeightUnit() {
|
||||||
if (this.value === 'custom_name') {
|
if (this.value !== 'custom_name') return;
|
||||||
|
|
||||||
prompt('Provide a custom name for a height unit', {default: ''}, (custom) => {
|
prompt('Provide a custom name for a height unit', {default: ''}, (custom) => {
|
||||||
this.options.add(new Option(custom, custom, false, true));
|
this.options.add(new Option(custom, custom, false, true));
|
||||||
lock('heightUnit');
|
lock('heightUnit');
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock('heightUnit');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeHeightExponent() {
|
function changeHeightExponent() {
|
||||||
document.getElementById('heightExponentInput').value = this.value;
|
|
||||||
document.getElementById('heightExponentOutput').value = this.value;
|
|
||||||
calculateTemperatures();
|
calculateTemperatures();
|
||||||
if (layerIsOn('toggleTemp')) drawTemp();
|
if (layerIsOn('toggleTemp')) drawTemp();
|
||||||
lock('heightExponent');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeTemperatureScale() {
|
function changeTemperatureScale() {
|
||||||
lock('temperatureScale');
|
|
||||||
if (layerIsOn('toggleTemp')) drawTemp();
|
if (layerIsOn('toggleTemp')) drawTemp();
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeScaleBarSize() {
|
|
||||||
document.getElementById('barSize').value = this.value;
|
|
||||||
document.getElementById('barSizeOutput').value = this.value;
|
|
||||||
drawScaleBar();
|
|
||||||
lock('barSize');
|
|
||||||
}
|
|
||||||
|
|
||||||
function changeScaleBarPosition() {
|
|
||||||
lock('barPosX');
|
|
||||||
lock('barPosY');
|
|
||||||
fitScaleBar();
|
|
||||||
}
|
|
||||||
|
|
||||||
function changeScaleBarLabel() {
|
|
||||||
lock('barLabel');
|
|
||||||
drawScaleBar();
|
|
||||||
}
|
|
||||||
|
|
||||||
function changeScaleBarOpacity() {
|
function changeScaleBarOpacity() {
|
||||||
scaleBar.select('rect').attr('opacity', this.value);
|
scaleBar.select('rect').attr('opacity', this.value);
|
||||||
lock('barBackOpacity');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeScaleBarColor() {
|
function changeScaleBarColor() {
|
||||||
scaleBar.select('rect').attr('fill', this.value);
|
scaleBar.select('rect').attr('fill', this.value);
|
||||||
lock('barBackColor');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function changePopulationRate() {
|
function changePopulationRate() {
|
||||||
const rate = +this.value;
|
populationRate = +this.value;
|
||||||
if (!rate || isNaN(rate) || rate <= 0) {
|
|
||||||
tip('Population rate should be a positive number', false, 'error');
|
|
||||||
this.value = document.getElementById('populationRate').dataset.value;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('populationRateOutput').value = rate;
|
|
||||||
document.getElementById('populationRate').value = rate;
|
|
||||||
document.getElementById('populationRate').dataset.value = rate;
|
|
||||||
lock('populationRate');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeUrbanizationRate() {
|
function changeUrbanizationRate() {
|
||||||
const rate = +this.value;
|
urbanization = +this.value;
|
||||||
if (!rate || isNaN(rate) || rate < 0) {
|
|
||||||
tip('Urbanization rate should be a number', false, 'error');
|
|
||||||
this.value = document.getElementById('urbanization').dataset.value;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('urbanizationOutput').value = rate;
|
|
||||||
document.getElementById('urbanization').value = rate;
|
|
||||||
document.getElementById('urbanization').dataset.value = rate;
|
|
||||||
lock('urbanization');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function restoreDefaultUnits() {
|
function restoreDefaultUnits() {
|
||||||
// distanceScale
|
// distanceScale
|
||||||
document.getElementById('distanceScaleOutput').value = 3;
|
document.getElementById('distanceScaleOutput').value = 3;
|
||||||
document.getElementById('distanceScaleInput').value = 3;
|
document.getElementById('distanceScaleInput').value = 3;
|
||||||
document.getElementById('distanceScaleInput').dataset.value = 3;
|
|
||||||
unlock('distanceScale');
|
unlock('distanceScale');
|
||||||
|
|
||||||
// units
|
// units
|
||||||
|
|
@ -180,7 +118,7 @@ function editUnits() {
|
||||||
calculateTemperatures();
|
calculateTemperatures();
|
||||||
|
|
||||||
// scale bar
|
// scale bar
|
||||||
barSizeOutput.value = barSize.value = 2;
|
barSizeOutput.value = barSizeInput.value = 2;
|
||||||
barLabel.value = '';
|
barLabel.value = '';
|
||||||
barBackOpacity.value = 0.2;
|
barBackOpacity.value = 0.2;
|
||||||
barBackColor.value = '#ffffff';
|
barBackColor.value = '#ffffff';
|
||||||
|
|
@ -195,8 +133,8 @@ function editUnits() {
|
||||||
drawScaleBar();
|
drawScaleBar();
|
||||||
|
|
||||||
// population
|
// population
|
||||||
populationRateOutput.value = populationRate.value = 1000;
|
populationRate = populationRateOutput.value = populationRateInput.value = 1000;
|
||||||
urbanizationOutput.value = urbanization.value = 1;
|
urbanization = urbanizationOutput.value = urbanizationInput.value = 1;
|
||||||
localStorage.removeItem('populationRate');
|
localStorage.removeItem('populationRate');
|
||||||
localStorage.removeItem('urbanization');
|
localStorage.removeItem('urbanization');
|
||||||
}
|
}
|
||||||
|
|
@ -328,12 +266,22 @@ function editUnits() {
|
||||||
|
|
||||||
function removeAllRulers() {
|
function removeAllRulers() {
|
||||||
if (!rulers.data.length) return;
|
if (!rulers.data.length) return;
|
||||||
|
alertMessage.innerHTML = `
|
||||||
const message = 'Are you sure you want to remove all placed rulers?<br>If you just want to hide rulers, toggle the Rulers layer off in Menu';
|
Are you sure you want to remove all placed rulers?
|
||||||
const onConfirm = () => {
|
<br>If you just want to hide rulers, toggle the Rulers layer off in Menu`;
|
||||||
|
$('#alert').dialog({
|
||||||
|
resizable: false,
|
||||||
|
title: 'Remove all rulers',
|
||||||
|
buttons: {
|
||||||
|
Remove: function () {
|
||||||
|
$(this).dialog('close');
|
||||||
rulers.undraw();
|
rulers.undraw();
|
||||||
rulers = new Rulers();
|
rulers = new Rulers();
|
||||||
};
|
},
|
||||||
confirmationDialog({title: 'Remove all rulers', message, confirm: 'Remove', onConfirm});
|
Cancel: function () {
|
||||||
|
$(this).dialog('close');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,10 @@ function editZones() {
|
||||||
modules.editZones = true;
|
modules.editZones = true;
|
||||||
|
|
||||||
$("#zonesEditor").dialog({
|
$("#zonesEditor").dialog({
|
||||||
title: "Zones Editor", resizable: false, width: fitContent(), close: () => exitZonesManualAssignment("close"),
|
title: "Zones Editor",
|
||||||
|
resizable: false,
|
||||||
|
width: fitContent(),
|
||||||
|
close: () => exitZonesManualAssignment("close"),
|
||||||
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
|
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -26,17 +29,35 @@ function editZones() {
|
||||||
document.getElementById("zonesRemove").addEventListener("click", toggleEraseMode);
|
document.getElementById("zonesRemove").addEventListener("click", toggleEraseMode);
|
||||||
|
|
||||||
body.addEventListener("click", function (ev) {
|
body.addEventListener("click", function (ev) {
|
||||||
const el = ev.target, cl = el.classList, zone = el.parentNode.dataset.id;
|
const el = ev.target,
|
||||||
if (cl.contains("culturePopulation")) {changePopulation(zone); return;}
|
cl = el.classList,
|
||||||
if (cl.contains("icon-trash-empty")) {zoneRemove(zone); return;}
|
zone = el.parentNode.dataset.id;
|
||||||
if (cl.contains("icon-eye")) {toggleVisibility(el); return;}
|
if (cl.contains("culturePopulation")) {
|
||||||
if (cl.contains("icon-pin")) {toggleFog(zone, cl); return;}
|
changePopulation(zone);
|
||||||
if (cl.contains("fillRect")) {changeFill(el); return;}
|
return;
|
||||||
|
}
|
||||||
|
if (cl.contains("icon-trash-empty")) {
|
||||||
|
zoneRemove(zone);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cl.contains("icon-eye")) {
|
||||||
|
toggleVisibility(el);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cl.contains("icon-pin")) {
|
||||||
|
toggleFog(zone, cl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cl.contains("fillRect")) {
|
||||||
|
changeFill(el);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (customization) selectZone(el);
|
if (customization) selectZone(el);
|
||||||
});
|
});
|
||||||
|
|
||||||
body.addEventListener("input", function (ev) {
|
body.addEventListener("input", function (ev) {
|
||||||
const el = ev.target, zone = el.parentNode.dataset.id;
|
const el = ev.target,
|
||||||
|
zone = el.parentNode.dataset.id;
|
||||||
if (el.classList.contains("religionName")) zones.select("#" + zone).attr("data-description", el.value);
|
if (el.classList.contains("religionName")) zones.select("#" + zone).attr("data-description", el.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -49,9 +70,9 @@ function editZones() {
|
||||||
const c = this.dataset.cells ? this.dataset.cells.split(",").map(c => +c) : [];
|
const c = this.dataset.cells ? this.dataset.cells.split(",").map(c => +c) : [];
|
||||||
const description = this.dataset.description;
|
const description = this.dataset.description;
|
||||||
const fill = this.getAttribute("fill");
|
const fill = this.getAttribute("fill");
|
||||||
const area = d3.sum(c.map(i => pack.cells.area[i])) * (distanceScaleInput.value ** 2);
|
const area = d3.sum(c.map(i => pack.cells.area[i])) * distanceScaleInput.value ** 2;
|
||||||
const rural = d3.sum(c.map(i => pack.cells.pop[i])) * populationRate.value;
|
const rural = d3.sum(c.map(i => pack.cells.pop[i])) * populationRate;
|
||||||
const urban = d3.sum(c.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate.value * urbanization.value;
|
const urban = d3.sum(c.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate * urbanization;
|
||||||
const population = rural + urban;
|
const population = rural + urban;
|
||||||
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}. Click to change`;
|
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}. Click to change`;
|
||||||
const inactive = this.style.display === "none";
|
const inactive = this.style.display === "none";
|
||||||
|
|
@ -67,8 +88,8 @@ function editZones() {
|
||||||
<span data-tip="${populationTip}" class="icon-male hide"></span>
|
<span data-tip="${populationTip}" class="icon-male hide"></span>
|
||||||
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
|
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
|
||||||
<span data-tip="Drag to raise or lower the zone" class="icon-resize-vertical hide"></span>
|
<span data-tip="Drag to raise or lower the zone" class="icon-resize-vertical hide"></span>
|
||||||
<span data-tip="Toggle zone focus" class="icon-pin ${focused?'':' inactive'} hide ${c.length?'':' placeholder'}"></span>
|
<span data-tip="Toggle zone focus" class="icon-pin ${focused ? "" : " inactive"} hide ${c.length ? "" : " placeholder"}"></span>
|
||||||
<span data-tip="Toggle zone visibility" class="icon-eye ${inactive?' inactive':''} hide ${c.length?'':' placeholder'}"></span>
|
<span data-tip="Toggle zone visibility" class="icon-eye ${inactive ? " inactive" : ""} hide ${c.length ? "" : " placeholder"}"></span>
|
||||||
<span data-tip="Remove zone" class="icon-trash-empty hide"></span>
|
<span data-tip="Remove zone" class="icon-trash-empty hide"></span>
|
||||||
</div>`;
|
</div>`;
|
||||||
});
|
});
|
||||||
|
|
@ -76,8 +97,8 @@ function editZones() {
|
||||||
body.innerHTML = lines;
|
body.innerHTML = lines;
|
||||||
|
|
||||||
// update footer
|
// update footer
|
||||||
const totalArea = zonesFooterArea.dataset.area = graphWidth * graphHeight * (distanceScaleInput.value ** 2);
|
const totalArea = (zonesFooterArea.dataset.area = graphWidth * graphHeight * distanceScaleInput.value ** 2);
|
||||||
const totalPop = (d3.sum(pack.cells.pop) + d3.sum(pack.burgs.filter(b => !b.removed).map(b => b.population)) * urbanization.value) * populationRate.value;
|
const totalPop = (d3.sum(pack.cells.pop) + d3.sum(pack.burgs.filter(b => !b.removed).map(b => b.population)) * urbanization) * populationRate;
|
||||||
zonesFooterPopulation.dataset.population = totalPop;
|
zonesFooterPopulation.dataset.population = totalPop;
|
||||||
zonesFooterNumber.innerHTML = zones.selectAll("g").size();
|
zonesFooterNumber.innerHTML = zones.selectAll("g").size();
|
||||||
zonesFooterCells.innerHTML = pack.cells.i.length;
|
zonesFooterCells.innerHTML = pack.cells.i.length;
|
||||||
|
|
@ -88,7 +109,10 @@ function editZones() {
|
||||||
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => zoneHighlightOn(ev)));
|
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => zoneHighlightOn(ev)));
|
||||||
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => zoneHighlightOff(ev)));
|
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => zoneHighlightOff(ev)));
|
||||||
|
|
||||||
if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
|
if (body.dataset.type === "percentage") {
|
||||||
|
body.dataset.type = "absolute";
|
||||||
|
togglePercentageMode();
|
||||||
|
}
|
||||||
$("#zonesEditor").dialog({width: fitContent()});
|
$("#zonesEditor").dialog({width: fitContent()});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,7 +130,10 @@ function editZones() {
|
||||||
function movezone(ev, ui) {
|
function movezone(ev, ui) {
|
||||||
const zone = $("#" + ui.item.attr("data-id"));
|
const zone = $("#" + ui.item.attr("data-id"));
|
||||||
const prev = $("#" + ui.item.prev().attr("data-id"));
|
const prev = $("#" + ui.item.prev().attr("data-id"));
|
||||||
if (prev) {zone.insertAfter(prev); return;}
|
if (prev) {
|
||||||
|
zone.insertAfter(prev);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const next = $("#" + ui.item.next().attr("data-id"));
|
const next = $("#" + ui.item.next().attr("data-id"));
|
||||||
if (next) zone.insertBefore(next);
|
if (next) zone.insertBefore(next);
|
||||||
}
|
}
|
||||||
|
|
@ -114,22 +141,21 @@ function editZones() {
|
||||||
function enterZonesManualAssignent() {
|
function enterZonesManualAssignent() {
|
||||||
if (!layerIsOn("toggleZones")) toggleZones();
|
if (!layerIsOn("toggleZones")) toggleZones();
|
||||||
customization = 10;
|
customization = 10;
|
||||||
document.querySelectorAll("#zonesBottom > button").forEach(el => el.style.display = "none");
|
document.querySelectorAll("#zonesBottom > button").forEach(el => (el.style.display = "none"));
|
||||||
document.getElementById("zonesManuallyButtons").style.display = "inline-block";
|
document.getElementById("zonesManuallyButtons").style.display = "inline-block";
|
||||||
|
|
||||||
zonesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
|
zonesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
|
||||||
zonesFooter.style.display = "none";
|
zonesFooter.style.display = "none";
|
||||||
body.querySelectorAll("div > input, select, svg").forEach(e => e.style.pointerEvents = "none");
|
body.querySelectorAll("div > input, select, svg").forEach(e => (e.style.pointerEvents = "none"));
|
||||||
$("#zonesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
|
$("#zonesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
|
||||||
|
|
||||||
tip("Click to select a zone, drag to paint a zone", true);
|
tip("Click to select a zone, drag to paint a zone", true);
|
||||||
viewbox.style("cursor", "crosshair")
|
viewbox.style("cursor", "crosshair").on("click", selectZoneOnMapClick).call(d3.drag().on("start", dragZoneBrush)).on("touchmove mousemove", moveZoneBrush);
|
||||||
.on("click", selectZoneOnMapClick)
|
|
||||||
.call(d3.drag().on("start", dragZoneBrush))
|
|
||||||
.on("touchmove mousemove", moveZoneBrush);
|
|
||||||
|
|
||||||
body.querySelector("div").classList.add("selected");
|
body.querySelector("div").classList.add("selected");
|
||||||
zones.selectAll("g").each(function() {this.setAttribute("data-init", this.getAttribute("data-cells"));});
|
zones.selectAll("g").each(function () {
|
||||||
|
this.setAttribute("data-init", this.getAttribute("data-cells"));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectZone(el) {
|
function selectZone(el) {
|
||||||
|
|
@ -175,7 +201,10 @@ function editZones() {
|
||||||
selection.forEach(i => {
|
selection.forEach(i => {
|
||||||
if (cells.includes(i)) return;
|
if (cells.includes(i)) return;
|
||||||
cells.push(i);
|
cells.push(i);
|
||||||
zone.append("polygon").attr("points", getPackPolygon(i)).attr("id", base + i);
|
zone
|
||||||
|
.append("polygon")
|
||||||
|
.attr("points", getPackPolygon(i))
|
||||||
|
.attr("id", base + i);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -211,8 +240,13 @@ function editZones() {
|
||||||
zone.attr("data-cells", cells);
|
zone.attr("data-cells", cells);
|
||||||
zone.selectAll("*").remove();
|
zone.selectAll("*").remove();
|
||||||
const base = zone.attr("id") + "_"; // id generic part
|
const base = zone.attr("id") + "_"; // id generic part
|
||||||
zone.selectAll("*").data(cells).enter().append("polygon")
|
zone
|
||||||
.attr("points", d => getPackPolygon(d)).attr("id", d => base + d);
|
.selectAll("*")
|
||||||
|
.data(cells)
|
||||||
|
.enter()
|
||||||
|
.append("polygon")
|
||||||
|
.attr("points", d => getPackPolygon(d))
|
||||||
|
.attr("id", d => base + d);
|
||||||
});
|
});
|
||||||
|
|
||||||
exitZonesManualAssignment();
|
exitZonesManualAssignment();
|
||||||
|
|
@ -221,17 +255,19 @@ function editZones() {
|
||||||
function exitZonesManualAssignment(close) {
|
function exitZonesManualAssignment(close) {
|
||||||
customization = 0;
|
customization = 0;
|
||||||
removeCircle();
|
removeCircle();
|
||||||
document.querySelectorAll("#zonesBottom > button").forEach(el => el.style.display = "inline-block");
|
document.querySelectorAll("#zonesBottom > button").forEach(el => (el.style.display = "inline-block"));
|
||||||
document.getElementById("zonesManuallyButtons").style.display = "none";
|
document.getElementById("zonesManuallyButtons").style.display = "none";
|
||||||
|
|
||||||
zonesEditor.querySelectorAll(".hide:not(.show)").forEach(el => el.classList.remove("hidden"));
|
zonesEditor.querySelectorAll(".hide:not(.show)").forEach(el => el.classList.remove("hidden"));
|
||||||
zonesFooter.style.display = "block";
|
zonesFooter.style.display = "block";
|
||||||
body.querySelectorAll("div > input, select, svg").forEach(e => e.style.pointerEvents = "all");
|
body.querySelectorAll("div > input, select, svg").forEach(e => (e.style.pointerEvents = "all"));
|
||||||
if (!close) $("#zonesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
|
if (!close) $("#zonesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
|
||||||
|
|
||||||
restoreDefaultEvents();
|
restoreDefaultEvents();
|
||||||
clearMainTip();
|
clearMainTip();
|
||||||
zones.selectAll("g").each(function() {this.removeAttribute("data-init");});
|
zones.selectAll("g").each(function () {
|
||||||
|
this.removeAttribute("data-init");
|
||||||
|
});
|
||||||
const selected = body.querySelector("div.selected");
|
const selected = body.querySelector("div.selected");
|
||||||
if (selected) selected.classList.remove("selected");
|
if (selected) selected.classList.remove("selected");
|
||||||
}
|
}
|
||||||
|
|
@ -241,7 +277,7 @@ function editZones() {
|
||||||
const callback = function (fill) {
|
const callback = function (fill) {
|
||||||
el.setAttribute("fill", fill);
|
el.setAttribute("fill", fill);
|
||||||
document.getElementById(el.parentNode.parentNode.dataset.id).setAttribute("fill", fill);
|
document.getElementById(el.parentNode.parentNode.dataset.id).setAttribute("fill", fill);
|
||||||
}
|
};
|
||||||
|
|
||||||
openPicker(fill, callback);
|
openPicker(fill, callback);
|
||||||
}
|
}
|
||||||
|
|
@ -257,20 +293,30 @@ function editZones() {
|
||||||
const dataCells = zones.select("#" + z).attr("data-cells");
|
const dataCells = zones.select("#" + z).attr("data-cells");
|
||||||
if (!dataCells) return;
|
if (!dataCells) return;
|
||||||
|
|
||||||
const path = "M" + dataCells.split(",").map(c => getPackPolygon(+c)).join("M") + "Z", id = "focusZone"+z;
|
const path =
|
||||||
|
"M" +
|
||||||
|
dataCells
|
||||||
|
.split(",")
|
||||||
|
.map(c => getPackPolygon(+c))
|
||||||
|
.join("M") +
|
||||||
|
"Z",
|
||||||
|
id = "focusZone" + z;
|
||||||
cl.contains("inactive") ? fog(id, path) : unfog(id);
|
cl.contains("inactive") ? fog(id, path) : unfog(id);
|
||||||
cl.toggle("inactive");
|
cl.toggle("inactive");
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleLegend() {
|
function toggleLegend() {
|
||||||
if (legend.selectAll("*").size()) {clearLegend(); return;}; // hide legend
|
if (legend.selectAll("*").size()) {
|
||||||
|
clearLegend();
|
||||||
|
return;
|
||||||
|
} // hide legend
|
||||||
const data = [];
|
const data = [];
|
||||||
|
|
||||||
zones.selectAll("g").each(function () {
|
zones.selectAll("g").each(function () {
|
||||||
const id = this.dataset.id;
|
const id = this.dataset.id;
|
||||||
const description = this.dataset.description;
|
const description = this.dataset.description;
|
||||||
const fill = this.getAttribute("fill");
|
const fill = this.getAttribute("fill");
|
||||||
data.push([id, fill, description])
|
data.push([id, fill, description]);
|
||||||
});
|
});
|
||||||
|
|
||||||
drawLegend("Zones", data);
|
drawLegend("Zones", data);
|
||||||
|
|
@ -284,11 +330,10 @@ function editZones() {
|
||||||
const totalPopulation = +zonesFooterPopulation.dataset.population;
|
const totalPopulation = +zonesFooterPopulation.dataset.population;
|
||||||
|
|
||||||
body.querySelectorAll(":scope > div").forEach(function (el) {
|
body.querySelectorAll(":scope > div").forEach(function (el) {
|
||||||
el.querySelector(".stateCells").innerHTML = rn(+el.dataset.cells / totalCells * 100, 2) + "%";
|
el.querySelector(".stateCells").innerHTML = rn((+el.dataset.cells / totalCells) * 100, 2) + "%";
|
||||||
el.querySelector(".biomeArea").innerHTML = rn(+el.dataset.area / totalArea * 100, 2) + "%";
|
el.querySelector(".biomeArea").innerHTML = rn((+el.dataset.area / totalArea) * 100, 2) + "%";
|
||||||
el.querySelector(".culturePopulation").innerHTML = rn(+el.dataset.population / totalPopulation * 100, 2) + "%";
|
el.querySelector(".culturePopulation").innerHTML = rn((+el.dataset.population / totalPopulation) * 100, 2) + "%";
|
||||||
});
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
body.dataset.type = "absolute";
|
body.dataset.type = "absolute";
|
||||||
zonesEditorAddLines();
|
zonesEditorAddLines();
|
||||||
|
|
@ -298,7 +343,7 @@ function editZones() {
|
||||||
function addZonesLayer() {
|
function addZonesLayer() {
|
||||||
const id = getNextId("zone");
|
const id = getNextId("zone");
|
||||||
const description = "Unknown zone";
|
const description = "Unknown zone";
|
||||||
const fill = "url(#hatch" + id.slice(4)%14 + ")";
|
const fill = "url(#hatch" + (id.slice(4) % 14) + ")";
|
||||||
zones.append("g").attr("id", id).attr("data-description", description).attr("data-cells", "").attr("fill", fill);
|
zones.append("g").attr("id", id).attr("data-description", description).attr("data-cells", "").attr("fill", fill);
|
||||||
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
|
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
|
||||||
|
|
||||||
|
|
@ -344,61 +389,77 @@ function editZones() {
|
||||||
|
|
||||||
function changePopulation(zone) {
|
function changePopulation(zone) {
|
||||||
const dataCells = zones.select("#" + zone).attr("data-cells");
|
const dataCells = zones.select("#" + zone).attr("data-cells");
|
||||||
const cells = dataCells ? dataCells.split(",").map(i => +i).filter(i => pack.cells.h[i] >= 20) : [];
|
const cells = dataCells
|
||||||
if (!cells.length) {tip("Zone does not have any land cells, cannot change population", false, "error"); return;}
|
? dataCells
|
||||||
|
.split(",")
|
||||||
|
.map(i => +i)
|
||||||
|
.filter(i => pack.cells.h[i] >= 20)
|
||||||
|
: [];
|
||||||
|
if (!cells.length) {
|
||||||
|
tip("Zone does not have any land cells, cannot change population", false, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
const burgs = pack.burgs.filter(b => !b.removed && cells.includes(b.cell));
|
const burgs = pack.burgs.filter(b => !b.removed && cells.includes(b.cell));
|
||||||
|
|
||||||
const rural = rn(d3.sum(cells.map(i => pack.cells.pop[i])) * populationRate.value);
|
const rural = rn(d3.sum(cells.map(i => pack.cells.pop[i])) * populationRate);
|
||||||
const urban = rn(d3.sum(cells.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate.value * urbanization.value);
|
const urban = rn(d3.sum(cells.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate * urbanization);
|
||||||
const total = rural + urban;
|
const total = rural + urban;
|
||||||
const l = n => Number(n).toLocaleString();
|
const l = n => Number(n).toLocaleString();
|
||||||
|
|
||||||
alertMessage.innerHTML = `
|
alertMessage.innerHTML = `
|
||||||
Rural: <input type="number" min=0 step=1 id="ruralPop" value=${rural} style="width:6em">
|
Rural: <input type="number" min=0 step=1 id="ruralPop" value=${rural} style="width:6em">
|
||||||
Urban: <input type="number" min=0 step=1 id="urbanPop" value=${urban} style="width:6em" ${burgs.length?'':"disabled"}>
|
Urban: <input type="number" min=0 step=1 id="urbanPop" value=${urban} style="width:6em" ${burgs.length ? "" : "disabled"}>
|
||||||
<p>Total population: ${l(total)} ⇒ <span id="totalPop">${l(total)}</span> (<span id="totalPopPerc">100</span>%)</p>`;
|
<p>Total population: ${l(total)} ⇒ <span id="totalPop">${l(total)}</span> (<span id="totalPopPerc">100</span>%)</p>`;
|
||||||
|
|
||||||
const update = function () {
|
const update = function () {
|
||||||
const totalNew = ruralPop.valueAsNumber + urbanPop.valueAsNumber;
|
const totalNew = ruralPop.valueAsNumber + urbanPop.valueAsNumber;
|
||||||
if (isNaN(totalNew)) return;
|
if (isNaN(totalNew)) return;
|
||||||
totalPop.innerHTML = l(totalNew);
|
totalPop.innerHTML = l(totalNew);
|
||||||
totalPopPerc.innerHTML = rn(totalNew / total * 100);
|
totalPopPerc.innerHTML = rn((totalNew / total) * 100);
|
||||||
}
|
};
|
||||||
|
|
||||||
ruralPop.oninput = () => update();
|
ruralPop.oninput = () => update();
|
||||||
urbanPop.oninput = () => update();
|
urbanPop.oninput = () => update();
|
||||||
|
|
||||||
$("#alert").dialog({
|
$("#alert").dialog({
|
||||||
resizable: false, title: "Change zone population", width: "24em", buttons: {
|
resizable: false,
|
||||||
Apply: function() {applyPopulationChange(); $(this).dialog("close");},
|
title: "Change zone population",
|
||||||
Cancel: function() {$(this).dialog("close");}
|
width: "24em",
|
||||||
}, position: {my: "center", at: "center", of: "svg"}
|
buttons: {
|
||||||
|
Apply: function () {
|
||||||
|
applyPopulationChange();
|
||||||
|
$(this).dialog("close");
|
||||||
|
},
|
||||||
|
Cancel: function () {
|
||||||
|
$(this).dialog("close");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
position: {my: "center", at: "center", of: "svg"}
|
||||||
});
|
});
|
||||||
|
|
||||||
function applyPopulationChange() {
|
function applyPopulationChange() {
|
||||||
const ruralChange = ruralPop.value / rural;
|
const ruralChange = ruralPop.value / rural;
|
||||||
if (isFinite(ruralChange) && ruralChange !== 1) {
|
if (isFinite(ruralChange) && ruralChange !== 1) {
|
||||||
cells.forEach(i => pack.cells.pop[i] *= ruralChange);
|
cells.forEach(i => (pack.cells.pop[i] *= ruralChange));
|
||||||
}
|
}
|
||||||
if (!isFinite(ruralChange) && +ruralPop.value > 0) {
|
if (!isFinite(ruralChange) && +ruralPop.value > 0) {
|
||||||
const points = ruralPop.value / populationRate.value;
|
const points = ruralPop.value / populationRate;
|
||||||
const pop = rn(points / cells.length);
|
const pop = rn(points / cells.length);
|
||||||
cells.forEach(i => pack.cells.pop[i] = pop);
|
cells.forEach(i => (pack.cells.pop[i] = pop));
|
||||||
}
|
}
|
||||||
|
|
||||||
const urbanChange = urbanPop.value / urban;
|
const urbanChange = urbanPop.value / urban;
|
||||||
if (isFinite(urbanChange) && urbanChange !== 1) {
|
if (isFinite(urbanChange) && urbanChange !== 1) {
|
||||||
burgs.forEach(b => b.population = rn(b.population * urbanChange, 4));
|
burgs.forEach(b => (b.population = rn(b.population * urbanChange, 4)));
|
||||||
}
|
}
|
||||||
if (!isFinite(urbanChange) && +urbanPop.value > 0) {
|
if (!isFinite(urbanChange) && +urbanPop.value > 0) {
|
||||||
const points = urbanPop.value / populationRate.value / urbanization.value;
|
const points = urbanPop.value / populationRate / urbanization;
|
||||||
const population = rn(points / burgs.length, 4);
|
const population = rn(points / burgs.length, 4);
|
||||||
burgs.forEach(b => b.population = population);
|
burgs.forEach(b => (b.population = population));
|
||||||
}
|
}
|
||||||
|
|
||||||
zonesEditorAddLines();
|
zonesEditorAddLines();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function zoneRemove(zone) {
|
function zoneRemove(zone) {
|
||||||
|
|
@ -406,5 +467,4 @@ function editZones() {
|
||||||
unfog("focusZone" + zone);
|
unfog("focusZone" + zone);
|
||||||
zonesEditorAddLines();
|
zonesEditorAddLines();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue