mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 17:51:24 +01:00
Merge branch 'master' of https://github.com/Azgaar/Fantasy-Map-Generator into burg-groups
This commit is contained in:
commit
e402120b8d
42 changed files with 1526 additions and 1067 deletions
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
|
|
@ -1,3 +1,3 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: Azgaar
|
||||
patreon: Azgaar
|
||||
|
|
|
|||
353
index.html
353
index.html
|
|
@ -1081,8 +1081,14 @@
|
|||
id="styleGridSizeFriendly"
|
||||
data-tip="Distance between grid cell centers (in map scale)"
|
||||
></output>
|
||||
<a href="https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Scale-and-distance#grids" target="_blank">
|
||||
<span data-tip="Open wiki article scale and distance to know about grid scale" class="icon-info-circled pointer"></span>
|
||||
<a
|
||||
href="https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Scale-and-distance#grids"
|
||||
target="_blank"
|
||||
>
|
||||
<span
|
||||
data-tip="Open wiki article scale and distance to know about grid scale"
|
||||
class="icon-info-circled pointer"
|
||||
></span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -1631,10 +1637,10 @@
|
|||
</td>
|
||||
<td>Cultures number</td>
|
||||
<td>
|
||||
<input id="culturesInput" data-stored="cultures" type="range" min="1" max="32" value="14" />
|
||||
<input id="culturesInput" data-stored="cultures" type="range" min="1" />
|
||||
</td>
|
||||
<td>
|
||||
<input id="culturesOutput" data-stored="cultures" type="number" min="1" max="32" value="14" />
|
||||
<input id="culturesOutput" data-stored="cultures" type="number" min="1" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
|
@ -1834,6 +1840,7 @@
|
|||
<select id="azgaarAssistant" data-stored="azgaarAssistant">
|
||||
<option value="show" selected>Show</option>
|
||||
<option value="hide">Hide</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
|
@ -2179,8 +2186,8 @@
|
|||
|
||||
<div class="separator">Create</div>
|
||||
<div class="grid">
|
||||
<button id="openSubmapMenu" data-tip="Click to generate a submap from the current viewport">Submap</button>
|
||||
<button id="openResampleMenu" data-tip="Click to transform the map">Transform</button>
|
||||
<button id="openSubmapTool" data-tip="Click to generate a submap from the current viewport">Submap</button>
|
||||
<button id="openTransformTool" data-tip="Click to transform the map">Transform</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -2418,7 +2425,9 @@
|
|||
|
||||
<div id="exitCustomization">
|
||||
<div data-tip="Drag to move the pane">
|
||||
<button data-tip="Finalize the heightmap and exit the edit mode" id="finalizeHeightmap">Exit Customization</button>
|
||||
<button data-tip="Finalize the heightmap and exit the edit mode" id="finalizeHeightmap">
|
||||
Exit Customization
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -3498,7 +3507,11 @@
|
|||
<button id="burgEditEmblem" data-tip="Edit emblem" class="icon-shield-alt"></button>
|
||||
<button id="burgSetPreviewLink" data-tip="Set custom burg map URL" class="icon-map-o"></button>
|
||||
<button id="burgLocate" data-tip="Zoom map and center view in the burg" class="icon-target"></button>
|
||||
<button id="burgRelocate" data-tip="Relocate burg. Click on map to move the burg" class="icon-map-pin"></button>
|
||||
<button
|
||||
id="burgRelocate"
|
||||
data-tip="Relocate burg. Click on map to move the burg"
|
||||
class="icon-map-pin"
|
||||
></button>
|
||||
<button id="burglLegend" data-tip="Edit free text notes (legend) for this burg" class="icon-edit"></button>
|
||||
<button id="burgLock" class="icon-lock-open" onmouseover="showElementLockTip(event)"></button>
|
||||
<button
|
||||
|
|
@ -4756,7 +4769,7 @@
|
|||
<span style="margin-left: 2px">Names data: </span>
|
||||
</div>
|
||||
|
||||
<div id="namesbaseBody" style="margin-block: 2px">
|
||||
<div id="namesbaseBody" style="margin-block: 2px; width: auto">
|
||||
<textarea
|
||||
id="namesbaseTextarea"
|
||||
data-base="0"
|
||||
|
|
@ -4765,6 +4778,7 @@
|
|||
placeholder="Provide a names data: a comma separated list of source names"
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
style="resize: none"
|
||||
></textarea>
|
||||
|
||||
<div>
|
||||
|
|
@ -4936,18 +4950,21 @@
|
|||
>Model:
|
||||
<select id="aiGeneratorModel"></select>
|
||||
</label>
|
||||
|
||||
<label
|
||||
for="aiGeneratorTemperature"
|
||||
data-tip="Temperature controls response randomness; higher values mean more creativity, lower values mean more predictability"
|
||||
>
|
||||
Temperature:
|
||||
<input id="aiGeneratorTemperature" type="number" min="-1" max="2" step=".1" class="icon-key" />
|
||||
</label>
|
||||
<label for="aiGeneratorKey"
|
||||
>Key:
|
||||
<input id="aiGeneratorKey" placeholder="Enter OpenAI API key" class="icon-key" />
|
||||
<a
|
||||
href="https://platform.openai.com/account/api-keys"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
<input id="aiGeneratorKey" placeholder="Enter API key" class="icon-key" />
|
||||
<button
|
||||
id="aiGeneratorKeyHelp"
|
||||
class="icon-help-circled"
|
||||
style="text-decoration: none"
|
||||
data-tip="Get the key at OpenAI website. The key will be stored in your browser and send to OpenAI API directly. The Map Genenerator doesn't store the key or any generated data"
|
||||
></a>
|
||||
data-tip="Open provider's website to get the API key there. Note: the Map Genenerator doesn't store the key or any generated data"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -5275,9 +5292,7 @@
|
|||
<div data-tip="Click to sort by culture name" class="sortable alphabetically" data-sortby="culture">
|
||||
Culture
|
||||
</div>
|
||||
<div data-tip="Click to sort by culture group" class="sortable alphabetically" data-sortby="group">
|
||||
Group
|
||||
</div>
|
||||
<div data-tip="Click to sort by culture group" class="sortable alphabetically" data-sortby="group">Group</div>
|
||||
<div
|
||||
data-tip="Click to sort by burg population"
|
||||
class="sortable icon-sort-number-down"
|
||||
|
|
@ -5285,7 +5300,9 @@
|
|||
>
|
||||
Population
|
||||
</div>
|
||||
<div data-tip="Click to sort by burg features" class="sortable alphabetically" data-sortby="features">Features </div>
|
||||
<div data-tip="Click to sort by burg features" class="sortable alphabetically" data-sortby="features">
|
||||
Features
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="burgsBody" class="table"></div>
|
||||
|
|
@ -5342,7 +5359,11 @@
|
|||
<th data-tip="Type group name">Name</th>
|
||||
<th data-tip="Burg preview generator">Preview generator</th>
|
||||
<th data-tip="Set min population constraint" colspan="2">Population</th>
|
||||
<th data-tip="Set population percentile: 0-100, where 90 means the burg must have a population higher than 90% of all burgs">Percentile</th>
|
||||
<th
|
||||
data-tip="Set population percentile: 0-100, where 90 means the burg must have a population higher than 90% of all burgs"
|
||||
>
|
||||
Percentile
|
||||
</th>
|
||||
<th data-tip="Select allowed biomes">Biomes</th>
|
||||
<th data-tip="Select allowed states">States</th>
|
||||
<th data-tip="Select allowed cultures">Cultures</th>
|
||||
|
|
@ -5765,6 +5786,83 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id="submapTool" style="display: none" class="dialog">
|
||||
<p style="font-weight: bold">
|
||||
This operation is destructive and irreversible. It will create a completely new map based on the current one.
|
||||
Don't forget to save the .map file to your machine first!
|
||||
</p>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 0.5em">
|
||||
<div data-tip="Set points (cells) number of the submap" style="display: flex; gap: 1em">
|
||||
<div>Points number</div>
|
||||
<div>
|
||||
<input id="submapPointsInput" type="range" min="1" max="13" value="4" />
|
||||
<output id="submapPointsFormatted" style="color: #053305">10K</output>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-tip="Check to fit burg styles (icon and label size) to the submap scale">
|
||||
<input type="checkbox" class="checkbox" id="submapRescaleBurgStyles" checked />
|
||||
<label for="submapRescaleBurgStyles" class="checkbox-label">Rescale burg styles</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="transformTool" style="display: none" class="dialog">
|
||||
<div style="padding-top: 0.5em; width: 40em; font-weight: bold">
|
||||
This operation is destructive and irreversible. It will create a completely new map based on the current one.
|
||||
Don't forget to save the .map file to your machine first!
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="transformToolBody"
|
||||
style="
|
||||
padding: 0.5em 0;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: repeat(5, 1fr);
|
||||
align-items: center;
|
||||
"
|
||||
>
|
||||
<div>Points number</div>
|
||||
<div>
|
||||
<input id="transformPointsInput" type="range" min="1" max="13" value="4" />
|
||||
<output id="transformPointsFormatted" style="color: #053305">10K</output>
|
||||
</div>
|
||||
|
||||
<div>Shift</div>
|
||||
<div>
|
||||
<label>X: <input id="transformShiftX" type="number" size="4" value="0" /></label>
|
||||
<label>Y: <input id="transformShiftY" type="number" size="4" value="0" /></label>
|
||||
</div>
|
||||
|
||||
<div>Rotate</div>
|
||||
<div>
|
||||
<input id="transformAngleInput" type="range" min="0" max="359" value="0" />
|
||||
<output id="transformAngleOutput">0</output>°
|
||||
</div>
|
||||
|
||||
<div>Scale</div>
|
||||
<div>
|
||||
<input id="transformScaleInput" type="range" min="-25" max="25" value="0" />
|
||||
<output id="transformScaleResult">1</output>x
|
||||
</div>
|
||||
|
||||
<div>Mirror</div>
|
||||
<div style="display: flex; gap: 0.5em">
|
||||
<input type="checkbox" class="checkbox" id="transformMirrorH" />
|
||||
<label for="transformMirrorH" class="checkbox-label">horizontally</label>
|
||||
<input type="checkbox" class="checkbox" id="transformMirrorV" />
|
||||
<label for="transformMirrorV" class="checkbox-label">vertically</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="transformPreview" style="position: relative; overflow: hidden; outline: 1px solid #666">
|
||||
<canvas id="transformPreviewCanvas" style="position: absolute; transform-origin: center"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="options3d" class="dialog stable" style="display: none">
|
||||
<div id="options3dMesh" style="display: none">
|
||||
<div data-tip="Set map rotation speed. Set to 0 is you want to toggle off the rotation">
|
||||
|
|
@ -6084,105 +6182,6 @@
|
|||
<div id="tileStatus" style="font-style: italic"></div>
|
||||
</div>
|
||||
|
||||
<div id="resampleDialog" style="display: none" class="dialog">
|
||||
<div style="width: 34em; max-width: 80vw; font-weight: bold; padding: 6px">
|
||||
This operation is destructive and irreversible. It will create a completely new map based on the current one.
|
||||
Don't forget to save the current project to your machine first!
|
||||
</div>
|
||||
|
||||
<div
|
||||
style="
|
||||
width: auto;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: repeat(5, 1fr);
|
||||
align-items: center;
|
||||
padding: 0.5em;
|
||||
"
|
||||
>
|
||||
<div>Points number</div>
|
||||
<div>
|
||||
<input id="submapPointsInput" type="range" min="1" max="13" value="4" />
|
||||
<output id="submapPointsOutputFormatted" style="color: #053305">10K</output>
|
||||
</div>
|
||||
|
||||
<div>Shift</div>
|
||||
<div>
|
||||
<label>X: <input id="submapShiftX" type="number" size="4" value="0" /></label>
|
||||
<label>Y: <input id="submapShiftY" type="number" size="4" value="0" /></label>
|
||||
</div>
|
||||
|
||||
<div>Rotate</div>
|
||||
<div>
|
||||
<input id="submapAngleInput" type="range" min="0" max="359" value="0" />
|
||||
<output id="submapAngleOutput">0</output>°
|
||||
</div>
|
||||
|
||||
<div>Scale</div>
|
||||
<div>
|
||||
<input id="submapScaleInput" type="range" min="-25" max="25" value="0" />
|
||||
<output id="submapScaleOutput">1</output>x
|
||||
</div>
|
||||
|
||||
<div>Mirror</div>
|
||||
<div>
|
||||
<input type="checkbox" class="checkbox" id="submapMirrorH" />
|
||||
<label for="submapMirrorH" class="checkbox-label">horizontally</label>
|
||||
|
||||
<input type="checkbox" class="checkbox" id="submapMirrorV" />
|
||||
<label for="submapMirrorV" class="checkbox-label">vertically</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="submapPreview"
|
||||
style="border: 1px solid #666; margin: 1em auto; overflow: hidden; position: relative"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div id="submapOptionsDialog" style="display: none" class="dialog">
|
||||
<p style="font-weight: bold">
|
||||
This operation is destructive and irreversible. It will create a completely new map based on the current one.
|
||||
Don't forget to save the current project as a .map file first!
|
||||
</p>
|
||||
|
||||
<p>Settings to be changed: population rate, map pixel size</p>
|
||||
<p>
|
||||
Data to be copied: heightmap, biomes, religions, population, precipitation, cultures, states, provinces,
|
||||
military regiments
|
||||
</p>
|
||||
<p>Data to be regenerated: zones, routes, rivers</p>
|
||||
<p>Burgs may be remapped incorrectly, manual change is required</p>
|
||||
|
||||
<p>Keep data for:</p>
|
||||
<div data-tip="Lock all markers copied from the original map">
|
||||
<input id="submapLockMarkers" class="checkbox" type="checkbox" checked />
|
||||
<label for="submapLockMarkers" class="checkbox-label">Markers</label>
|
||||
</div>
|
||||
<div data-tip="Lock all burgs copied from the original map">
|
||||
<input id="submapLockBurgs" class="checkbox" type="checkbox" checked />
|
||||
<label for="submapLockBurgs" class="checkbox-label">Burgs</label>
|
||||
</div>
|
||||
|
||||
<p>Experimental features:</p>
|
||||
<div data-tip="Rivers on the parent map will errode land (helps to get similar river network)">
|
||||
<input id="submapDepressRivers" class="checkbox" type="checkbox" />
|
||||
<label for="submapDepressRivers" class="checkbox-label">Errode riverbeds</label>
|
||||
</div>
|
||||
<div data-tip="Rescale styles (burg labels, emblem size) to match the new scale">
|
||||
<input id="submapRescaleStyles" class="checkbox" type="checkbox" checked />
|
||||
<label for="submapRescaleStyles" class="checkbox-label">Rescale styles</label>
|
||||
</div>
|
||||
<div data-tip="Move all existing towns to the 'largetown' burg group">
|
||||
<input id="submapPromoteTowns" class="checkbox" type="checkbox" />
|
||||
<label for="submapPromoteTowns" class="checkbox-label">Promote towns to largetowns</label>
|
||||
</div>
|
||||
<div data-tip="Add lakes in depressions (can be very slow on big landmasses)">
|
||||
<input id="submapAddLakeInDepression" class="checkbox" type="checkbox" />
|
||||
<label for="submapAddLakeInDepression" class="checkbox-label">Add lakes in depressions (slow)</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alert" style="display: none" class="dialog">
|
||||
<p id="alertMessage">Warning!</p>
|
||||
</div>
|
||||
|
|
@ -7798,9 +7797,11 @@
|
|||
</symbol>
|
||||
|
||||
<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
|
||||
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"
|
||||
/>
|
||||
</symbol>
|
||||
|
||||
|
||||
<symbol id="icon-route" viewBox="0 0 512 512">
|
||||
<path
|
||||
d="M416 320h-96c-17.6 0-32-14.4-32-32s14.4-32 32-32h96s96-107 96-160-43-96-96-96-96 43-96 96c0 25.5 22.2 63.4 45.3 96H320c-52.9 0-96 43.1-96 96s43.1 96 96 96h96c17.6 0 32 14.4 32 32s-14.4 32-32 32H185.5c-16 24.8-33.8 47.7-47.3 64H416c52.9 0 96-43.1 96-96s-43.1-96-96-96zm0-256c17.7 0 32 14.3 32 32s-14.3 32-32 32-32-14.3-32-32 14.3-32 32-32zM96 256c-53 0-96 43-96 96s96 160 96 160 96-107 96-160-43-96-96-96zm0 128c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z"
|
||||
|
|
@ -7866,32 +7867,57 @@
|
|||
<pattern id="pattern_square" width="25" height="25" patternUnits="userSpaceOnUse" fill="none">
|
||||
<path d="M 25 0 L 0 0 0 25" />
|
||||
</pattern>
|
||||
|
||||
<pattern id="pattern_pointyHex" width="25" height="43.4" patternUnits="userSpaceOnUse" fill="none">
|
||||
<path d="M 0,0 12.5,7.2 25,0 M 12.5,21.7 V 7.2 Z M 0,43.4 V 28.9 L 12.5,21.7 25,28.9 v 14.5" />
|
||||
</pattern>
|
||||
|
||||
<pattern id="pattern_flatHex" width="43.4" height="25" patternUnits="userSpaceOnUse" fill="none">
|
||||
<path d="M 43.4,0 36.2,12.5 43.4,25 M 21.7,12.5 H 36.2 Z M 0,0 H 14.5 L 21.7,12.5 14.5,25 H 0" />
|
||||
</pattern>
|
||||
|
||||
<pattern id="pattern_square45deg" width="35.355" height="35.355" patternUnits="userSpaceOnUse" fill="none">
|
||||
<path d="M 0 0 L 35.355 35.355 M 0 35.355 L 35.355 0" />
|
||||
</pattern>
|
||||
|
||||
<pattern id="pattern_squareTruncated" width="25" height="25" patternUnits="userSpaceOnUse" fill="none">
|
||||
<path d="M 8.33 25 L 0 16.66 V 8.33 L 8.33 0 16.66 0 25 8.33 M 16.66 25 L 25 16.66 L 25 8.33 M 8.33 25 L 16.66 25" />
|
||||
<path
|
||||
d="M 8.33 25 L 0 16.66 V 8.33 L 8.33 0 16.66 0 25 8.33 M 16.66 25 L 25 16.66 L 25 8.33 M 8.33 25 L 16.66 25"
|
||||
/>
|
||||
</pattern>
|
||||
|
||||
<pattern id="pattern_squareTetrakis" width="25" height="25" patternUnits="userSpaceOnUse" fill="none">
|
||||
<path d="M 25 0 L 0 0 0 25 M 0 0 L 25 25 M 0 25 L 25 0 M 12.5 0 L 12.5 25 M 0 12.5 L 25 12.5 M 0 25 L 25 25 L 25 0" />
|
||||
<path
|
||||
d="M 25 0 L 0 0 0 25 M 0 0 L 25 25 M 0 25 L 25 0 M 12.5 0 L 12.5 25 M 0 12.5 L 25 12.5 M 0 25 L 25 25 L 25 0"
|
||||
/>
|
||||
</pattern>
|
||||
<pattern id="pattern_triangleHorizontal" width="41.76" height="72.33" patternUnits="userSpaceOnUse" fill="none">
|
||||
<path d="M 41.76 36.165 H 0 L 20.88 0 41.76 36.165 20.88 72.33 0 36.165 M 0 0 H 72.33 M 0 72.33 L 41.76 72.33" />
|
||||
|
||||
<pattern
|
||||
id="pattern_triangleHorizontal"
|
||||
width="41.76"
|
||||
height="72.33"
|
||||
patternUnits="userSpaceOnUse"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M 41.76 36.165 H 0 L 20.88 0 41.76 36.165 20.88 72.33 0 36.165 M 0 0 H 72.33 M 0 72.33 L 41.76 72.33"
|
||||
/>
|
||||
</pattern>
|
||||
|
||||
<pattern id="pattern_triangleVertical" width="72.33" height="41.76" patternUnits="userSpaceOnUse" fill="none">
|
||||
<path d="M 36.165 0 L 0 20.88 36.165 41.76 72.33 20.88 36.165 0 V 41.76 M 0 0 V 72.33 M 72.33 0 L 72.33 41.76">
|
||||
<path
|
||||
d="M 36.165 0 L 0 20.88 36.165 41.76 72.33 20.88 36.165 0 V 41.76 M 0 0 V 72.33 M 72.33 0 L 72.33 41.76"
|
||||
/>
|
||||
</pattern>
|
||||
|
||||
<pattern id="pattern_trihexagonal" width="25" height="43.4" patternUnits="userSpaceOnUse" fill="none">
|
||||
<path d="M 25 10.85 H 0 L 18.85 43.4 25 32.55 H 0 L 18.85 0 25 10.85" />
|
||||
</pattern>
|
||||
|
||||
<pattern id="pattern_rhombille" width="82.5" height="50" patternUnits="userSpaceOnUse" fill="none">
|
||||
<path d="M 13.8 50 L 0 25 13.8 0 H 41.2 L 27.5 25 41.2 50 55 25 41.2 0 68.8 0 82.5 25 68.8 50 M 0 25 H 27.5 M 55 25 H 82.5 M 13.8 50 H 41.2 L 68.8 50" />
|
||||
<path
|
||||
d="M 13.8 50 L 0 25 13.8 0 H 41.2 L 27.5 25 41.2 50 55 25 41.2 0 68.8 0 82.5 25 68.8 50 M 0 25 H 27.5 M 55 25 H 82.5 M 13.8 50 H 41.2 L 68.8 50"
|
||||
/>
|
||||
</pattern>
|
||||
</g>
|
||||
|
||||
|
|
@ -8051,26 +8077,25 @@
|
|||
<script src="libs/jquery-ui.min.js"></script>
|
||||
<script src="versioning.js"></script>
|
||||
<script src="libs/d3.min.js"></script>
|
||||
<script src="libs/priority-queue.min.js"></script>
|
||||
<script src="libs/flatqueue.js"></script>
|
||||
<script src="libs/delaunator.min.js"></script>
|
||||
<script src="libs/indexedDB.js?v=1.99.00"></script>
|
||||
|
||||
<script src="utils/shorthands.js?v=1.99.00"></script>
|
||||
<script src="utils/shorthands.js?v=1.106.0"></script>
|
||||
<script src="utils/commonUtils.js?v=1.103.0"></script>
|
||||
<script src="utils/arrayUtils.js?v=1.99.00"></script>
|
||||
<script src="utils/functionUtils.js?v=1.99.00"></script>
|
||||
<script src="utils/colorUtils.js?v=1.99.00"></script>
|
||||
<script src="utils/graphUtils.js?v=1.99.00"></script>
|
||||
<script src="utils/graphUtils.js?v=1.106.0"></script>
|
||||
<script src="utils/nodeUtils.js?v=1.99.00"></script>
|
||||
<script src="utils/numberUtils.js?v=1.99.00"></script>
|
||||
<script src="utils/polyfills.js?v=1.99.00"></script>
|
||||
<script src="utils/probabilityUtils.js?v=1.99.05"></script>
|
||||
<script src="utils/stringUtils.js?v=1.99.00"></script>
|
||||
<script src="utils/stringUtils.js?v=1.106.0"></script>
|
||||
<script src="utils/languageUtils.js?v=1.99.00"></script>
|
||||
<script src="utils/unitUtils.js?v=1.99.00"></script>
|
||||
<script src="utils/pathUtils.js?v=1.105.6"></script>
|
||||
<script defer src="utils/debugUtils.js?v=1.99.00"></script>
|
||||
<script src="utils/pathUtils.js?v=1.106.0"></script>
|
||||
<script defer src="utils/debugUtils.js?v=1.106.0"></script>
|
||||
|
||||
<script src="modules/voronoi.js"></script>
|
||||
<script src="config/heightmap-templates.js"></script>
|
||||
|
|
@ -8078,62 +8103,61 @@
|
|||
<script src="modules/heightmap-generator.js?v=1.99.00"></script>
|
||||
<script src="modules/features.js?v=1.104.0"></script>
|
||||
<script src="modules/ocean-layers.js?v=1.104.8"></script>
|
||||
<script src="modules/river-generator.js?v=1.99.05"></script>
|
||||
<script src="modules/river-generator.js?v=1.106.0"></script>
|
||||
<script src="modules/lakes.js?v=1.99.00"></script>
|
||||
<script src="modules/biomes.js?v=1.99.00"></script>
|
||||
<script src="modules/names-generator.js?v=1.87.14"></script>
|
||||
<script src="modules/cultures-generator.js?v=1.99.05"></script>
|
||||
<script src="modules/burgs-generator.js?v=1.105.7"></script>
|
||||
<script src="modules/states-generator.js?v=1.105.7"></script>
|
||||
<script src="modules/provinces-generator.js?v=1.104.0"></script>
|
||||
<script src="modules/routes-generator.js?v=1.104.10"></script>
|
||||
<script src="modules/religions-generator.js?v=1.99.05"></script>
|
||||
<script src="modules/names-generator.js?v=1.106.0"></script>
|
||||
<script src="modules/cultures-generator.js?v=1.106.0"></script>
|
||||
<script src="modules/burgs-and-states.js?v=1.106.0"></script>
|
||||
<script src="modules/provinces-generator.js?v=1.106.0"></script>
|
||||
<script src="modules/routes-generator.js?v=1.106.0"></script>
|
||||
<script src="modules/religions-generator.js?v=1.106.0"></script>
|
||||
<script src="modules/military-generator.js?v=1.104.0"></script>
|
||||
<script src="modules/markers-generator.js?v=1.104.0"></script>
|
||||
<script src="modules/zones-generator.js?v=1.104.0"></script>
|
||||
<script src="modules/zones-generator.js?v=1.106.0"></script>
|
||||
<script src="modules/coa-generator.js?v=1.99.00"></script>
|
||||
<script src="modules/submap.js?v=1.104.0"></script>
|
||||
<script src="modules/resample.js?v=1.105.13"></script>
|
||||
<script src="libs/alea.min.js?v1.105.0"></script>
|
||||
<script src="libs/polylabel.min.js?v1.105.0"></script>
|
||||
<script src="libs/lineclip.min.js?v1.105.0"></script>
|
||||
<script src="libs/simplify.js?v1.105.6"></script>
|
||||
<script src="modules/fonts.js?v=1.99.03"></script>
|
||||
<script src="modules/ui/layers.js?v=1.101.00"></script>
|
||||
<script src="modules/ui/layers.js?v=1.106.0"></script>
|
||||
<script src="modules/ui/measurers.js?v=1.99.00"></script>
|
||||
<script src="modules/ui/style-presets.js?v=1.100.00"></script>
|
||||
<script src="modules/ui/general.js?v=1.100.00"></script>
|
||||
<script src="modules/ui/options.js?v=1.105.0"></script>
|
||||
<script src="main.js?v=1.105.2"></script>
|
||||
<script src="modules/ui/options.js?v=1.106.0"></script>
|
||||
<script src="main.js?v=1.106.0"></script>
|
||||
|
||||
<script defer src="modules/relief-icons.js?v=1.99.05"></script>
|
||||
<script defer src="modules/ui/style.js?v=1.104.0"></script>
|
||||
<script defer src="modules/ui/editors.js?v=1.105.2"></script>
|
||||
<script defer src="modules/ui/tools.js?v=1.104.0"></script>
|
||||
<script defer src="modules/ui/editors.js?v=1.105.23"></script>
|
||||
<script defer src="modules/ui/tools.js?v=1.106.0"></script>
|
||||
<script defer src="modules/ui/world-configurator.js?v=1.105.4"></script>
|
||||
<script defer src="modules/ui/heightmap-editor.js?v=1.105.2"></script>
|
||||
<script defer src="modules/ui/provinces-editor.js?v=1.104.0"></script>
|
||||
<script defer src="modules/ui/biomes-editor.js?v=1.99.05"></script>
|
||||
<script defer src="modules/ui/namesbase-editor.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/namesbase-editor.js?v=1.105.11"></script>
|
||||
<script defer src="modules/ui/elevation-profile.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/temperature-graph.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/routes-editor.js?v=1.104.3"></script>
|
||||
<script defer src="modules/ui/routes-creator.js?v=1.104.3"></script>
|
||||
<script defer src="modules/ui/route-group-editor.js?v=1.103.8"></script>
|
||||
<script defer src="modules/ui/ice-editor.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/lakes-editor.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/lakes-editor.js?v=1.106.0"></script>
|
||||
<script defer src="modules/ui/coastline-editor.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/labels-editor.js?v=1.101.00"></script>
|
||||
<script defer src="modules/ui/rivers-editor.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/rivers-creator.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/labels-editor.js?v=1.106.0"></script>
|
||||
<script defer src="modules/ui/rivers-editor.js?v=1.106.0"></script>
|
||||
<script defer src="modules/ui/rivers-creator.js?v=1.106.0"></script>
|
||||
<script defer src="modules/ui/relief-editor.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/burg-editor.js?v=1.102.00"></script>
|
||||
<script defer src="modules/ui/burg-group-editor.js?v=1.106.0"></script>
|
||||
<script defer src="modules/ui/units-editor.js?v=1.104.0"></script>
|
||||
<script defer src="modules/ui/notes-editor.js?v=1.99.06"></script>
|
||||
<script defer src="modules/ui/ai-generator.js?v=1.99.09"></script>
|
||||
<script defer src="modules/ui/ai-generator.js?v=1.105.22"></script>
|
||||
<script defer src="modules/ui/diplomacy-editor.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/zones-editor.js?v=1.100.00"></script>
|
||||
<script defer src="modules/ui/burgs-overview.js?v=1.105.7"></script>
|
||||
<script defer src="modules/ui/zones-editor.js?v=1.105.20"></script>
|
||||
<script defer src="modules/ui/burgs-overview.js?v=1.105.15"></script>
|
||||
<script defer src="modules/ui/routes-overview.js?v=1.104.3"></script>
|
||||
<script defer src="modules/ui/rivers-overview.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/military-overview.js?v=1.99.00"></script>
|
||||
|
|
@ -8144,17 +8168,18 @@
|
|||
<script defer src="modules/ui/emblems-editor.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/markers-editor.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/3d.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/submap.js?v=1.99.10"></script>
|
||||
<script defer src="modules/ui/submap-tool.js?v=1.105.13"></script>
|
||||
<script defer src="modules/ui/transform-tool.js?v=1.105.13"></script>
|
||||
<script defer src="modules/ui/hotkeys.js?v=1.104.0"></script>
|
||||
<script defer src="modules/coa-renderer.js?v=1.99.00"></script>
|
||||
<script defer src="libs/rgbquant.min.js"></script>
|
||||
<script defer src="libs/jquery.ui.touch-punch.min.js"></script>
|
||||
<script defer src="modules/io/save.js?v=1.100.00"></script>
|
||||
<script defer src="modules/io/load.js?v=1.105.5"></script>
|
||||
<script defer src="modules/io/cloud.js?v=1.99.00"></script>
|
||||
<script defer src="modules/io/load.js?v=1.105.24"></script>
|
||||
<script defer src="modules/io/cloud.js?v=1.106.0"></script>
|
||||
<script defer src="modules/io/export.js?v=1.100.00"></script>
|
||||
|
||||
<script defer src="modules/renderers/draw-features.js?v=1.104.15"></script>
|
||||
<script defer src="modules/renderers/draw-features.js?v=1.106.0"></script>
|
||||
<script defer src="modules/renderers/draw-borders.js?v=1.104.0"></script>
|
||||
<script defer src="modules/renderers/draw-heightmap.js?v=1.104.0"></script>
|
||||
<script defer src="modules/renderers/draw-markers.js?v=1.104.0"></script>
|
||||
|
|
@ -8162,7 +8187,7 @@
|
|||
<script defer src="modules/renderers/draw-temperature.js?v=1.104.0"></script>
|
||||
<script defer src="modules/renderers/draw-emblems.js?v=1.104.0"></script>
|
||||
<script defer src="modules/renderers/draw-military.js?v=1.104.13"></script>
|
||||
<script defer src="modules/renderers/draw-state-labels.js?v=1.104.0"></script>
|
||||
<script defer src="modules/renderers/draw-state-labels.js?v=1.106.0"></script>
|
||||
<script defer src="modules/renderers/draw-burg-labels.js?v=1.104.0"></script>
|
||||
<script defer src="modules/renderers/draw-burg-icons.js?v=1.104.0"></script>
|
||||
</body>
|
||||
|
|
|
|||
1
libs/priority-queue.min.js
vendored
1
libs/priority-queue.min.js
vendored
File diff suppressed because one or more lines are too long
19
main.js
19
main.js
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
// set debug options
|
||||
const PRODUCTION = location.hostname && location.hostname !== "localhost" && location.hostname !== "127.0.0.1";
|
||||
const DEBUG = localStorage.getItem("debug");
|
||||
const DEBUG = JSON.safeParse(localStorage.getItem("debug")) || {};
|
||||
const INFO = true;
|
||||
const TIME = true;
|
||||
const WARN = true;
|
||||
|
|
@ -494,14 +494,6 @@ function resetZoom(d = 1000) {
|
|||
svg.transition().duration(d).call(zoom.transform, d3.zoomIdentity);
|
||||
}
|
||||
|
||||
// calculate x y extreme points of viewBox
|
||||
function getViewBoxExtent() {
|
||||
return [
|
||||
[Math.abs(viewX / scale), Math.abs(viewY / scale)],
|
||||
[Math.abs(viewX / scale) + graphWidth / scale, Math.abs(viewY / scale) + graphHeight / scale]
|
||||
];
|
||||
}
|
||||
|
||||
// active zooming feature
|
||||
function invokeActiveZooming() {
|
||||
const isOptimized = shapeRendering.value === "optimizeSpeed";
|
||||
|
|
@ -728,10 +720,11 @@ function setSeed(precreatedSeed) {
|
|||
|
||||
function addLakesInDeepDepressions() {
|
||||
TIME && console.time("addLakesInDeepDepressions");
|
||||
const elevationLimit = +byId("lakeElevationLimitOutput").value;
|
||||
if (elevationLimit === 80) return;
|
||||
|
||||
const {cells, features} = grid;
|
||||
const {c, h, b} = cells;
|
||||
const ELEVATION_LIMIT = +byId("lakeElevationLimitOutput").value;
|
||||
if (ELEVATION_LIMIT === 80) return;
|
||||
|
||||
for (const i of cells.i) {
|
||||
if (b[i] || h[i] < 20) continue;
|
||||
|
|
@ -740,7 +733,7 @@ function addLakesInDeepDepressions() {
|
|||
if (h[i] > minHeight) continue;
|
||||
|
||||
let deep = true;
|
||||
const threshold = h[i] + ELEVATION_LIMIT;
|
||||
const threshold = h[i] + elevationLimit;
|
||||
const queue = [i];
|
||||
const checked = [];
|
||||
checked[i] = true;
|
||||
|
|
@ -926,7 +919,7 @@ function calculateTemperatures() {
|
|||
const [, y] = grid.points[rowCellId];
|
||||
const rowLatitude = mapCoordinates.latN - (y / graphHeight) * mapCoordinates.latT; // [90; -90]
|
||||
const tempSeaLevel = calculateSeaLevelTemp(rowLatitude);
|
||||
DEBUG && console.info(`${rn(rowLatitude)}° sea temperature: ${rn(tempSeaLevel)}°C`);
|
||||
DEBUG.temperature && console.info(`${rn(rowLatitude)}° sea temperature: ${rn(tempSeaLevel)}°C`);
|
||||
|
||||
for (let cellId = rowCellId; cellId < rowCellId + grid.cellsX; cellId++) {
|
||||
const tempAltitudeDrop = getAltitudeTemperatureDrop(cells.h[cellId]);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ window.Cultures = (function () {
|
|||
cells = pack.cells;
|
||||
|
||||
const cultureIds = new Uint16Array(cells.i.length); // cell cultures
|
||||
let count = Math.min(+culturesInput.value, +culturesSet.selectedOptions[0].dataset.max);
|
||||
|
||||
const culturesInputNumber = +byId("culturesInput").value;
|
||||
const culturesInSetNumber = +byId("culturesSet").selectedOptions[0].dataset.max;
|
||||
let count = Math.min(culturesInputNumber, culturesInSetNumber);
|
||||
|
||||
const populated = cells.i.filter(i => cells.s[i]); // populated cells
|
||||
if (populated.length < count * 25) {
|
||||
|
|
@ -120,26 +123,26 @@ window.Cultures = (function () {
|
|||
cultures.forEach(c => (c.base = c.base % nameBases.length));
|
||||
|
||||
function selectCultures(culturesNumber) {
|
||||
let def = getDefault(culturesNumber);
|
||||
let defaultCultures = getDefault(culturesNumber);
|
||||
const cultures = [];
|
||||
|
||||
pack.cultures?.forEach(function (culture) {
|
||||
if (culture.lock) cultures.push(culture);
|
||||
if (culture.lock && !culture.removed) cultures.push(culture);
|
||||
});
|
||||
|
||||
if (!cultures.length) {
|
||||
if (culturesNumber === def.length) return def;
|
||||
if (def.every(d => d.odd === 1)) return def.splice(0, culturesNumber);
|
||||
if (culturesNumber === defaultCultures.length) return defaultCultures;
|
||||
if (defaultCultures.every(d => d.odd === 1)) return defaultCultures.splice(0, culturesNumber);
|
||||
}
|
||||
|
||||
for (let culture, rnd, i = 0; cultures.length < culturesNumber && def.length > 0; ) {
|
||||
for (let culture, rnd, i = 0; cultures.length < culturesNumber && defaultCultures.length > 0; ) {
|
||||
do {
|
||||
rnd = rand(def.length - 1);
|
||||
culture = def[rnd];
|
||||
rnd = rand(defaultCultures.length - 1);
|
||||
culture = defaultCultures[rnd];
|
||||
i++;
|
||||
} while (i < 200 && !P(culture.odd));
|
||||
cultures.push(culture);
|
||||
def.splice(rnd, 1);
|
||||
defaultCultures.splice(rnd, 1);
|
||||
}
|
||||
return cultures;
|
||||
}
|
||||
|
|
@ -515,7 +518,7 @@ window.Cultures = (function () {
|
|||
TIME && console.time("expandCultures");
|
||||
const {cells, cultures} = pack;
|
||||
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.priority - b.priority});
|
||||
const queue = new FlatQueue();
|
||||
const cost = [];
|
||||
|
||||
const neutralRate = byId("neutralRate")?.valueAsNumber || 1;
|
||||
|
|
@ -535,11 +538,11 @@ window.Cultures = (function () {
|
|||
|
||||
for (const culture of cultures) {
|
||||
if (!culture.i || culture.removed || culture.lock) continue;
|
||||
queue.queue({cellId: culture.center, cultureId: culture.i, priority: 0});
|
||||
queue.push({cellId: culture.center, cultureId: culture.i, priority: 0}, 0);
|
||||
}
|
||||
|
||||
while (queue.length) {
|
||||
const {cellId, priority, cultureId} = queue.dequeue();
|
||||
const {cellId, priority, cultureId} = queue.pop();
|
||||
const {type, expansionism} = cultures[cultureId];
|
||||
|
||||
cells.c[cellId].forEach(neibCellId => {
|
||||
|
|
@ -563,7 +566,7 @@ window.Cultures = (function () {
|
|||
if (!cost[neibCellId] || totalCost < cost[neibCellId]) {
|
||||
if (cells.pop[neibCellId] > 0) cells.culture[neibCellId] = cultureId; // assign culture to populated cell
|
||||
cost[neibCellId] = totalCost;
|
||||
queue.queue({cellId: neibCellId, cultureId, priority: totalCost});
|
||||
queue.push({cellId: neibCellId, cultureId, priority: totalCost}, totalCost);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -943,7 +943,21 @@ export function resolveVersionConflicts(mapVersion) {
|
|||
viewbox.select("#coastline").selectAll("path, use").remove();
|
||||
drawFeatures();
|
||||
|
||||
// v1.106.0 change burg groups and added customizable icons
|
||||
// v1.104.0 introduced bugs with state borders
|
||||
regions
|
||||
.attr("opacity", null)
|
||||
.attr("stroke-width", null)
|
||||
.attr("letter-spacing", null)
|
||||
.attr("fill", null)
|
||||
.attr("stroke", null);
|
||||
|
||||
// pole can be missing for some states/provinces
|
||||
BurgsAndStates.getPoles();
|
||||
Provinces.getPoles();
|
||||
}
|
||||
|
||||
if (isOlderThan("1.107.0")) {
|
||||
// v1.107.0 changeв burg groups and added customizable icons
|
||||
icons.selectAll("circle, use").remove();
|
||||
|
||||
const groups = Array.from(document.querySelectorAll("#burgIcons > g")).map(g => g.id);
|
||||
|
|
|
|||
|
|
@ -266,6 +266,7 @@ function getTypeOptions(type) {
|
|||
function getBaseOptions(base) {
|
||||
let options = "";
|
||||
nameBases.forEach((n, i) => (options += `<option ${base === i ? "selected" : ""} value="${i}">${n.name}</option>`));
|
||||
if (!nameBases[base]) options += `<option selected value="${base}">removed</option>`; // in case namesbase was removed
|
||||
return options;
|
||||
}
|
||||
|
||||
|
|
@ -344,10 +345,13 @@ function cultureChangeName() {
|
|||
}
|
||||
|
||||
function cultureRegenerateName() {
|
||||
const culture = +this.parentNode.dataset.id;
|
||||
const name = Names.getCultureShort(culture);
|
||||
const cultureId = +this.parentNode.dataset.id;
|
||||
const base = pack.cultures[cultureId].base;
|
||||
if (!nameBases[base]) return tip("Namesbase is not defined, please select a valid namesbase", false, "error", 5000);
|
||||
|
||||
const name = Names.getCultureShort(cultureId);
|
||||
this.parentNode.querySelector("input.cultureName").value = name;
|
||||
pack.cultures[culture].name = name;
|
||||
pack.cultures[cultureId].name = name;
|
||||
}
|
||||
|
||||
function cultureChangeExpansionism() {
|
||||
|
|
@ -493,12 +497,15 @@ function cultureRegenerateBurgs() {
|
|||
if (customization === 4) return;
|
||||
|
||||
const cultureId = +this.parentNode.dataset.id;
|
||||
const cBurgs = pack.burgs.filter(b => b.culture === cultureId && !b.lock);
|
||||
cBurgs.forEach(b => {
|
||||
const base = pack.cultures[cultureId].base;
|
||||
if (!nameBases[base]) return tip("Namesbase is not defined, please select a valid namesbase", false, "error", 5000);
|
||||
|
||||
const cultureBurgs = pack.burgs.filter(b => b.culture === cultureId && !b.removed && !b.lock);
|
||||
cultureBurgs.forEach(b => {
|
||||
b.name = Names.getCulture(cultureId);
|
||||
labels.select("[data-id='" + b.i + "']").text(b.name);
|
||||
});
|
||||
tip(`Names for ${cBurgs.length} burgs are regenerated`, false, "success");
|
||||
tip(`Names for ${cultureBurgs.length} burgs are regenerated`, false, "success");
|
||||
}
|
||||
|
||||
function removeCulture(cultureId) {
|
||||
|
|
@ -848,14 +855,15 @@ async function uploadCulturesData() {
|
|||
this.value = "";
|
||||
const csv = await file.text();
|
||||
const data = d3.csvParse(csv, d => ({
|
||||
i: +d.Id,
|
||||
name: d.Name,
|
||||
i: +d.Id,
|
||||
color: d.Color,
|
||||
expansionism: +d.Expansionism,
|
||||
type: d.Type,
|
||||
population: +d.Population,
|
||||
emblemsShape: d["Emblems Shape"],
|
||||
origins: d.Origins
|
||||
origins: d.Origins,
|
||||
namesbase: d.Namesbase
|
||||
}));
|
||||
|
||||
const {cultures, cells} = pack;
|
||||
|
|
@ -882,7 +890,7 @@ async function uploadCulturesData() {
|
|||
culture.i
|
||||
);
|
||||
} else {
|
||||
current = {i: cultures.length, center: ra(populated), area: 0, cells: 0, origin: 0, rural: 0, urban: 0};
|
||||
current = {i: cultures.length, center: ra(populated), area: 0, cells: 0, origins: [0], rural: 0, urban: 0};
|
||||
cultures.push(current);
|
||||
}
|
||||
|
||||
|
|
@ -902,6 +910,10 @@ async function uploadCulturesData() {
|
|||
else current.type = "Generic";
|
||||
}
|
||||
|
||||
culture.origins = current.i ? restoreOrigins(culture.origins || "") : [null];
|
||||
current.shield = shapes.includes(culture.emblemsShape) ? culture.emblemsShape : "heater";
|
||||
current.base = nameBases.findIndex(n => n.name == culture.namesbase); // can be -1 if namesbase is not found
|
||||
|
||||
function restoreOrigins(originsString) {
|
||||
const originNames = originsString
|
||||
.replaceAll('"', "")
|
||||
|
|
@ -917,12 +929,6 @@ async function uploadCulturesData() {
|
|||
current.origins = originIds.filter(id => id !== null);
|
||||
if (!current.origins.length) current.origins = [0];
|
||||
}
|
||||
|
||||
culture.origins = current.i ? restoreOrigins(culture.origins || "") : [null];
|
||||
current.shield = shapes.includes(culture.emblemsShape) ? culture.emblemsShape : "heater";
|
||||
|
||||
const nameBaseIndex = nameBases.findIndex(n => n.name == culture.namesbase);
|
||||
current.base = nameBaseIndex === -1 ? 0 : nameBaseIndex;
|
||||
}
|
||||
|
||||
cultures.filter(c => c.removed).forEach(c => removeCulture(c.i));
|
||||
|
|
|
|||
|
|
@ -583,4 +583,10 @@ James Benware
|
|||
FortunesFaded
|
||||
breadsticks
|
||||
Murderbits
|
||||
Ben Jones`;
|
||||
Ben Jones
|
||||
Marco Faltracco
|
||||
L
|
||||
silentArtifact
|
||||
Keith Potter
|
||||
Morgan Gilbert
|
||||
Alengork Gamer`;
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ window.Cloud = (function () {
|
|||
|
||||
async save(fileName, contents) {
|
||||
const resp = await this.call("filesUpload", {path: "/" + fileName, contents});
|
||||
DEBUG && console.info("Dropbox response:", resp);
|
||||
DEBUG.cloud && console.info("Dropbox response:", resp);
|
||||
return true;
|
||||
},
|
||||
|
||||
|
|
@ -104,7 +104,7 @@ window.Cloud = (function () {
|
|||
|
||||
// Callback function for auth window
|
||||
async setDropBoxToken(token) {
|
||||
DEBUG && console.info("Access token:", token);
|
||||
DEBUG.cloud && console.info("Access token:", token);
|
||||
setToken(this.name, token);
|
||||
await this.connect(token);
|
||||
this.authWindow.close();
|
||||
|
|
@ -131,7 +131,7 @@ window.Cloud = (function () {
|
|||
allow_download: true
|
||||
};
|
||||
const resp = await this.call("sharingCreateSharedLinkWithSettings", {path, settings});
|
||||
DEBUG && console.info("Dropbox link object:", resp.result);
|
||||
DEBUG.cloud && console.info("Dropbox link object:", resp.result);
|
||||
return resp.result.url;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ async function quickLoad() {
|
|||
async function loadFromDropbox() {
|
||||
const mapPath = byId("loadFromDropboxSelect")?.value;
|
||||
|
||||
DEBUG && console.info("Loading map from Dropbox:", mapPath);
|
||||
console.info("Loading map from Dropbox:", mapPath);
|
||||
const blob = await Cloud.providers.dropbox.load(mapPath);
|
||||
uploadMap(blob);
|
||||
}
|
||||
|
|
@ -96,6 +96,7 @@ function showUploadErrorMessage(error, URL, random) {
|
|||
title: "Loading error",
|
||||
width: "32em",
|
||||
buttons: {
|
||||
"Clear cache": () => cleanupData(),
|
||||
OK: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
|
|
@ -152,11 +153,21 @@ async function uncompress(compressedData) {
|
|||
async function parseLoadedResult(result) {
|
||||
try {
|
||||
const resultAsString = new TextDecoder().decode(result);
|
||||
|
||||
// data can be in FMG internal format or base64 encoded
|
||||
const isDelimited = resultAsString.substring(0, 10).includes("|");
|
||||
const decoded = isDelimited ? resultAsString : decodeURIComponent(atob(resultAsString));
|
||||
let content = isDelimited ? resultAsString : decodeURIComponent(atob(resultAsString));
|
||||
|
||||
const mapData = decoded.split("\r\n"); // split by CRLF
|
||||
// fix if svg part has CRLF line endings instead of LF
|
||||
const svgMatch = content.match(/<svg[^>]*id="map"[\s\S]*?<\/svg>/);
|
||||
const svgContent = svgMatch[0];
|
||||
const hasCrlfEndings = svgContent.includes("\r\n");
|
||||
if (hasCrlfEndings) {
|
||||
const correctedSvgContent = svgContent.replace(/\r\n/g, "\n");
|
||||
content = content.replace(svgContent, correctedSvgContent);
|
||||
}
|
||||
|
||||
const mapData = content.split("\r\n"); // split by CRLF
|
||||
const mapVersion = parseMapVersion(mapData[0].split("|")[0] || mapData[0] || "");
|
||||
|
||||
return {mapData, mapVersion};
|
||||
|
|
@ -195,6 +206,7 @@ function showUploadMessage(type, mapData, mapVersion) {
|
|||
$("#alert").dialog({
|
||||
title,
|
||||
buttons: {
|
||||
"Clear cache": () => cleanupData(),
|
||||
OK: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
|
|
@ -459,7 +471,7 @@ async function parseLoadedData(data, mapVersion) {
|
|||
|
||||
{
|
||||
// dynamically import and run auto-update script
|
||||
const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.105.5");
|
||||
const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.105.24");
|
||||
resolveVersionConflicts(mapVersion);
|
||||
}
|
||||
|
||||
|
|
@ -735,6 +747,7 @@ async function parseLoadedData(data, mapVersion) {
|
|||
title: "Loading error",
|
||||
maxWidth: "50em",
|
||||
buttons: {
|
||||
"Clear cache": () => cleanupData(),
|
||||
"Select file": function () {
|
||||
$(this).dialog("close");
|
||||
mapToLoad.click();
|
||||
|
|
|
|||
|
|
@ -48,18 +48,28 @@ window.Names = (function () {
|
|||
return chain;
|
||||
};
|
||||
|
||||
// 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]?.b ? calculateChain(nameBases[i].b) : null;
|
||||
};
|
||||
|
||||
// update chains for all used bases
|
||||
const clearChains = () => (chains = []);
|
||||
const clearChains = () => {
|
||||
chains = [];
|
||||
};
|
||||
|
||||
// generate name using Markov's chain
|
||||
const getBase = function (base, min, max, dupl) {
|
||||
if (base === undefined) {
|
||||
ERROR && console.error("Please define a base");
|
||||
return;
|
||||
if (base === undefined) return ERROR && console.error("Please define a base");
|
||||
|
||||
if (nameBases[base] === undefined) {
|
||||
if (nameBases[0]) {
|
||||
WARN && console.warn("Namebase " + base + " is not found. First available namebase will be used");
|
||||
base = 0;
|
||||
} else {
|
||||
ERROR && console.error("Namebase " + base + " is not found");
|
||||
return "ERROR";
|
||||
}
|
||||
}
|
||||
|
||||
if (!chains[base]) updateChain(base);
|
||||
|
||||
const data = chains[base];
|
||||
|
|
@ -141,16 +151,8 @@ window.Names = (function () {
|
|||
|
||||
// generate short name for base
|
||||
const getBaseShort = function (base) {
|
||||
if (nameBases[base] === undefined) {
|
||||
tip(
|
||||
`Namebase ${base} does not exist. Please upload custom namebases of change the base in Cultures Editor`,
|
||||
false,
|
||||
"error"
|
||||
);
|
||||
base = 1;
|
||||
}
|
||||
const min = nameBases[base].min - 1;
|
||||
const max = Math.max(nameBases[base].max - 2, min);
|
||||
const min = nameBases[base] ? nameBases[base].min - 1 : null;
|
||||
const max = min ? Math.max(nameBases[base].max - 2, min) : null;
|
||||
return getBase(base, min, max, "", 0);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -77,18 +77,18 @@ window.Provinces = (function () {
|
|||
});
|
||||
|
||||
// expand generated provinces
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||
const queue = new FlatQueue();
|
||||
const cost = [];
|
||||
|
||||
provinces.forEach(p => {
|
||||
if (!p.i || p.removed || isProvinceLocked(p)) return;
|
||||
provinceIds[p.center] = p.i;
|
||||
queue.queue({e: p.center, p: 0, province: p.i, state: p.state});
|
||||
queue.push({e: p.center, province: p.i, state: p.state, p: 0}, 0);
|
||||
cost[p.center] = 1;
|
||||
});
|
||||
|
||||
while (queue.length) {
|
||||
const {e, p, province, state} = queue.dequeue();
|
||||
const {e, p, province, state} = queue.pop();
|
||||
|
||||
cells.c[e].forEach(e => {
|
||||
if (isProvinceCellLocked(e)) return; // do not overwrite cell of locked provinces
|
||||
|
|
@ -103,7 +103,7 @@ window.Provinces = (function () {
|
|||
if (!cost[e] || totalCost < cost[e]) {
|
||||
if (land) provinceIds[e] = province; // assign province to a cell
|
||||
cost[e] = totalCost;
|
||||
queue.queue({e, p: totalCost, province, state});
|
||||
queue.push({e, province, state, p: totalCost}, totalCost);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -158,9 +158,9 @@ window.Provinces = (function () {
|
|||
// expand province
|
||||
const cost = [];
|
||||
cost[center] = 1;
|
||||
queue.queue({e: center, p: 0});
|
||||
queue.push({e: center, p: 0}, 0);
|
||||
while (queue.length) {
|
||||
const {e, p} = queue.dequeue();
|
||||
const {e, p} = queue.pop();
|
||||
|
||||
cells.c[e].forEach(nextCellId => {
|
||||
if (provinceIds[nextCellId]) return;
|
||||
|
|
@ -173,7 +173,7 @@ window.Provinces = (function () {
|
|||
if (!cost[nextCellId] || totalCost < cost[nextCellId]) {
|
||||
if (land && cells.state[nextCellId] === s.i) provinceIds[nextCellId] = provinceId; // assign province to a cell
|
||||
cost[nextCellId] = totalCost;
|
||||
queue.queue({e: nextCellId, p: totalCost});
|
||||
queue.push({e: nextCellId, p: totalCost}, totalCost);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -216,15 +216,15 @@ window.Provinces = (function () {
|
|||
// check if there is a land way within the same state between two cells
|
||||
function isPassable(from, to) {
|
||||
if (cells.f[from] !== cells.f[to]) return false; // on different islands
|
||||
const queue = [from],
|
||||
const passableQueue = [from],
|
||||
used = new Uint8Array(cells.i.length),
|
||||
state = cells.state[from];
|
||||
while (queue.length) {
|
||||
const current = queue.pop();
|
||||
while (passableQueue.length) {
|
||||
const current = passableQueue.pop();
|
||||
if (current === to) return true; // way is found
|
||||
cells.c[current].forEach(c => {
|
||||
if (used[c] || cells.h[c] < 20 || cells.state[c] !== state) return;
|
||||
queue.push(c);
|
||||
passableQueue.push(c);
|
||||
used[c] = 1;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -695,7 +695,7 @@ window.Religions = (function () {
|
|||
const {cells, routes} = pack;
|
||||
const religionIds = spreadFolkReligions(religions);
|
||||
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||
const queue = new FlatQueue();
|
||||
const cost = [];
|
||||
|
||||
// limit cost for organized religions growth
|
||||
|
|
@ -705,14 +705,14 @@ window.Religions = (function () {
|
|||
.filter(r => r.i && !r.lock && r.type !== "Folk" && !r.removed)
|
||||
.forEach(r => {
|
||||
religionIds[r.center] = r.i;
|
||||
queue.queue({e: r.center, p: 0, r: r.i, s: cells.state[r.center]});
|
||||
queue.push({e: r.center, p: 0, r: r.i, s: cells.state[r.center]}, 0);
|
||||
cost[r.center] = 1;
|
||||
});
|
||||
|
||||
const religionsMap = new Map(religions.map(r => [r.i, r]));
|
||||
|
||||
while (queue.length) {
|
||||
const {e: cellId, p, r, s: state} = queue.dequeue();
|
||||
const {e: cellId, p, r, s: state} = queue.pop();
|
||||
const {culture, expansion, expansionism} = religionsMap.get(r);
|
||||
|
||||
cells.c[cellId].forEach(nextCell => {
|
||||
|
|
@ -732,7 +732,7 @@ window.Religions = (function () {
|
|||
if (cells.culture[nextCell]) religionIds[nextCell] = r; // assign religion to cell
|
||||
cost[nextCell] = totalCost;
|
||||
|
||||
queue.queue({e: nextCell, p: totalCost, r, s: state});
|
||||
queue.push({e: nextCell, p: totalCost, r, s: state}, totalCost);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ function getFeaturePath(feature) {
|
|||
const clippedPoints = clipPoly(simplifiedPoints, 1);
|
||||
|
||||
const lineGen = d3.line().curve(d3.curveBasisClosed);
|
||||
const path = round(lineGen(clippedPoints));
|
||||
const path = round(lineGen(clippedPoints)) + "Z";
|
||||
|
||||
return path;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@ function drawStateLabels(list) {
|
|||
// increase step to 15 or 30 to make it faster and more horyzontal
|
||||
// decrease step to 5 to improve accuracy
|
||||
const ANGLE_STEP = 9;
|
||||
const raycast = precalculateAngles(ANGLE_STEP);
|
||||
const angles = precalculateAngles(ANGLE_STEP);
|
||||
|
||||
const INITIAL_DISTANCE = 10;
|
||||
const DISTANCE_STEP = 15;
|
||||
const MAX_ITERATIONS = 100;
|
||||
const LENGTH_START = 5;
|
||||
const LENGTH_STEP = 5;
|
||||
const LENGTH_MAX = 300;
|
||||
|
||||
const labelPaths = getLabelPaths();
|
||||
const letterLength = checkExampleLetterLength();
|
||||
|
|
@ -35,87 +35,27 @@ function drawStateLabels(list) {
|
|||
if (list && !list.includes(state.i)) continue;
|
||||
|
||||
const offset = getOffsetWidth(state.cells);
|
||||
const maxLakeSize = state.cells / 50;
|
||||
const maxLakeSize = state.cells / 20;
|
||||
const [x0, y0] = state.pole;
|
||||
|
||||
const offsetPoints = new Map(
|
||||
(offset ? raycast : []).map(({angle, x: x1, y: y1}) => {
|
||||
const [x, y] = [x0 + offset * x1, y0 + offset * y1];
|
||||
return [angle, {x, y}];
|
||||
})
|
||||
);
|
||||
|
||||
const distances = raycast.map(({angle, x: dx, y: dy, modifier}) => {
|
||||
let distanceMin;
|
||||
const distance1 = getMaxDistance(state.i, {x: x0, y: y0}, dx, dy, maxLakeSize);
|
||||
|
||||
if (offset) {
|
||||
const point2 = offsetPoints.get(angle - 90 < 0 ? angle + 270 : angle - 90);
|
||||
const distance2 = getMaxDistance(state.i, point2, dx, dy, maxLakeSize);
|
||||
|
||||
const point3 = offsetPoints.get(angle + 90 >= 360 ? angle - 270 : angle + 90);
|
||||
const distance3 = getMaxDistance(state.i, point3, dx, dy, maxLakeSize);
|
||||
|
||||
distanceMin = Math.min(distance1, distance2, distance3);
|
||||
} else {
|
||||
distanceMin = distance1;
|
||||
}
|
||||
|
||||
const [x, y] = [x0 + distanceMin * dx, y0 + distanceMin * dy];
|
||||
return {angle, distance: distanceMin * modifier, x, y};
|
||||
const rays = angles.map(({angle, dx, dy}) => {
|
||||
const {length, x, y} = raycast({stateId: state.i, x0, y0, dx, dy, maxLakeSize, offset});
|
||||
return {angle, length, x, y};
|
||||
});
|
||||
const [ray1, ray2] = findBestRayPair(rays);
|
||||
|
||||
const {
|
||||
angle,
|
||||
x: x1,
|
||||
y: y1
|
||||
} = distances.reduce(
|
||||
(acc, {angle, distance, x, y}) => {
|
||||
if (distance > acc.distance) return {angle, distance, x, y};
|
||||
return acc;
|
||||
},
|
||||
{angle: 0, distance: 0, x: 0, y: 0}
|
||||
);
|
||||
const pathPoints = [[ray1.x, ray1.y], state.pole, [ray2.x, ray2.y]];
|
||||
if (ray1.x > ray2.x) pathPoints.reverse();
|
||||
|
||||
const oppositeAngle = angle >= 180 ? angle - 180 : angle + 180;
|
||||
const {x: x2, y: y2} = distances.reduce(
|
||||
(acc, {angle, distance, x, y}) => {
|
||||
const angleDif = getAnglesDif(angle, oppositeAngle);
|
||||
const score = distance * getAngleModifier(angleDif);
|
||||
if (score > acc.score) return {angle, score, x, y};
|
||||
return acc;
|
||||
},
|
||||
{angle: 0, score: 0, x: 0, y: 0}
|
||||
);
|
||||
if (DEBUG.stateLabels) {
|
||||
drawPoint(state.pole, {color: "black", radius: 1});
|
||||
drawPath(pathPoints, {color: "black", width: 0.2});
|
||||
}
|
||||
|
||||
const pathPoints = [[x1, y1], state.pole, [x2, y2]];
|
||||
if (x1 > x2) pathPoints.reverse();
|
||||
labelPaths.push([state.i, pathPoints]);
|
||||
}
|
||||
|
||||
return labelPaths;
|
||||
|
||||
function getMaxDistance(stateId, point, dx, dy, maxLakeSize) {
|
||||
let distance = INITIAL_DISTANCE;
|
||||
|
||||
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
||||
const [x, y] = [point.x + distance * dx, point.y + distance * dy];
|
||||
const cellId = findCell(x, y, DISTANCE_STEP);
|
||||
|
||||
// drawPoint([x, y], {color: cellId && isPassable(cellId) ? "blue" : "red", radius: 0.8});
|
||||
|
||||
if (!cellId || !isPassable(cellId)) break;
|
||||
distance += DISTANCE_STEP;
|
||||
}
|
||||
|
||||
return distance;
|
||||
|
||||
function isPassable(cellId) {
|
||||
const feature = features[cells.f[cellId]];
|
||||
if (feature.type === "lake") return feature.cells <= maxLakeSize;
|
||||
return stateIds[cellId] === stateId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkExampleLetterLength() {
|
||||
|
|
@ -129,7 +69,7 @@ function drawStateLabels(list) {
|
|||
|
||||
function drawLabelPath(letterLength) {
|
||||
const mode = options.stateLabelsMode || "auto";
|
||||
const lineGen = d3.line().curve(d3.curveBundle.beta(1));
|
||||
const lineGen = d3.line().curve(d3.curveNatural);
|
||||
|
||||
const textGroup = d3.select("g#labels > g#states");
|
||||
const pathGroup = d3.select("defs > g#deftemp > g#textPaths");
|
||||
|
|
@ -192,35 +132,15 @@ function drawStateLabels(list) {
|
|||
const text = pathLength > state.fullName.length * 1.8 ? state.fullName : state.name;
|
||||
textElement.innerHTML = `<tspan x="0">${text}</tspan>`;
|
||||
|
||||
const correctedRatio = minmax(rn((pathLength / text.length) * 50), 40, 130);
|
||||
const correctedRatio = minmax(rn((pathLength / text.length) * 50), 50, 130);
|
||||
textElement.setAttribute("font-size", correctedRatio + "%");
|
||||
}
|
||||
}
|
||||
|
||||
// point offset to reduce label overlap with state borders
|
||||
function getOffsetWidth(cellsNumber) {
|
||||
if (cellsNumber < 80) return 0;
|
||||
if (cellsNumber < 140) return 5;
|
||||
if (cellsNumber < 200) return 15;
|
||||
if (cellsNumber < 300) return 20;
|
||||
if (cellsNumber < 500) return 25;
|
||||
return 30;
|
||||
}
|
||||
|
||||
// difference between two angles in range [0, 180]
|
||||
function getAnglesDif(angle1, angle2) {
|
||||
return 180 - Math.abs(Math.abs(angle1 - angle2) - 180);
|
||||
}
|
||||
|
||||
// score multiplier based on angle difference betwee left and right sides
|
||||
function getAngleModifier(angleDif) {
|
||||
if (angleDif === 0) return 1;
|
||||
if (angleDif <= 15) return 0.95;
|
||||
if (angleDif <= 30) return 0.9;
|
||||
if (angleDif <= 45) return 0.6;
|
||||
if (angleDif <= 60) return 0.3;
|
||||
if (angleDif <= 90) return 0.1;
|
||||
return 0; // >90
|
||||
if (cellsNumber < 40) return 0;
|
||||
if (cellsNumber < 200) return 5;
|
||||
return 10;
|
||||
}
|
||||
|
||||
function precalculateAngles(step) {
|
||||
|
|
@ -228,37 +148,135 @@ function drawStateLabels(list) {
|
|||
const RAD = Math.PI / 180;
|
||||
|
||||
for (let angle = 0; angle < 360; angle += step) {
|
||||
const x = Math.cos(angle * RAD);
|
||||
const y = Math.sin(angle * RAD);
|
||||
const angleDif = 90 - Math.abs((angle % 180) - 90);
|
||||
const modifier = 1 - angleDif / 120; // [0.25, 1]
|
||||
angles.push({angle, modifier, x, y});
|
||||
const dx = Math.cos(angle * RAD);
|
||||
const dy = Math.sin(angle * RAD);
|
||||
angles.push({angle, dx, dy});
|
||||
}
|
||||
|
||||
return angles;
|
||||
}
|
||||
|
||||
function raycast({stateId, x0, y0, dx, dy, maxLakeSize, offset}) {
|
||||
let ray = {length: 0, x: x0, y: y0};
|
||||
|
||||
for (let length = LENGTH_START; length < LENGTH_MAX; length += LENGTH_STEP) {
|
||||
const [x, y] = [x0 + length * dx, y0 + length * dy];
|
||||
// offset points are perpendicular to the ray
|
||||
const offset1 = [x + -dy * offset, y + dx * offset];
|
||||
const offset2 = [x + dy * offset, y + -dx * offset];
|
||||
|
||||
if (DEBUG.stateLabels) {
|
||||
drawPoint([x, y], {color: isInsideState(x, y) ? "blue" : "red", radius: 0.8});
|
||||
drawPoint(offset1, {color: isInsideState(...offset1) ? "blue" : "red", radius: 0.4});
|
||||
drawPoint(offset2, {color: isInsideState(...offset2) ? "blue" : "red", radius: 0.4});
|
||||
}
|
||||
|
||||
const inState = isInsideState(x, y) && isInsideState(...offset1) && isInsideState(...offset2);
|
||||
if (!inState) break;
|
||||
ray = {length, x, y};
|
||||
}
|
||||
|
||||
return ray;
|
||||
|
||||
function isInsideState(x, y) {
|
||||
if (x < 0 || x > graphWidth || y < 0 || y > graphHeight) return false;
|
||||
const cellId = findCell(x, y);
|
||||
|
||||
const feature = features[cells.f[cellId]];
|
||||
if (feature.type === "lake") return isInnerLake(feature) || isSmallLake(feature);
|
||||
|
||||
return stateIds[cellId] === stateId;
|
||||
}
|
||||
|
||||
function isInnerLake(feature) {
|
||||
return feature.shoreline.every(cellId => stateIds[cellId] === stateId);
|
||||
}
|
||||
|
||||
function isSmallLake(feature) {
|
||||
return feature.cells <= maxLakeSize;
|
||||
}
|
||||
}
|
||||
|
||||
function findBestRayPair(rays) {
|
||||
let bestPair = null;
|
||||
let bestScore = -Infinity;
|
||||
|
||||
for (let i = 0; i < rays.length; i++) {
|
||||
const score1 = rays[i].length * scoreRayAngle(rays[i].angle);
|
||||
|
||||
for (let j = i + 1; j < rays.length; j++) {
|
||||
const score2 = rays[j].length * scoreRayAngle(rays[j].angle);
|
||||
const pairScore = (score1 + score2) * scoreCurvature(rays[i].angle, rays[j].angle);
|
||||
|
||||
if (pairScore > bestScore) {
|
||||
bestScore = pairScore;
|
||||
bestPair = [rays[i], rays[j]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestPair;
|
||||
}
|
||||
|
||||
function scoreRayAngle(angle) {
|
||||
const normalizedAngle = Math.abs(angle % 180); // [0, 180]
|
||||
const horizontality = Math.abs(normalizedAngle - 90) / 90; // [0, 1]
|
||||
|
||||
if (horizontality === 1) return 1; // Best: horizontal
|
||||
if (horizontality >= 0.75) return 0.9; // Very good: slightly slanted
|
||||
if (horizontality >= 0.5) return 0.6; // Good: moderate slant
|
||||
if (horizontality >= 0.25) return 0.5; // Acceptable: more slanted
|
||||
if (horizontality >= 0.15) return 0.2; // Poor: almost vertical
|
||||
return 0.1; // Very poor: almost vertical
|
||||
}
|
||||
|
||||
function scoreCurvature(angle1, angle2) {
|
||||
const delta = getAngleDelta(angle1, angle2);
|
||||
const similarity = evaluateArc(angle1, angle2);
|
||||
|
||||
if (delta === 180) return 1; // straight line: best
|
||||
if (delta < 90) return 0; // acute: not allowed
|
||||
if (delta < 120) return 0.6 * similarity;
|
||||
if (delta < 140) return 0.7 * similarity;
|
||||
if (delta < 160) return 0.8 * similarity;
|
||||
|
||||
return similarity;
|
||||
}
|
||||
|
||||
function getAngleDelta(angle1, angle2) {
|
||||
let delta = Math.abs(angle1 - angle2) % 360;
|
||||
if (delta > 180) delta = 360 - delta; // [0, 180]
|
||||
return delta;
|
||||
}
|
||||
|
||||
// compute arc similarity towards x-axis
|
||||
function evaluateArc(angle1, angle2) {
|
||||
const proximity1 = Math.abs((angle1 % 180) - 90);
|
||||
const proximity2 = Math.abs((angle2 % 180) - 90);
|
||||
return 1 - Math.abs(proximity1 - proximity2) / 90;
|
||||
}
|
||||
|
||||
function getLinesAndRatio(mode, name, fullName, pathLength) {
|
||||
// short name
|
||||
if (mode === "short" || (mode === "auto" && pathLength <= name.length)) {
|
||||
const lines = splitInTwo(name);
|
||||
if (mode === "short") return getShortOneLine();
|
||||
if (pathLength > fullName.length * 2) return getFullOneLine();
|
||||
return getFullTwoLines();
|
||||
|
||||
function getShortOneLine() {
|
||||
const ratio = pathLength / name.length;
|
||||
return [[name], minmax(rn(ratio * 60), 50, 150)];
|
||||
}
|
||||
|
||||
function getFullOneLine() {
|
||||
const ratio = pathLength / fullName.length;
|
||||
return [[fullName], minmax(rn(ratio * 70), 70, 170)];
|
||||
}
|
||||
|
||||
function getFullTwoLines() {
|
||||
const lines = splitInTwo(fullName);
|
||||
const longestLineLength = d3.max(lines.map(({length}) => length));
|
||||
const ratio = pathLength / longestLineLength;
|
||||
return [lines, minmax(rn(ratio * 60), 50, 150)];
|
||||
return [lines, minmax(rn(ratio * 60), 70, 150)];
|
||||
}
|
||||
|
||||
// full name: one line
|
||||
if (pathLength > fullName.length * 2) {
|
||||
const lines = [fullName];
|
||||
const ratio = pathLength / lines[0].length;
|
||||
return [lines, minmax(rn(ratio * 70), 70, 170)];
|
||||
}
|
||||
|
||||
// full name: two lines
|
||||
const lines = splitInTwo(fullName);
|
||||
const longestLineLength = d3.max(lines.map(({length}) => length));
|
||||
const ratio = pathLength / longestLineLength;
|
||||
return [lines, minmax(rn(ratio * 60), 70, 150)];
|
||||
}
|
||||
|
||||
// check whether multi-lined label is mostly inside the state. If no, replace it with short name label
|
||||
|
|
|
|||
365
modules/resample.js
Normal file
365
modules/resample.js
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
"use strict";
|
||||
|
||||
window.Resample = (function () {
|
||||
/*
|
||||
generate new map based on an existing one (resampling parentMap)
|
||||
parentMap: {grid, pack, notes} from original map
|
||||
projection: f(Number, Number) -> [Number, Number]
|
||||
inverse: f(Number, Number) -> [Number, Number]
|
||||
scale: Number
|
||||
*/
|
||||
function process({projection, inverse, scale}) {
|
||||
const parentMap = {grid: deepCopy(grid), pack: deepCopy(pack), notes: deepCopy(notes)};
|
||||
const riversData = saveRiversData(pack.rivers);
|
||||
|
||||
grid = generateGrid();
|
||||
pack = {};
|
||||
notes = parentMap.notes;
|
||||
|
||||
resamplePrimaryGridData(parentMap, inverse, scale);
|
||||
|
||||
Features.markupGrid();
|
||||
addLakesInDeepDepressions();
|
||||
openNearSeaLakes();
|
||||
|
||||
OceanLayers();
|
||||
calculateMapCoordinates();
|
||||
calculateTemperatures();
|
||||
|
||||
reGraph();
|
||||
Features.markupPack();
|
||||
createDefaultRuler();
|
||||
|
||||
restoreCellData(parentMap, inverse, scale);
|
||||
restoreRivers(riversData, projection, scale);
|
||||
restoreCultures(parentMap, projection);
|
||||
restoreBurgs(parentMap, projection, scale);
|
||||
restoreStates(parentMap, projection);
|
||||
restoreRoutes(parentMap, projection);
|
||||
restoreReligions(parentMap, projection);
|
||||
restoreProvinces(parentMap);
|
||||
restoreFeatureDetails(parentMap, inverse);
|
||||
restoreMarkers(parentMap, projection);
|
||||
restoreZones(parentMap, projection, scale);
|
||||
|
||||
showStatistics();
|
||||
}
|
||||
|
||||
function resamplePrimaryGridData(parentMap, inverse, scale) {
|
||||
grid.cells.h = new Uint8Array(grid.points.length);
|
||||
grid.cells.temp = new Int8Array(grid.points.length);
|
||||
grid.cells.prec = new Uint8Array(grid.points.length);
|
||||
|
||||
grid.points.forEach(([x, y], newGridCell) => {
|
||||
const [parentX, parentY] = inverse(x, y);
|
||||
const parentPackCell = parentMap.pack.cells.q.find(parentX, parentY, Infinity)[2];
|
||||
const parentGridCell = parentMap.pack.cells.g[parentPackCell];
|
||||
|
||||
grid.cells.h[newGridCell] = parentMap.grid.cells.h[parentGridCell];
|
||||
grid.cells.temp[newGridCell] = parentMap.grid.cells.temp[parentGridCell];
|
||||
grid.cells.prec[newGridCell] = parentMap.grid.cells.prec[parentGridCell];
|
||||
});
|
||||
|
||||
if (scale >= 2) smoothHeightmap();
|
||||
}
|
||||
|
||||
function smoothHeightmap() {
|
||||
grid.cells.h.forEach((height, newGridCell) => {
|
||||
const heights = [height, ...grid.cells.c[newGridCell].map(c => grid.cells.h[c])];
|
||||
const meanHeight = d3.mean(heights);
|
||||
grid.cells.h[newGridCell] = isWater(grid, newGridCell) ? Math.min(meanHeight, 19) : Math.max(meanHeight, 20);
|
||||
});
|
||||
}
|
||||
|
||||
function restoreCellData(parentMap, inverse, scale) {
|
||||
pack.cells.biome = new Uint8Array(pack.cells.i.length);
|
||||
pack.cells.fl = new Uint16Array(pack.cells.i.length);
|
||||
pack.cells.s = new Int16Array(pack.cells.i.length);
|
||||
pack.cells.pop = new Float32Array(pack.cells.i.length);
|
||||
pack.cells.culture = new Uint16Array(pack.cells.i.length);
|
||||
pack.cells.state = new Uint16Array(pack.cells.i.length);
|
||||
pack.cells.burg = new Uint16Array(pack.cells.i.length);
|
||||
pack.cells.religion = new Uint16Array(pack.cells.i.length);
|
||||
pack.cells.province = new Uint16Array(pack.cells.i.length);
|
||||
|
||||
const parentPackCellGroups = groupCellsByType(parentMap.pack);
|
||||
const parentPackLandCellsQuadtree = d3.quadtree(parentPackCellGroups.land);
|
||||
|
||||
for (const newPackCell of pack.cells.i) {
|
||||
const [x, y] = inverse(...pack.cells.p[newPackCell]);
|
||||
if (isWater(pack, newPackCell)) continue;
|
||||
|
||||
const parentPackCell = parentPackLandCellsQuadtree.find(x, y, Infinity)[2];
|
||||
const parentCellArea = parentMap.pack.cells.area[parentPackCell];
|
||||
const areaRatio = pack.cells.area[newPackCell] / parentCellArea;
|
||||
const scaleRatio = areaRatio / scale;
|
||||
|
||||
pack.cells.biome[newPackCell] = parentMap.pack.cells.biome[parentPackCell];
|
||||
pack.cells.fl[newPackCell] = parentMap.pack.cells.fl[parentPackCell];
|
||||
pack.cells.s[newPackCell] = parentMap.pack.cells.s[parentPackCell] * scaleRatio;
|
||||
pack.cells.pop[newPackCell] = parentMap.pack.cells.pop[parentPackCell] * scaleRatio;
|
||||
pack.cells.culture[newPackCell] = parentMap.pack.cells.culture[parentPackCell];
|
||||
pack.cells.state[newPackCell] = parentMap.pack.cells.state[parentPackCell];
|
||||
pack.cells.religion[newPackCell] = parentMap.pack.cells.religion[parentPackCell];
|
||||
pack.cells.province[newPackCell] = parentMap.pack.cells.province[parentPackCell];
|
||||
}
|
||||
}
|
||||
|
||||
function saveRiversData(parentRivers) {
|
||||
return parentRivers.map(river => {
|
||||
const meanderedPoints = Rivers.addMeandering(river.cells, river.points);
|
||||
return {...river, meanderedPoints};
|
||||
});
|
||||
}
|
||||
|
||||
function restoreRivers(riversData, projection, scale) {
|
||||
pack.cells.r = new Uint16Array(pack.cells.i.length);
|
||||
pack.cells.conf = new Uint8Array(pack.cells.i.length);
|
||||
|
||||
pack.rivers = riversData
|
||||
.map(river => {
|
||||
let wasInMap = true;
|
||||
const points = [];
|
||||
|
||||
river.meanderedPoints.forEach(([parentX, parentY]) => {
|
||||
const [x, y] = projection(parentX, parentY);
|
||||
const inMap = isInMap(x, y);
|
||||
if (inMap || wasInMap) points.push([rn(x, 2), rn(y, 2)]);
|
||||
wasInMap = inMap;
|
||||
});
|
||||
if (points.length < 2) return null;
|
||||
|
||||
const cells = points.map(point => findCell(...point));
|
||||
cells.forEach(cellId => {
|
||||
if (pack.cells.r[cellId]) pack.cells.conf[cellId] = 1;
|
||||
pack.cells.r[cellId] = river.i;
|
||||
});
|
||||
|
||||
const widthFactor = river.widthFactor * scale;
|
||||
return {...river, cells, points, source: cells.at(0), mouth: cells.at(-2), widthFactor};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
pack.rivers.forEach(river => {
|
||||
river.basin = Rivers.getBasin(river.i);
|
||||
river.length = Rivers.getApproximateLength(river.points);
|
||||
});
|
||||
}
|
||||
|
||||
function restoreCultures(parentMap, projection) {
|
||||
const validCultures = new Set(pack.cells.culture);
|
||||
const culturePoles = getPolesOfInaccessibility(pack, cellId => pack.cells.culture[cellId]);
|
||||
pack.cultures = parentMap.pack.cultures.map(culture => {
|
||||
if (!culture.i || culture.removed) return culture;
|
||||
if (!validCultures.has(culture.i)) return {...culture, removed: true, lock: false};
|
||||
|
||||
const [xp, yp] = projection(...parentMap.pack.cells.p[culture.center]);
|
||||
const [x, y] = [rn(xp, 2), rn(yp, 2)];
|
||||
const centerCoords = isInMap(x, y) ? [x, y] : culturePoles[culture.i];
|
||||
const center = findCell(...centerCoords);
|
||||
return {...culture, center};
|
||||
});
|
||||
}
|
||||
|
||||
function restoreBurgs(parentMap, projection, scale) {
|
||||
const packLandCellsQuadtree = d3.quadtree(groupCellsByType(pack).land);
|
||||
const findLandCell = (x, y) => packLandCellsQuadtree.find(x, y, Infinity)?.[2];
|
||||
|
||||
pack.burgs = parentMap.pack.burgs.map(burg => {
|
||||
if (!burg.i || burg.removed) return burg;
|
||||
burg.population *= scale; // adjust for populationRate change
|
||||
|
||||
const [xp, yp] = projection(burg.x, burg.y);
|
||||
if (!isInMap(xp, yp)) return {...burg, removed: true, lock: false};
|
||||
|
||||
const closestCell = findCell(xp, yp);
|
||||
const cell = isWater(pack, closestCell) ? findLandCell(xp, yp) : closestCell;
|
||||
|
||||
if (pack.cells.burg[cell]) {
|
||||
WARN && console.warn(`Cell ${cell} already has a burg. Removing burg ${burg.name} (${burg.i})`);
|
||||
return {...burg, removed: true, lock: false};
|
||||
}
|
||||
|
||||
pack.cells.burg[cell] = burg.i;
|
||||
const [x, y] = getBurgCoordinates(burg, closestCell, cell, xp, yp);
|
||||
return {...burg, cell, x, y};
|
||||
});
|
||||
|
||||
function getBurgCoordinates(burg, closestCell, cell, xp, yp) {
|
||||
const haven = pack.cells.haven[cell];
|
||||
if (burg.port && haven) return BurgsAndStates.getCloseToEdgePoint(cell, haven);
|
||||
|
||||
if (closestCell !== cell) return pack.cells.p[cell];
|
||||
return [rn(xp, 2), rn(yp, 2)];
|
||||
}
|
||||
}
|
||||
|
||||
function restoreStates(parentMap, projection) {
|
||||
const validStates = new Set(pack.cells.state);
|
||||
pack.states = parentMap.pack.states.map(state => {
|
||||
if (!state.i || state.removed) return state;
|
||||
if (validStates.has(state.i)) return state;
|
||||
return {...state, removed: true, lock: false};
|
||||
});
|
||||
|
||||
BurgsAndStates.getPoles();
|
||||
const regimentCellsMap = {};
|
||||
const VERTICAL_GAP = 8;
|
||||
|
||||
pack.states = pack.states.map(state => {
|
||||
if (!state.i || state.removed) return state;
|
||||
|
||||
const capital = pack.burgs[state.capital];
|
||||
state.center = !capital || capital.removed ? findCell(...state.pole) : capital.cell;
|
||||
|
||||
const military = state.military.map(regiment => {
|
||||
const cellCoords = projection(...parentMap.pack.cells.p[regiment.cell]);
|
||||
const cell = isInMap(...cellCoords) ? findCell(...cellCoords) : state.center;
|
||||
|
||||
const [xPos, yPos] = projection(regiment.x, regiment.y);
|
||||
const [xBase, yBase] = projection(regiment.bx, regiment.by);
|
||||
const [xCell, yCell] = pack.cells.p[cell];
|
||||
|
||||
const regsOnCell = regimentCellsMap[cell] || 0;
|
||||
regimentCellsMap[cell] = regsOnCell + 1;
|
||||
|
||||
const name =
|
||||
isInMap(xPos, yPos) || regiment.name.includes("[relocated]") ? regiment.name : `[relocated] ${regiment.name}`;
|
||||
|
||||
const pos = isInMap(xPos, yPos)
|
||||
? {x: rn(xPos, 2), y: rn(yPos, 2)}
|
||||
: {x: xCell, y: yCell + regsOnCell * VERTICAL_GAP};
|
||||
|
||||
const base = isInMap(xBase, yBase) ? {bx: rn(xBase, 2), by: rn(yBase, 2)} : {bx: xCell, by: yCell};
|
||||
|
||||
return {...regiment, cell, name, ...base, ...pos};
|
||||
});
|
||||
|
||||
const neighbors = state.neighbors.filter(stateId => validStates.has(stateId));
|
||||
return {...state, neighbors, military};
|
||||
});
|
||||
}
|
||||
|
||||
function restoreRoutes(parentMap, projection) {
|
||||
pack.routes = parentMap.pack.routes
|
||||
.map(route => {
|
||||
let wasInMap = true;
|
||||
const points = [];
|
||||
|
||||
route.points.forEach(([parentX, parentY]) => {
|
||||
const [x, y] = projection(parentX, parentY);
|
||||
const inMap = isInMap(x, y);
|
||||
if (inMap || wasInMap) points.push([rn(x, 2), rn(y, 2)]);
|
||||
wasInMap = inMap;
|
||||
});
|
||||
if (points.length < 2) return null;
|
||||
|
||||
const firstCell = points[0][2];
|
||||
const feature = pack.cells.f[firstCell];
|
||||
return {...route, feature, points};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
pack.cells.routes = Routes.buildLinks(pack.routes);
|
||||
}
|
||||
|
||||
function restoreReligions(parentMap, projection) {
|
||||
const validReligions = new Set(pack.cells.religion);
|
||||
const religionPoles = getPolesOfInaccessibility(pack, cellId => pack.cells.religion[cellId]);
|
||||
|
||||
pack.religions = parentMap.pack.religions.map(religion => {
|
||||
if (!religion.i || religion.removed) return religion;
|
||||
if (!validReligions.has(religion.i)) return {...religion, removed: true, lock: false};
|
||||
|
||||
const [xp, yp] = projection(...parentMap.pack.cells.p[religion.center]);
|
||||
const [x, y] = [rn(xp, 2), rn(yp, 2)];
|
||||
const centerCoords = isInMap(x, y) ? [x, y] : religionPoles[religion.i];
|
||||
const center = findCell(...centerCoords);
|
||||
return {...religion, center};
|
||||
});
|
||||
}
|
||||
|
||||
function restoreProvinces(parentMap) {
|
||||
const validProvinces = new Set(pack.cells.province);
|
||||
pack.provinces = parentMap.pack.provinces.map(province => {
|
||||
if (!province.i || province.removed) return province;
|
||||
if (!validProvinces.has(province.i)) return {...province, removed: true, lock: false};
|
||||
|
||||
return province;
|
||||
});
|
||||
|
||||
Provinces.getPoles();
|
||||
|
||||
pack.provinces.forEach(province => {
|
||||
if (!province.i || province.removed) return;
|
||||
const capital = pack.burgs[province.burg];
|
||||
province.center = !capital?.removed ? capital.cell : findCell(...province.pole);
|
||||
});
|
||||
}
|
||||
|
||||
function restoreMarkers(parentMap, projection) {
|
||||
pack.markers = parentMap.pack.markers;
|
||||
pack.markers.forEach(marker => {
|
||||
const [x, y] = projection(marker.x, marker.y);
|
||||
if (!isInMap(x, y)) Markers.deleteMarker(marker.i);
|
||||
|
||||
const cell = findCell(x, y);
|
||||
marker.x = rn(x, 2);
|
||||
marker.y = rn(y, 2);
|
||||
marker.cell = cell;
|
||||
});
|
||||
}
|
||||
|
||||
function restoreZones(parentMap, projection, scale) {
|
||||
const getSearchRadius = cellId => Math.sqrt(parentMap.pack.cells.area[cellId] / Math.PI) * scale;
|
||||
|
||||
pack.zones = parentMap.pack.zones.map(zone => {
|
||||
const cells = zone.cells
|
||||
.map(cellId => {
|
||||
const [x, y] = projection(...parentMap.pack.cells.p[cellId]);
|
||||
if (!isInMap(x, y)) return null;
|
||||
return findAll(x, y, getSearchRadius(cellId));
|
||||
})
|
||||
.filter(Boolean)
|
||||
.flat();
|
||||
|
||||
return {...zone, cells: unique(cells)};
|
||||
});
|
||||
}
|
||||
|
||||
function restoreFeatureDetails(parentMap, inverse) {
|
||||
pack.features.forEach(feature => {
|
||||
if (!feature) return;
|
||||
const [x, y] = pack.cells.p[feature.firstCell];
|
||||
const [parentX, parentY] = inverse(x, y);
|
||||
const parentCell = parentMap.pack.cells.q.find(parentX, parentY, Infinity)[2];
|
||||
if (parentCell === undefined) return;
|
||||
const parentFeature = parentMap.pack.features[parentMap.pack.cells.f[parentCell]];
|
||||
|
||||
if (parentFeature.group) feature.group = parentFeature.group;
|
||||
if (parentFeature.name) feature.name = parentFeature.name;
|
||||
if (parentFeature.height) feature.height = parentFeature.height;
|
||||
});
|
||||
}
|
||||
|
||||
function groupCellsByType(graph) {
|
||||
return graph.cells.p.reduce(
|
||||
(acc, [x, y], cellId) => {
|
||||
const group = isWater(graph, cellId) ? "water" : "land";
|
||||
acc[group].push([x, y, cellId]);
|
||||
return acc;
|
||||
},
|
||||
{land: [], water: []}
|
||||
);
|
||||
}
|
||||
|
||||
function isWater(graph, cellId) {
|
||||
return graph.cells.h[cellId] < 20;
|
||||
}
|
||||
|
||||
function isInMap(x, y) {
|
||||
return x >= 0 && x <= graphWidth && y >= 0 && y <= graphHeight;
|
||||
}
|
||||
|
||||
return {process};
|
||||
})();
|
||||
|
|
@ -190,7 +190,15 @@ window.Rivers = (function () {
|
|||
const meanderedPoints = addMeandering(riverCells);
|
||||
const discharge = cells.fl[mouth]; // m3 in second
|
||||
const length = getApproximateLength(meanderedPoints);
|
||||
const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, 0));
|
||||
const sourceWidth = getSourceWidth(cells.fl[source]);
|
||||
const width = getWidth(
|
||||
getOffset({
|
||||
flux: discharge,
|
||||
pointIndex: meanderedPoints.length,
|
||||
widthFactor,
|
||||
sourceWidth
|
||||
})
|
||||
);
|
||||
|
||||
pack.rivers.push({
|
||||
i: riverId,
|
||||
|
|
@ -200,7 +208,7 @@ window.Rivers = (function () {
|
|||
length,
|
||||
width,
|
||||
widthFactor,
|
||||
sourceWidth: 0,
|
||||
sourceWidth,
|
||||
parent,
|
||||
cells: riverCells
|
||||
});
|
||||
|
|
@ -306,59 +314,49 @@ window.Rivers = (function () {
|
|||
|
||||
// add points at 1/3 and 2/3 of a line between adjacents river cells
|
||||
const addMeandering = function (riverCells, riverPoints = null, meandering = 0.5) {
|
||||
const {fl, conf, h} = pack.cells;
|
||||
const {fl, h} = pack.cells;
|
||||
const meandered = [];
|
||||
const lastStep = riverCells.length - 1;
|
||||
const points = getRiverPoints(riverCells, riverPoints);
|
||||
let step = h[riverCells[0]] < 20 ? 1 : 10;
|
||||
|
||||
let fluxPrev = 0;
|
||||
const getFlux = (step, flux) => (step === lastStep ? fluxPrev : flux);
|
||||
|
||||
for (let i = 0; i <= lastStep; i++, step++) {
|
||||
const cell = riverCells[i];
|
||||
const isLastCell = i === lastStep;
|
||||
|
||||
const [x1, y1] = points[i];
|
||||
const flux1 = getFlux(i, fl[cell]);
|
||||
fluxPrev = flux1;
|
||||
|
||||
meandered.push([x1, y1, flux1]);
|
||||
meandered.push([x1, y1, fl[cell]]);
|
||||
if (isLastCell) break;
|
||||
|
||||
const nextCell = riverCells[i + 1];
|
||||
const [x2, y2] = points[i + 1];
|
||||
|
||||
if (nextCell === -1) {
|
||||
meandered.push([x2, y2, fluxPrev]);
|
||||
meandered.push([x2, y2, fl[cell]]);
|
||||
break;
|
||||
}
|
||||
|
||||
const dist2 = (x2 - x1) ** 2 + (y2 - y1) ** 2; // square distance between cells
|
||||
if (dist2 <= 25 && riverCells.length >= 6) continue;
|
||||
|
||||
const flux2 = getFlux(i + 1, fl[nextCell]);
|
||||
const keepInitialFlux = conf[nextCell] || flux1 === flux2;
|
||||
|
||||
const meander = meandering + 1 / step + Math.max(meandering - step / 100, 0);
|
||||
const angle = Math.atan2(y2 - y1, x2 - x1);
|
||||
const sinMeander = Math.sin(angle) * meander;
|
||||
const cosMeander = Math.cos(angle) * meander;
|
||||
|
||||
if (step < 10 && (dist2 > 64 || (dist2 > 36 && riverCells.length < 5))) {
|
||||
if (step < 20 && (dist2 > 64 || (dist2 > 36 && riverCells.length < 5))) {
|
||||
// if dist2 is big or river is small add extra points at 1/3 and 2/3 of segment
|
||||
const p1x = (x1 * 2 + x2) / 3 + -sinMeander;
|
||||
const p1y = (y1 * 2 + y2) / 3 + cosMeander;
|
||||
const p2x = (x1 + x2 * 2) / 3 + sinMeander / 2;
|
||||
const p2y = (y1 + y2 * 2) / 3 - cosMeander / 2;
|
||||
const [p1fl, p2fl] = keepInitialFlux ? [flux1, flux1] : [(flux1 * 2 + flux2) / 3, (flux1 + flux2 * 2) / 3];
|
||||
meandered.push([p1x, p1y, p1fl], [p2x, p2y, p2fl]);
|
||||
meandered.push([p1x, p1y, 0], [p2x, p2y, 0]);
|
||||
} else if (dist2 > 25 || riverCells.length < 6) {
|
||||
// if dist is medium or river is small add 1 extra middlepoint
|
||||
const p1x = (x1 + x2) / 2 + -sinMeander;
|
||||
const p1y = (y1 + y2) / 2 + cosMeander;
|
||||
const p1fl = keepInitialFlux ? flux1 : (flux1 + flux2) / 2;
|
||||
meandered.push([p1x, p1y, p1fl]);
|
||||
meandered.push([p1x, p1y, 0]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -385,29 +383,36 @@ window.Rivers = (function () {
|
|||
};
|
||||
|
||||
const FLUX_FACTOR = 500;
|
||||
const MAX_FLUX_WIDTH = 2;
|
||||
const MAX_FLUX_WIDTH = 1;
|
||||
const LENGTH_FACTOR = 200;
|
||||
const STEP_WIDTH = 1 / LENGTH_FACTOR;
|
||||
const LENGTH_STEP_WIDTH = 1 / LENGTH_FACTOR;
|
||||
const LENGTH_PROGRESSION = [1, 1, 2, 3, 5, 8, 13, 21, 34].map(n => n / LENGTH_FACTOR);
|
||||
const MAX_PROGRESSION = last(LENGTH_PROGRESSION);
|
||||
|
||||
const getOffset = (flux, pointNumber, widthFactor, startingWidth = 0) => {
|
||||
const fluxWidth = Math.min(flux ** 0.9 / FLUX_FACTOR, MAX_FLUX_WIDTH);
|
||||
const lengthWidth = pointNumber * STEP_WIDTH + (LENGTH_PROGRESSION[pointNumber] || MAX_PROGRESSION);
|
||||
const getOffset = ({flux, pointIndex, widthFactor, startingWidth}) => {
|
||||
if (pointIndex === 0) return startingWidth;
|
||||
|
||||
const fluxWidth = Math.min(flux ** 0.7 / FLUX_FACTOR, MAX_FLUX_WIDTH);
|
||||
const lengthWidth = pointIndex * LENGTH_STEP_WIDTH + (LENGTH_PROGRESSION[pointIndex] || MAX_PROGRESSION);
|
||||
return widthFactor * (lengthWidth + fluxWidth) + startingWidth;
|
||||
};
|
||||
|
||||
const getSourceWidth = flux => rn(Math.min(flux ** 0.9 / FLUX_FACTOR, MAX_FLUX_WIDTH), 2);
|
||||
|
||||
// build polygon from a list of points and calculated offset (width)
|
||||
const getRiverPath = function (points, widthFactor, startingWidth = 0) {
|
||||
const getRiverPath = (points, widthFactor, startingWidth) => {
|
||||
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
|
||||
const riverPointsLeft = [];
|
||||
const riverPointsRight = [];
|
||||
let flux = 0;
|
||||
|
||||
for (let p = 0; p < points.length; p++) {
|
||||
const [x0, y0] = points[p - 1] || points[p];
|
||||
const [x1, y1, flux] = points[p];
|
||||
const [x2, y2] = points[p + 1] || points[p];
|
||||
for (let pointIndex = 0; pointIndex < points.length; pointIndex++) {
|
||||
const [x0, y0] = points[pointIndex - 1] || points[pointIndex];
|
||||
const [x1, y1, pointFlux] = points[pointIndex];
|
||||
const [x2, y2] = points[pointIndex + 1] || points[pointIndex];
|
||||
if (pointFlux > flux) flux = pointFlux;
|
||||
|
||||
const offset = getOffset(flux, p, widthFactor, startingWidth);
|
||||
const offset = getOffset({flux, pointIndex, widthFactor, startingWidth});
|
||||
const angle = Math.atan2(y0 - y2, x0 - x2);
|
||||
const sinOffset = Math.sin(angle) * offset;
|
||||
const cosOffset = Math.cos(angle) * offset;
|
||||
|
|
@ -507,6 +512,7 @@ window.Rivers = (function () {
|
|||
getBasin,
|
||||
getWidth,
|
||||
getOffset,
|
||||
getSourceWidth,
|
||||
getApproximateLength,
|
||||
getRiverPoints,
|
||||
remove,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,15 @@
|
|||
const ROUTES_SHARP_ANGLE = 135;
|
||||
const ROUTES_VERY_SHARP_ANGLE = 115;
|
||||
|
||||
const MIN_PASSABLE_SEA_TEMP = -4;
|
||||
const ROUTE_TYPE_MODIFIERS = {
|
||||
"-1": 1, // coastline
|
||||
"-2": 1.8, // sea
|
||||
"-3": 4, // open sea
|
||||
"-4": 6, // ocean
|
||||
default: 8 // far ocean
|
||||
};
|
||||
|
||||
window.Routes = (function () {
|
||||
function generate(lockedRoutes = []) {
|
||||
const {capitalsByFeature, burgsByFeature, portsByFeature} = sortBurgsByFeature(pack.burgs);
|
||||
|
|
@ -118,10 +127,9 @@ window.Routes = (function () {
|
|||
}
|
||||
|
||||
function findPathSegments({isWater, connections, start, exit}) {
|
||||
const from = findPath(isWater, start, exit, connections);
|
||||
if (!from) return [];
|
||||
|
||||
const pathCells = restorePath(start, exit, from);
|
||||
const getCost = createCostEvaluator({isWater, connections});
|
||||
const pathCells = findPath(start, current => current === exit, getCost);
|
||||
if (!pathCells) return [];
|
||||
const segments = getRouteSegments(pathCells, connections);
|
||||
return segments;
|
||||
}
|
||||
|
|
@ -172,29 +180,61 @@ window.Routes = (function () {
|
|||
|
||||
return routesMerged > 1 ? mergeRoutes(routes) : routes;
|
||||
}
|
||||
}
|
||||
|
||||
function buildLinks(routes) {
|
||||
const links = {};
|
||||
function createCostEvaluator({isWater, connections}) {
|
||||
return isWater ? getWaterPathCost : getLandPathCost;
|
||||
|
||||
for (const {points, i: routeId} of routes) {
|
||||
const cells = points.map(p => p[2]);
|
||||
function getLandPathCost(current, next) {
|
||||
if (pack.cells.h[next] < 20) return Infinity; // ignore water cells
|
||||
|
||||
for (let i = 0; i < cells.length - 1; i++) {
|
||||
const cellId = cells[i];
|
||||
const nextCellId = cells[i + 1];
|
||||
const habitability = biomesData.habitability[pack.cells.biome[next]];
|
||||
if (!habitability) return Infinity; // inhabitable cells are not passable (e.g. glacier)
|
||||
|
||||
if (cellId !== nextCellId) {
|
||||
if (!links[cellId]) links[cellId] = {};
|
||||
links[cellId][nextCellId] = routeId;
|
||||
const distanceCost = dist2(pack.cells.p[current], pack.cells.p[next]);
|
||||
const habitabilityModifier = 1 + Math.max(100 - habitability, 0) / 1000; // [1, 1.1];
|
||||
const heightModifier = 1 + Math.max(pack.cells.h[next] - 25, 25) / 25; // [1, 3];
|
||||
const connectionModifier = connections.has(`${current}-${next}`) ? 0.5 : 1;
|
||||
const burgModifier = pack.cells.burg[next] ? 1 : 3;
|
||||
|
||||
if (!links[nextCellId]) links[nextCellId] = {};
|
||||
links[nextCellId][cellId] = routeId;
|
||||
}
|
||||
const pathCost = distanceCost * habitabilityModifier * heightModifier * connectionModifier * burgModifier;
|
||||
return pathCost;
|
||||
}
|
||||
|
||||
function getWaterPathCost(current, next) {
|
||||
if (pack.cells.h[next] >= 20) return Infinity; // ignore land cells
|
||||
if (grid.cells.temp[pack.cells.g[next]] < MIN_PASSABLE_SEA_TEMP) return Infinity; // ignore too cold cells
|
||||
|
||||
const distanceCost = dist2(pack.cells.p[current], pack.cells.p[next]);
|
||||
const typeModifier = ROUTE_TYPE_MODIFIERS[pack.cells.t[next]] || ROUTE_TYPE_MODIFIERS.default;
|
||||
const connectionModifier = connections.has(`${current}-${next}`) ? 0.5 : 1;
|
||||
|
||||
const pathCost = distanceCost * typeModifier * connectionModifier;
|
||||
return pathCost;
|
||||
}
|
||||
}
|
||||
|
||||
function buildLinks(routes) {
|
||||
const links = {};
|
||||
|
||||
for (const {points, i: routeId} of routes) {
|
||||
const cells = points.map(p => p[2]);
|
||||
|
||||
for (let i = 0; i < cells.length - 1; i++) {
|
||||
const cellId = cells[i];
|
||||
const nextCellId = cells[i + 1];
|
||||
|
||||
if (cellId !== nextCellId) {
|
||||
if (!links[cellId]) links[cellId] = {};
|
||||
links[cellId][nextCellId] = routeId;
|
||||
|
||||
if (!links[nextCellId]) links[nextCellId] = {};
|
||||
links[nextCellId][cellId] = routeId;
|
||||
}
|
||||
}
|
||||
|
||||
return links;
|
||||
}
|
||||
|
||||
return links;
|
||||
}
|
||||
|
||||
function preparePointsArray() {
|
||||
|
|
@ -249,109 +289,6 @@ window.Routes = (function () {
|
|||
return data; // [[x, y, cell], [x, y, cell]];
|
||||
}
|
||||
|
||||
const MIN_PASSABLE_SEA_TEMP = -4;
|
||||
const TYPE_MODIFIERS = {
|
||||
"-1": 1, // coastline
|
||||
"-2": 1.8, // sea
|
||||
"-3": 4, // open sea
|
||||
"-4": 6, // ocean
|
||||
default: 8 // far ocean
|
||||
};
|
||||
|
||||
function findPath(isWater, start, exit, connections) {
|
||||
const {temp} = grid.cells;
|
||||
const {cells} = pack;
|
||||
|
||||
const from = [];
|
||||
const cost = [];
|
||||
const queue = new FlatQueue();
|
||||
queue.push(start, 0);
|
||||
|
||||
return isWater ? findWaterPath() : findLandPath();
|
||||
|
||||
function findLandPath() {
|
||||
while (queue.length) {
|
||||
const priority = queue.peekValue();
|
||||
const next = queue.pop();
|
||||
|
||||
for (const neibCellId of cells.c[next]) {
|
||||
if (neibCellId === exit) {
|
||||
from[neibCellId] = next;
|
||||
return from;
|
||||
}
|
||||
|
||||
if (cells.h[neibCellId] < 20) continue; // ignore water cells
|
||||
const habitability = biomesData.habitability[cells.biome[neibCellId]];
|
||||
if (!habitability) continue; // inhabitable cells are not passable (eg. lava, glacier)
|
||||
|
||||
const distanceCost = dist2(cells.p[next], cells.p[neibCellId]);
|
||||
const habitabilityModifier = 1 + Math.max(100 - habitability, 0) / 1000; // [1, 1.1];
|
||||
const heightModifier = 1 + Math.max(cells.h[neibCellId] - 25, 25) / 25; // [1, 3];
|
||||
const connectionModifier = connections.has(`${next}-${neibCellId}`) ? 1 : 2;
|
||||
const burgModifier = cells.burg[neibCellId] ? 1 : 3;
|
||||
|
||||
const cellsCost = distanceCost * habitabilityModifier * heightModifier * connectionModifier * burgModifier;
|
||||
const totalCost = priority + cellsCost;
|
||||
|
||||
if (totalCost >= cost[neibCellId]) continue;
|
||||
from[neibCellId] = next;
|
||||
cost[neibCellId] = totalCost;
|
||||
queue.push(neibCellId, totalCost);
|
||||
}
|
||||
}
|
||||
|
||||
return null; // path is not found
|
||||
}
|
||||
|
||||
function findWaterPath() {
|
||||
while (queue.length) {
|
||||
const priority = queue.peekValue();
|
||||
const next = queue.pop();
|
||||
|
||||
for (const neibCellId of cells.c[next]) {
|
||||
if (neibCellId === exit) {
|
||||
from[neibCellId] = next;
|
||||
return from;
|
||||
}
|
||||
|
||||
if (cells.h[neibCellId] >= 20) continue; // ignore land cells
|
||||
if (temp[cells.g[neibCellId]] < MIN_PASSABLE_SEA_TEMP) continue; // ignore too cold cells
|
||||
|
||||
const distanceCost = dist2(cells.p[next], cells.p[neibCellId]);
|
||||
const typeModifier = TYPE_MODIFIERS[cells.t[neibCellId]] || TYPE_MODIFIERS.default;
|
||||
const connectionModifier = connections.has(`${next}-${neibCellId}`) ? 1 : 2;
|
||||
|
||||
const cellsCost = distanceCost * typeModifier * connectionModifier;
|
||||
const totalCost = priority + cellsCost;
|
||||
|
||||
if (totalCost >= cost[neibCellId]) continue;
|
||||
from[neibCellId] = next;
|
||||
cost[neibCellId] = totalCost;
|
||||
queue.push(neibCellId, totalCost);
|
||||
}
|
||||
}
|
||||
|
||||
return null; // path is not found
|
||||
}
|
||||
}
|
||||
|
||||
function restorePath(start, end, from) {
|
||||
const cells = [];
|
||||
|
||||
let current = end;
|
||||
let prev = end;
|
||||
|
||||
while (current !== start) {
|
||||
cells.push(current);
|
||||
prev = from[current];
|
||||
current = prev;
|
||||
}
|
||||
|
||||
cells.push(current);
|
||||
|
||||
return cells;
|
||||
}
|
||||
|
||||
function getRouteSegments(pathCells, connections) {
|
||||
const segments = [];
|
||||
let segment = [];
|
||||
|
|
@ -422,21 +359,16 @@ window.Routes = (function () {
|
|||
|
||||
// connect cell with routes system by land
|
||||
function connect(cellId) {
|
||||
if (isConnected(cellId)) return;
|
||||
const getCost = createCostEvaluator({isWater: false, connections: new Map()});
|
||||
const pathCells = findPath(cellId, isConnected, getCost);
|
||||
if (!pathCells) return;
|
||||
|
||||
const {cells, routes} = pack;
|
||||
|
||||
const path = findConnectionPath(cellId);
|
||||
if (!path) return;
|
||||
|
||||
const pathCells = restorePath(...path);
|
||||
const pointsArray = preparePointsArray();
|
||||
const points = getPoints("trails", pathCells, pointsArray);
|
||||
const feature = cells.f[cellId];
|
||||
|
||||
const feature = pack.cells.f[cellId];
|
||||
const routeId = getNextId();
|
||||
const newRoute = {i: routeId, group: "trails", feature, points};
|
||||
routes.push(newRoute);
|
||||
pack.routes.push(newRoute);
|
||||
|
||||
for (let i = 0; i < pathCells.length; i++) {
|
||||
const cellId = pathCells[i];
|
||||
|
|
@ -446,43 +378,6 @@ window.Routes = (function () {
|
|||
|
||||
return newRoute;
|
||||
|
||||
function findConnectionPath(start) {
|
||||
const from = [];
|
||||
const cost = [];
|
||||
const queue = new FlatQueue();
|
||||
queue.push(start, 0);
|
||||
|
||||
while (queue.length) {
|
||||
const priority = queue.peekValue();
|
||||
const next = queue.pop();
|
||||
|
||||
for (const neibCellId of cells.c[next]) {
|
||||
if (isConnected(neibCellId)) {
|
||||
from[neibCellId] = next;
|
||||
return [start, neibCellId, from];
|
||||
}
|
||||
|
||||
if (cells.h[neibCellId] < 20) continue; // ignore water cells
|
||||
const habitability = biomesData.habitability[cells.biome[neibCellId]];
|
||||
if (!habitability) continue; // inhabitable cells are not passable (eg. lava, glacier)
|
||||
|
||||
const distanceCost = dist2(cells.p[next], cells.p[neibCellId]);
|
||||
const habitabilityModifier = 1 + Math.max(100 - habitability, 0) / 1000; // [1, 1.1];
|
||||
const heightModifier = 1 + Math.max(cells.h[neibCellId] - 25, 25) / 25; // [1, 3];
|
||||
|
||||
const cellsCost = distanceCost * habitabilityModifier * heightModifier;
|
||||
const totalCost = priority + cellsCost;
|
||||
|
||||
if (totalCost >= cost[neibCellId]) continue;
|
||||
from[neibCellId] = next;
|
||||
cost[neibCellId] = totalCost;
|
||||
queue.push(neibCellId, totalCost);
|
||||
}
|
||||
}
|
||||
|
||||
return null; // path is not found
|
||||
}
|
||||
|
||||
function addConnection(from, to, routeId) {
|
||||
const routes = pack.cells.routes;
|
||||
|
||||
|
|
@ -763,6 +658,7 @@ window.Routes = (function () {
|
|||
|
||||
return {
|
||||
generate,
|
||||
buildLinks,
|
||||
connect,
|
||||
isConnected,
|
||||
areConnected,
|
||||
|
|
|
|||
|
|
@ -50,7 +50,8 @@ window.States = (() => {
|
|||
const {cells, states, cultures, burgs} = pack;
|
||||
|
||||
cells.state = cells.state || new Uint16Array(cells.i.length);
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||
|
||||
const queue = new FlatQueue();
|
||||
const cost = [];
|
||||
|
||||
const globalGrowthRate = byId("growthRate").valueAsNumber || 1;
|
||||
|
|
@ -71,12 +72,13 @@ window.States = (() => {
|
|||
cells.state[capitalCell] = state.i;
|
||||
const cultureCenter = cultures[state.culture].center;
|
||||
const b = cells.biome[cultureCenter]; // state native biome
|
||||
queue.queue({e: state.center, p: 0, s: state.i, b});
|
||||
queue.push({e: state.center, p: 0, s: state.i, b}, 0);
|
||||
cost[state.center] = 1;
|
||||
}
|
||||
|
||||
while (queue.length) {
|
||||
const next = queue.dequeue();
|
||||
const next = queue.pop();
|
||||
|
||||
const {e, p, s, b} = next;
|
||||
const {type, culture} = states[s];
|
||||
|
||||
|
|
@ -99,7 +101,7 @@ window.States = (() => {
|
|||
if (!cost[e] || totalCost < cost[e]) {
|
||||
if (cells.h[e] >= 20) cells.state[e] = s; // assign state to cell
|
||||
cost[e] = totalCost;
|
||||
queue.queue({e, p: totalCost, s, b});
|
||||
queue.push({e, p: totalCost, s, b}, totalCost);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ window.Submap = (function () {
|
|||
seed = parentMap.seed;
|
||||
Math.random = aleaPRNG(seed);
|
||||
INFO && console.group("SubMap with seed: " + seed);
|
||||
DEBUG && console.info("Using Options:", options);
|
||||
|
||||
applyGraphSize();
|
||||
grid = generateGrid();
|
||||
|
|
@ -373,7 +372,7 @@ window.Submap = (function () {
|
|||
b.removed = true;
|
||||
return;
|
||||
}
|
||||
DEBUG && console.info(`Moving ${b.name} from ${cityCell} to ${newCell} near ${neighbor}.`);
|
||||
|
||||
[b.x, b.y] = b.port ? getCloseToEdgePoint(newCell, neighbor) : cells.p[newCell];
|
||||
if (b.port) b.port = cells.f[neighbor]; // copy feature number
|
||||
b.cell = newCell;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,115 @@
|
|||
"use strict";
|
||||
|
||||
const GPT_MODELS = ["gpt-4o-mini", "chatgpt-4o-latest", "gpt-4o", "gpt-4-turbo", "gpt-4", "gpt-3.5-turbo"];
|
||||
const PROVIDERS = {
|
||||
openai: {
|
||||
keyLink: "https://platform.openai.com/account/api-keys",
|
||||
generate: generateWithOpenAI
|
||||
},
|
||||
anthropic: {
|
||||
keyLink: "https://console.anthropic.com/account/keys",
|
||||
generate: generateWithAnthropic
|
||||
}
|
||||
};
|
||||
|
||||
const DEFAULT_MODEL = "gpt-4o-mini";
|
||||
|
||||
const MODELS = {
|
||||
"gpt-4o-mini": "openai",
|
||||
"chatgpt-4o-latest": "openai",
|
||||
"gpt-4o": "openai",
|
||||
"gpt-4-turbo": "openai",
|
||||
"o1-preview": "openai",
|
||||
"o1-mini": "openai",
|
||||
"claude-3-5-haiku-latest": "anthropic",
|
||||
"claude-3-5-sonnet-latest": "anthropic",
|
||||
"claude-3-opus-latest": "anthropic"
|
||||
};
|
||||
|
||||
const SYSTEM_MESSAGE = "I'm working on my fantasy map.";
|
||||
|
||||
function geneateWithAi(defaultPrompt, onApply) {
|
||||
async function generateWithOpenAI({key, model, prompt, temperature, onContent}) {
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${key}`
|
||||
};
|
||||
|
||||
const messages = [
|
||||
{role: "system", content: SYSTEM_MESSAGE},
|
||||
{role: "user", content: prompt}
|
||||
];
|
||||
|
||||
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({model, messages, temperature, stream: true})
|
||||
});
|
||||
|
||||
const getContent = json => {
|
||||
const content = json.choices?.[0]?.delta?.content;
|
||||
if (content) onContent(content);
|
||||
};
|
||||
|
||||
await handleStream(response, getContent);
|
||||
}
|
||||
|
||||
async function generateWithAnthropic({key, model, prompt, temperature, onContent}) {
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": key,
|
||||
"anthropic-version": "2023-06-01",
|
||||
"anthropic-dangerous-direct-browser-access": "true"
|
||||
};
|
||||
|
||||
const messages = [{role: "user", content: prompt}];
|
||||
|
||||
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({model, system: SYSTEM_MESSAGE, messages, temperature, max_tokens: 4096, stream: true})
|
||||
});
|
||||
|
||||
const getContent = json => {
|
||||
const content = json.delta?.text;
|
||||
if (content) onContent(content);
|
||||
};
|
||||
|
||||
await handleStream(response, getContent);
|
||||
}
|
||||
|
||||
async function handleStream(response, getContent) {
|
||||
if (!response.ok) {
|
||||
const json = await response.json();
|
||||
throw new Error(json?.error?.message || "Failed to generate");
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
let buffer = "";
|
||||
|
||||
while (true) {
|
||||
const {done, value} = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, {stream: true});
|
||||
const lines = buffer.split("\n");
|
||||
|
||||
for (let i = 0; i < lines.length - 1; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (line.startsWith("data: ") && line !== "data: [DONE]") {
|
||||
try {
|
||||
const json = JSON.parse(line.slice(6));
|
||||
getContent(json);
|
||||
} catch (jsonError) {
|
||||
ERROR && console.error(`Failed to parse JSON:`, jsonError, `Line: ${line}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buffer = lines.at(-1);
|
||||
}
|
||||
}
|
||||
|
||||
function generateWithAi(defaultPrompt, onApply) {
|
||||
updateValues();
|
||||
|
||||
$("#aiGenerator").dialog({
|
||||
|
|
@ -26,86 +132,56 @@ function geneateWithAi(defaultPrompt, onApply) {
|
|||
}
|
||||
});
|
||||
|
||||
if (modules.geneateWithAi) return;
|
||||
modules.geneateWithAi = true;
|
||||
if (modules.generateWithAi) return;
|
||||
modules.generateWithAi = true;
|
||||
|
||||
byId("aiGeneratorKeyHelp").on("click", function (e) {
|
||||
const model = byId("aiGeneratorModel").value;
|
||||
const provider = MODELS[model];
|
||||
openURL(PROVIDERS[provider].keyLink);
|
||||
});
|
||||
|
||||
function updateValues() {
|
||||
byId("aiGeneratorResult").value = "";
|
||||
byId("aiGeneratorPrompt").value = defaultPrompt;
|
||||
byId("aiGeneratorKey").value = localStorage.getItem("fmg-ai-kl") || "";
|
||||
byId("aiGeneratorTemperature").value = localStorage.getItem("fmg-ai-temperature") || "1";
|
||||
|
||||
const select = byId("aiGeneratorModel");
|
||||
select.options.length = 0;
|
||||
GPT_MODELS.forEach(model => select.options.add(new Option(model, model)));
|
||||
select.value = localStorage.getItem("fmg-ai-model") || GPT_MODELS[0];
|
||||
Object.keys(MODELS).forEach(model => select.options.add(new Option(model, model)));
|
||||
select.value = localStorage.getItem("fmg-ai-model");
|
||||
if (!select.value || !MODELS[select.value]) select.value = DEFAULT_MODEL;
|
||||
|
||||
const provider = MODELS[select.value];
|
||||
byId("aiGeneratorKey").value = localStorage.getItem(`fmg-ai-kl-${provider}`) || "";
|
||||
}
|
||||
|
||||
async function generate(button) {
|
||||
const key = byId("aiGeneratorKey").value;
|
||||
if (!key) return tip("Please enter an OpenAI API key", true, "error", 4000);
|
||||
localStorage.setItem("fmg-ai-kl", key);
|
||||
if (!key) return tip("Please enter an API key", true, "error", 4000);
|
||||
|
||||
const model = byId("aiGeneratorModel").value;
|
||||
if (!model) return tip("Please select a model", true, "error", 4000);
|
||||
localStorage.setItem("fmg-ai-model", model);
|
||||
|
||||
const provider = MODELS[model];
|
||||
localStorage.setItem(`fmg-ai-kl-${provider}`, key);
|
||||
|
||||
const prompt = byId("aiGeneratorPrompt").value;
|
||||
if (!prompt) return tip("Please enter a prompt", true, "error", 4000);
|
||||
|
||||
const temperature = byId("aiGeneratorTemperature").valueAsNumber;
|
||||
if (isNaN(temperature)) return tip("Temperature must be a number", true, "error", 4000);
|
||||
localStorage.setItem("fmg-ai-temperature", temperature);
|
||||
|
||||
try {
|
||||
button.disabled = true;
|
||||
const resultArea = byId("aiGeneratorResult");
|
||||
resultArea.value = "";
|
||||
resultArea.disabled = true;
|
||||
resultArea.value = "";
|
||||
const onContent = content => (resultArea.value += content);
|
||||
|
||||
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${key}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model,
|
||||
messages: [
|
||||
{role: "system", content: SYSTEM_MESSAGE},
|
||||
{role: "user", content: prompt}
|
||||
],
|
||||
temperature: 1.2,
|
||||
stream: true // Enable streaming
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const json = await response.json();
|
||||
throw new Error(json?.error?.message || "Failed to generate");
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
let buffer = "";
|
||||
|
||||
while (true) {
|
||||
const {done, value} = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, {stream: true});
|
||||
const lines = buffer.split("\n");
|
||||
|
||||
for (let i = 0; i < lines.length - 1; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (line.startsWith("data: ") && line !== "data: [DONE]") {
|
||||
try {
|
||||
const jsonData = JSON.parse(line.slice(6));
|
||||
const content = jsonData.choices[0].delta.content;
|
||||
if (content) resultArea.value += content;
|
||||
} catch (jsonError) {
|
||||
console.warn("Failed to parse JSON:", jsonError, "Line:", line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buffer = lines[lines.length - 1];
|
||||
}
|
||||
await PROVIDERS[provider].generate({key, model, prompt, temperature, onContent});
|
||||
} catch (error) {
|
||||
return tip(error.message, true, "error", 4000);
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -1066,7 +1066,7 @@ async function editStates() {
|
|||
|
||||
async function editCultures() {
|
||||
if (customization) return;
|
||||
const Editor = await import("../dynamic/editors/cultures-editor.js?v=1.104.0");
|
||||
const Editor = await import("../dynamic/editors/cultures-editor.js?v=1.105.23");
|
||||
Editor.open();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ function editLabel() {
|
|||
|
||||
function redrawLabelPath() {
|
||||
const path = byId("textPath_" + elSelected.attr("id"));
|
||||
lineGen.curve(d3.curveBundle.beta(1));
|
||||
lineGen.curve(d3.curveNatural);
|
||||
const points = [];
|
||||
debug
|
||||
.select("#controlPoints")
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ function editLake() {
|
|||
debug.append("g").attr("id", "vertices");
|
||||
elSelected = d3.select(node);
|
||||
updateLakeValues();
|
||||
selectLakeGroup(node);
|
||||
selectLakeGroup();
|
||||
drawLakeVertices();
|
||||
viewbox.on("touchmove mousemove", null);
|
||||
|
||||
|
|
@ -140,13 +140,13 @@ function editLake() {
|
|||
lake.name = lakeName.value = Names.getBase(rand(nameBases.length - 1));
|
||||
}
|
||||
|
||||
function selectLakeGroup(node) {
|
||||
const group = node.parentNode.id;
|
||||
function selectLakeGroup() {
|
||||
const lake = getLake();
|
||||
|
||||
const select = byId("lakeGroup");
|
||||
select.options.length = 0; // remove all options
|
||||
|
||||
lakes.selectAll("g").each(function () {
|
||||
select.options.add(new Option(this.id, this.id, false, this.id === group));
|
||||
select.options.add(new Option(this.id, this.id, false, this.id === lake.group));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -796,14 +796,12 @@ function drawRivers() {
|
|||
TIME && console.time("drawRivers");
|
||||
rivers.selectAll("*").remove();
|
||||
|
||||
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
|
||||
const riverPaths = pack.rivers.map(({cells, points, i, widthFactor, sourceWidth}) => {
|
||||
if (!cells || cells.length < 2) return;
|
||||
|
||||
if (points && points.length !== cells.length) {
|
||||
console.error(
|
||||
`River ${i} has ${cells.length} cells, but only ${points.length} points defined.`,
|
||||
"Resetting points data"
|
||||
`River ${i} has ${cells.length} cells, but only ${points.length} points defined. Resetting points data`
|
||||
);
|
||||
points = undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ function editNamesbase() {
|
|||
|
||||
$("#namesbaseEditor").dialog({
|
||||
title: "Namesbase Editor",
|
||||
width: "auto",
|
||||
width: "60vw",
|
||||
position: {my: "center", at: "center", of: "svg"}
|
||||
});
|
||||
|
||||
|
|
@ -66,7 +66,7 @@ function editNamesbase() {
|
|||
function updateExamples() {
|
||||
const base = +document.getElementById("namesbaseSelect").value;
|
||||
let examples = "";
|
||||
for (let i = 0; i < 10; i++) {
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const example = Names.getBase(base);
|
||||
if (example === undefined) {
|
||||
examples = "Cannot generate examples. Please verify the data";
|
||||
|
|
@ -250,7 +250,7 @@ function editNamesbase() {
|
|||
const [rawName, min, max, d, m, rawNames] = base.split("|");
|
||||
const name = rawName.replace(unsafe, "");
|
||||
const names = rawNames.replace(unsafe, "");
|
||||
nameBases.push({name, min, max, d, m, b: names});
|
||||
nameBases.push({name, min: +min, max: +max, d, m: +m, b: names});
|
||||
});
|
||||
|
||||
createBasesList();
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ function editNotes(id, name) {
|
|||
}
|
||||
};
|
||||
|
||||
geneateWithAi(prompt, onApply);
|
||||
generateWithAi(prompt, onApply);
|
||||
}
|
||||
|
||||
function downloadLegends() {
|
||||
|
|
|
|||
|
|
@ -332,16 +332,12 @@ const cellsDensityMap = {
|
|||
|
||||
function changeCellsDensity(value) {
|
||||
pointsInput.value = value;
|
||||
const cells = cellsDensityMap[value] || 1000;
|
||||
const cells = cellsDensityMap[value] || pointsInput.dataset.cells;
|
||||
pointsInput.dataset.cells = cells;
|
||||
pointsOutputFormatted.value = getCellsDensityValue(cells);
|
||||
pointsOutputFormatted.value = cells / 1000 + "K";
|
||||
pointsOutputFormatted.style.color = getCellsDensityColor(cells);
|
||||
}
|
||||
|
||||
function getCellsDensityValue(cells) {
|
||||
return cells / 1000 + "K";
|
||||
}
|
||||
|
||||
function getCellsDensityColor(cells) {
|
||||
return cells > 50000 ? "#b12117" : cells !== 10000 ? "#dfdf12" : "#053305";
|
||||
}
|
||||
|
|
@ -558,10 +554,10 @@ function applyStoredOptions() {
|
|||
if (key.slice(0, 5) === "style") applyOption(stylePreset, key, key.slice(5));
|
||||
}
|
||||
|
||||
if (stored("winds")) options.winds = localStorage.getItem("winds").split(",").map(Number);
|
||||
if (stored("temperatureEquator")) options.temperatureEquator = +localStorage.getItem("temperatureEquator");
|
||||
if (stored("temperatureNorthPole")) options.temperatureNorthPole = +localStorage.getItem("temperatureNorthPole");
|
||||
if (stored("temperatureSouthPole")) options.temperatureSouthPole = +localStorage.getItem("temperatureSouthPole");
|
||||
if (stored("winds")) options.winds = stored("winds").split(",").map(Number);
|
||||
if (stored("temperatureEquator")) options.temperatureEquator = +stored("temperatureEquator");
|
||||
if (stored("temperatureNorthPole")) options.temperatureNorthPole = +stored("temperatureNorthPole");
|
||||
if (stored("temperatureSouthPole")) options.temperatureSouthPole = +stored("temperatureSouthPole");
|
||||
if (stored("military")) options.military = JSON.parse(stored("military"));
|
||||
|
||||
if (stored("tooltipSize")) changeTooltipSize(stored("tooltipSize"));
|
||||
|
|
|
|||
|
|
@ -74,13 +74,10 @@ function createRiver() {
|
|||
|
||||
function addRiver() {
|
||||
const {rivers, cells} = pack;
|
||||
const {addMeandering, getApproximateLength, getWidth, getOffset, getName, getRiverPath, getBasin, getNextId} =
|
||||
Rivers;
|
||||
|
||||
const riverCells = createRiver.cells;
|
||||
if (riverCells.length < 2) return tip("Add at least 2 cells", false, "error");
|
||||
|
||||
const riverId = getNextId(rivers);
|
||||
const riverId = Rivers.getNextId(rivers);
|
||||
const parent = cells.r[last(riverCells)] || riverId;
|
||||
|
||||
riverCells.forEach(cell => {
|
||||
|
|
@ -89,17 +86,24 @@ function createRiver() {
|
|||
|
||||
const source = riverCells[0];
|
||||
const mouth = parent === riverId ? last(riverCells) : riverCells[riverCells.length - 2];
|
||||
const sourceWidth = 0.05;
|
||||
const sourceWidth = Rivers.getSourceWidth(cells.fl[source]);
|
||||
const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2);
|
||||
const widthFactor = 1.2 * defaultWidthFactor;
|
||||
|
||||
const meanderedPoints = addMeandering(riverCells);
|
||||
const meanderedPoints = Rivers.addMeandering(riverCells);
|
||||
|
||||
const discharge = cells.fl[mouth]; // m3 in second
|
||||
const length = getApproximateLength(meanderedPoints);
|
||||
const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, sourceWidth));
|
||||
const name = getName(mouth);
|
||||
const basin = getBasin(parent);
|
||||
const length = Rivers.getApproximateLength(meanderedPoints);
|
||||
const width = Rivers.getWidth(
|
||||
Rivers.getOffset({
|
||||
flux: discharge,
|
||||
pointIndex: meanderedPoints.length,
|
||||
widthFactor,
|
||||
startingWidth: sourceWidth
|
||||
})
|
||||
);
|
||||
const name = Rivers.getName(mouth);
|
||||
const basin = Rivers.getBasin(parent);
|
||||
|
||||
rivers.push({
|
||||
i: riverId,
|
||||
|
|
@ -118,13 +122,11 @@ function createRiver() {
|
|||
});
|
||||
const id = "river" + riverId;
|
||||
|
||||
// render river
|
||||
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
|
||||
viewbox
|
||||
.select("#rivers")
|
||||
.append("path")
|
||||
.attr("id", id)
|
||||
.attr("d", getRiverPath(meanderedPoints, widthFactor, sourceWidth));
|
||||
.attr("d", Rivers.getRiverPath(meanderedPoints, widthFactor, sourceWidth));
|
||||
|
||||
editRiver(id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,10 +86,16 @@ function editRiver(id) {
|
|||
}
|
||||
|
||||
function updateRiverWidth(river) {
|
||||
const {addMeandering, getWidth, getOffset} = Rivers;
|
||||
const {cells, discharge, widthFactor, sourceWidth} = river;
|
||||
const meanderedPoints = addMeandering(cells);
|
||||
river.width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, sourceWidth));
|
||||
const meanderedPoints = Rivers.addMeandering(cells);
|
||||
river.width = Rivers.getWidth(
|
||||
Rivers.getOffset({
|
||||
flux: discharge,
|
||||
pointIndex: meanderedPoints.length,
|
||||
widthFactor,
|
||||
startingWidth: sourceWidth
|
||||
})
|
||||
);
|
||||
|
||||
const width = `${rn(river.width * distanceScale, 3)} ${distanceUnitInput.value}`;
|
||||
byId("riverWidth").value = width;
|
||||
|
|
@ -158,11 +164,9 @@ function editRiver(id) {
|
|||
river.points = debug.selectAll("#controlPoints > *").data();
|
||||
river.cells = river.points.map(([x, y]) => findCell(x, y));
|
||||
|
||||
const {widthFactor, sourceWidth} = river;
|
||||
const meanderedPoints = Rivers.addMeandering(river.cells, river.points);
|
||||
|
||||
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
|
||||
const path = Rivers.getRiverPath(meanderedPoints, widthFactor, sourceWidth);
|
||||
const meanderedPoints = Rivers.addMeandering(river.cells, river.points);
|
||||
const path = Rivers.getRiverPath(meanderedPoints, river.widthFactor, river.sourceWidth);
|
||||
elSelected.attr("d", path);
|
||||
|
||||
updateRiverLength(river);
|
||||
|
|
|
|||
|
|
@ -116,20 +116,20 @@ function selectStyleElement() {
|
|||
if (
|
||||
[
|
||||
"armies",
|
||||
"routes",
|
||||
"lakes",
|
||||
"biomes",
|
||||
"borders",
|
||||
"cults",
|
||||
"relig",
|
||||
"cells",
|
||||
"coastline",
|
||||
"prec",
|
||||
"coordinates",
|
||||
"cults",
|
||||
"gridOverlay",
|
||||
"ice",
|
||||
"icons",
|
||||
"coordinates",
|
||||
"zones",
|
||||
"gridOverlay"
|
||||
"lakes",
|
||||
"prec",
|
||||
"relig",
|
||||
"routes",
|
||||
"zones"
|
||||
].includes(styleElement)
|
||||
) {
|
||||
styleStroke.style.display = "block";
|
||||
|
|
@ -140,7 +140,7 @@ function selectStyleElement() {
|
|||
|
||||
// stroke dash
|
||||
if (
|
||||
["routes", "borders", "temperature", "legend", "population", "coordinates", "zones", "gridOverlay"].includes(
|
||||
["borders", "cells", "coordinates", "gridOverlay", "legend", "population", "routes", "temperature", "zones"].includes(
|
||||
styleElement
|
||||
)
|
||||
) {
|
||||
|
|
@ -788,7 +788,7 @@ styleShadowInput.on("input", function () {
|
|||
styleFontAdd.on("click", function () {
|
||||
addFontNameInput.value = "";
|
||||
addFontURLInput.value = "";
|
||||
|
||||
|
||||
$("#addFontDialog").dialog({
|
||||
title: "Add custom font",
|
||||
width: "26em",
|
||||
|
|
|
|||
95
modules/ui/submap-tool.js
Normal file
95
modules/ui/submap-tool.js
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
"use strict";
|
||||
|
||||
function openSubmapTool() {
|
||||
resetInputs();
|
||||
|
||||
$("#submapTool").dialog({
|
||||
title: "Create a submap",
|
||||
resizable: false,
|
||||
width: "32em",
|
||||
position: {my: "center", at: "center", of: "svg"},
|
||||
buttons: {
|
||||
Submap: function () {
|
||||
closeDialogs();
|
||||
generateSubmap();
|
||||
},
|
||||
Cancel: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (modules.openSubmapTool) return;
|
||||
modules.openSubmapTool = true;
|
||||
|
||||
function resetInputs() {
|
||||
updateCellsNumber(byId("pointsInput").value);
|
||||
byId("submapPointsInput").oninput = e => updateCellsNumber(e.target.value);
|
||||
|
||||
function updateCellsNumber(value) {
|
||||
byId("submapPointsInput").value = value;
|
||||
const cells = cellsDensityMap[value];
|
||||
byId("submapPointsInput").dataset.cells = cells;
|
||||
const output = byId("submapPointsFormatted");
|
||||
output.value = cells / 1000 + "K";
|
||||
output.style.color = getCellsDensityColor(cells);
|
||||
}
|
||||
}
|
||||
|
||||
function generateSubmap() {
|
||||
INFO && console.group("generateSubmap");
|
||||
|
||||
const [x0, y0] = [Math.abs(viewX / scale), Math.abs(viewY / scale)]; // top-left corner
|
||||
recalculateMapSize(x0, y0);
|
||||
|
||||
const submapPointsValue = byId("submapPointsInput").value;
|
||||
const globalPointsValue = byId("pointsInput").value;
|
||||
if (submapPointsValue !== globalPointsValue) changeCellsDensity(submapPointsValue);
|
||||
|
||||
const projection = (x, y) => [(x - x0) * scale, (y - y0) * scale];
|
||||
const inverse = (x, y) => [x / scale + x0, y / scale + y0];
|
||||
|
||||
resetZoom(0);
|
||||
undraw();
|
||||
Resample.process({projection, inverse, scale});
|
||||
|
||||
if (byId("submapRescaleBurgStyles").checked) rescaleBurgStyles(scale);
|
||||
drawLayers();
|
||||
|
||||
INFO && console.groupEnd("generateSubmap");
|
||||
}
|
||||
|
||||
function recalculateMapSize(x0, y0) {
|
||||
const mapSize = +byId("mapSizeOutput").value;
|
||||
byId("mapSizeOutput").value = byId("mapSizeInput").value = rn(mapSize / scale, 2);
|
||||
|
||||
const latT = mapCoordinates.latT / scale;
|
||||
const latN = getLatitude(y0);
|
||||
const latShift = (90 - latN) / (180 - latT);
|
||||
byId("latitudeOutput").value = byId("latitudeInput").value = rn(latShift * 100, 2);
|
||||
|
||||
const lotT = mapCoordinates.lonT / scale;
|
||||
const lonE = getLongitude(x0 + graphWidth / scale);
|
||||
const lonShift = (180 - lonE) / (360 - lotT);
|
||||
byId("longitudeOutput").value = byId("longitudeInput").value = rn(lonShift * 100, 2);
|
||||
|
||||
distanceScale = distanceScaleInput.value = rn(distanceScale / scale, 2);
|
||||
populationRate = populationRateInput.value = rn(populationRate / scale, 2);
|
||||
}
|
||||
|
||||
function rescaleBurgStyles(scale) {
|
||||
const burgIcons = [...byId("burgIcons").querySelectorAll("g")];
|
||||
for (const group of burgIcons) {
|
||||
const newRadius = rn(minmax(group.getAttribute("size") * scale, 0.2, 10), 2);
|
||||
changeRadius(newRadius, group.id);
|
||||
const strokeWidth = group.attributes["stroke-width"];
|
||||
strokeWidth.value = strokeWidth.value * scale;
|
||||
}
|
||||
|
||||
const burgLabels = [...byId("burgLabels").querySelectorAll("g")];
|
||||
for (const group of burgLabels) {
|
||||
const size = +group.dataset.size;
|
||||
group.dataset.size = Math.max(rn((size + size / scale) / 2, 2), 1) * scale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,332 +0,0 @@
|
|||
"use strict";
|
||||
// UI elements for submap generation
|
||||
|
||||
window.UISubmap = (function () {
|
||||
byId("submapPointsInput").addEventListener("input", function () {
|
||||
const output = byId("submapPointsOutputFormatted");
|
||||
const cells = cellsDensityMap[+this.value] || 1000;
|
||||
this.dataset.cells = cells;
|
||||
output.value = getCellsDensityValue(cells);
|
||||
output.style.color = getCellsDensityColor(cells);
|
||||
});
|
||||
|
||||
byId("submapScaleInput").addEventListener("input", function (event) {
|
||||
const exp = Math.pow(1.1, +event.target.value);
|
||||
byId("submapScaleOutput").value = rn(exp, 2);
|
||||
});
|
||||
|
||||
byId("submapAngleInput").addEventListener("input", function (event) {
|
||||
byId("submapAngleOutput").value = event.target.value;
|
||||
});
|
||||
|
||||
const $previewBox = byId("submapPreview");
|
||||
const $scaleInput = byId("submapScaleInput");
|
||||
const $shiftX = byId("submapShiftX");
|
||||
const $shiftY = byId("submapShiftY");
|
||||
|
||||
function openSubmapMenu() {
|
||||
$("#submapOptionsDialog").dialog({
|
||||
title: "Create a submap",
|
||||
resizable: false,
|
||||
width: "32em",
|
||||
position: {my: "center", at: "center", of: "svg"},
|
||||
buttons: {
|
||||
Submap: function () {
|
||||
$(this).dialog("close");
|
||||
generateSubmap();
|
||||
},
|
||||
Cancel: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const getTransformInput = _ => ({
|
||||
angle: (+byId("submapAngleInput").value / 180) * Math.PI,
|
||||
shiftX: +byId("submapShiftX").value,
|
||||
shiftY: +byId("submapShiftY").value,
|
||||
ratio: +byId("submapScaleInput").value,
|
||||
mirrorH: byId("submapMirrorH").checked,
|
||||
mirrorV: byId("submapMirrorV").checked
|
||||
});
|
||||
|
||||
async function openResampleMenu() {
|
||||
resetZoom(0);
|
||||
|
||||
byId("submapAngleInput").value = 0;
|
||||
byId("submapAngleOutput").value = "0";
|
||||
byId("submapScaleOutput").value = 1;
|
||||
byId("submapMirrorH").checked = false;
|
||||
byId("submapMirrorV").checked = false;
|
||||
$scaleInput.value = 0;
|
||||
$shiftX.value = 0;
|
||||
$shiftY.value = 0;
|
||||
|
||||
const w = Math.min(400, window.innerWidth * 0.5);
|
||||
const previewScale = w / graphWidth;
|
||||
const h = graphHeight * previewScale;
|
||||
$previewBox.style.width = w + "px";
|
||||
$previewBox.style.height = h + "px";
|
||||
|
||||
// handle mouse input
|
||||
const dispatchInput = e => e.dispatchEvent(new Event("input", {bubbles: true}));
|
||||
|
||||
// mouse wheel
|
||||
$previewBox.onwheel = e => {
|
||||
$scaleInput.value = $scaleInput.valueAsNumber - Math.sign(e.deltaY);
|
||||
dispatchInput($scaleInput);
|
||||
};
|
||||
|
||||
// mouse drag
|
||||
let mouseIsDown = false,
|
||||
mouseX = 0,
|
||||
mouseY = 0;
|
||||
$previewBox.onmousedown = e => {
|
||||
mouseIsDown = true;
|
||||
mouseX = $shiftX.value - e.clientX / previewScale;
|
||||
mouseY = $shiftY.value - e.clientY / previewScale;
|
||||
};
|
||||
$previewBox.onmouseup = _ => (mouseIsDown = false);
|
||||
$previewBox.onmouseleave = _ => (mouseIsDown = false);
|
||||
$previewBox.onmousemove = e => {
|
||||
if (!mouseIsDown) return;
|
||||
e.preventDefault();
|
||||
$shiftX.value = Math.round(mouseX + e.clientX / previewScale);
|
||||
$shiftY.value = Math.round(mouseY + e.clientY / previewScale);
|
||||
dispatchInput($shiftX);
|
||||
// dispatchInput($shiftY); // not needed X bubbles anyway
|
||||
};
|
||||
|
||||
$("#resampleDialog").dialog({
|
||||
title: "Transform map",
|
||||
resizable: false,
|
||||
position: {my: "center", at: "center", of: "svg"},
|
||||
buttons: {
|
||||
Transform: function () {
|
||||
$(this).dialog("close");
|
||||
resampleCurrentMap();
|
||||
},
|
||||
Cancel: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// use double resolution for PNG to get sharper image
|
||||
const $preview = await loadPreview($previewBox, w * 2, h * 2);
|
||||
// could be done with SVG. Faster to load, slower to use.
|
||||
// const $preview = await loadPreviewSVG($previewBox, w, h);
|
||||
$preview.style.position = "absolute";
|
||||
$preview.style.width = w + "px";
|
||||
$preview.style.height = h + "px";
|
||||
|
||||
byId("resampleDialog").oninput = event => {
|
||||
const {angle, shiftX, shiftY, ratio, mirrorH, mirrorV} = getTransformInput();
|
||||
const scale = Math.pow(1.1, ratio);
|
||||
const transformStyle = `
|
||||
translate(${shiftX * previewScale}px, ${shiftY * previewScale}px)
|
||||
scale(${mirrorH ? -scale : scale}, ${mirrorV ? -scale : scale})
|
||||
rotate(${angle}rad)
|
||||
`;
|
||||
|
||||
$preview.style.transform = transformStyle;
|
||||
$preview.style["transform-origin"] = "center";
|
||||
event.stopPropagation();
|
||||
};
|
||||
}
|
||||
|
||||
async function loadPreview($container, w, h) {
|
||||
const url = await getMapURL("png", {
|
||||
globe: false,
|
||||
noWater: true,
|
||||
fullMap: true,
|
||||
noLabels: true,
|
||||
noScaleBar: true,
|
||||
noVignette: true,
|
||||
noIce: true
|
||||
});
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
const img = new Image();
|
||||
img.src = url;
|
||||
img.onload = function () {
|
||||
ctx.drawImage(img, 0, 0, w, h);
|
||||
};
|
||||
$container.textContent = "";
|
||||
$container.appendChild(canvas);
|
||||
return canvas;
|
||||
}
|
||||
|
||||
// Resample the whole map to different cell resolution or shape
|
||||
const resampleCurrentMap = debounce(function () {
|
||||
WARN && console.warn("Resampling current map");
|
||||
const cellNumId = +byId("submapPointsInput").value;
|
||||
if (!cellsDensityMap[cellNumId]) return console.error("Unknown cell number!");
|
||||
|
||||
const {angle, shiftX, shiftY, ratio, mirrorH, mirrorV} = getTransformInput();
|
||||
|
||||
const [cx, cy] = [graphWidth / 2, graphHeight / 2];
|
||||
const rot = alfa => (x, y) =>
|
||||
[
|
||||
(x - cx) * Math.cos(alfa) - (y - cy) * Math.sin(alfa) + cx,
|
||||
(y - cy) * Math.cos(alfa) + (x - cx) * Math.sin(alfa) + cy
|
||||
];
|
||||
const shift = (dx, dy) => (x, y) => [x + dx, y + dy];
|
||||
const scale = r => (x, y) => [(x - cx) * r + cx, (y - cy) * r + cy];
|
||||
const flipH = (x, y) => [-x + 2 * cx, y];
|
||||
const flipV = (x, y) => [x, -y + 2 * cy];
|
||||
const app = (f, g) => (x, y) => f(...g(x, y));
|
||||
const id = (x, y) => [x, y];
|
||||
|
||||
let projection = id;
|
||||
let inverse = id;
|
||||
|
||||
if (angle) [projection, inverse] = [rot(angle), rot(-angle)];
|
||||
if (ratio)
|
||||
[projection, inverse] = [
|
||||
app(scale(Math.pow(1.1, ratio)), projection),
|
||||
app(inverse, scale(Math.pow(1.1, -ratio)))
|
||||
];
|
||||
if (mirrorH) [projection, inverse] = [app(flipH, projection), app(inverse, flipH)];
|
||||
if (mirrorV) [projection, inverse] = [app(flipV, projection), app(inverse, flipV)];
|
||||
if (shiftX || shiftY) {
|
||||
projection = app(shift(shiftX, shiftY), projection);
|
||||
inverse = app(inverse, shift(-shiftX, -shiftY));
|
||||
}
|
||||
|
||||
changeCellsDensity(cellNumId);
|
||||
startResample({
|
||||
lockMarkers: false,
|
||||
lockBurgs: false,
|
||||
depressRivers: false,
|
||||
addLakesInDepressions: false,
|
||||
promoteTowns: false,
|
||||
smoothHeightMap: false,
|
||||
rescaleStyles: false,
|
||||
scale: 1,
|
||||
projection,
|
||||
inverse
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
// Create submap from the current map. Submap limits defined by the current window size (canvas viewport)
|
||||
const generateSubmap = debounce(function () {
|
||||
WARN && console.warn("Resampling current map");
|
||||
closeDialogs("#worldConfigurator, #options3d");
|
||||
const checked = id => Boolean(byId(id).checked);
|
||||
|
||||
// Create projection func from current zoom extents
|
||||
const [[x0, y0], [x1, y1]] = getViewBoxExtent();
|
||||
const origScale = scale;
|
||||
|
||||
const options = {
|
||||
lockMarkers: checked("submapLockMarkers"),
|
||||
lockBurgs: checked("submapLockBurgs"),
|
||||
|
||||
depressRivers: checked("submapDepressRivers"),
|
||||
addLakesInDepressions: checked("submapAddLakeInDepression"),
|
||||
promoteTowns: checked("submapPromoteTowns"),
|
||||
rescaleStyles: checked("submapRescaleStyles"),
|
||||
smoothHeightMap: scale > 2,
|
||||
inverse: (x, y) => [x / origScale + x0, y / origScale + y0],
|
||||
projection: (x, y) => [(x - x0) * origScale, (y - y0) * origScale],
|
||||
scale: origScale
|
||||
};
|
||||
|
||||
// converting map position on the planet
|
||||
const mapSizeOutput = byId("mapSizeOutput");
|
||||
const latitudeOutput = byId("latitudeOutput");
|
||||
const latN = 90 - ((180 - (mapSizeInput.value / 100) * 180) * latitudeOutput.value) / 100;
|
||||
const newLatN = latN - ((y0 / graphHeight) * mapSizeOutput.value * 180) / 100;
|
||||
mapSizeOutput.value /= scale;
|
||||
latitudeOutput.value = ((90 - newLatN) / (180 - (mapSizeOutput.value / 100) * 180)) * 100;
|
||||
byId("mapSizeInput").value = mapSizeOutput.value;
|
||||
byId("latitudeInput").value = latitudeOutput.value;
|
||||
|
||||
// fix scale
|
||||
distanceScale = distanceScaleInput.value = rn(distanceScaleInput.value / scale, 2);
|
||||
populationRate = populationRateInput.value = rn(populationRateInput.value / scale, 2);
|
||||
|
||||
customization = 0;
|
||||
startResample(options);
|
||||
}, 1000);
|
||||
|
||||
async function startResample(options) {
|
||||
// Do model changes with Submap.resample then do view changes if needed
|
||||
resetZoom(0);
|
||||
let oldstate = {
|
||||
grid: deepCopy(grid),
|
||||
pack: deepCopy(pack),
|
||||
notes: deepCopy(notes),
|
||||
seed,
|
||||
graphWidth,
|
||||
graphHeight
|
||||
};
|
||||
undraw();
|
||||
try {
|
||||
const oldScale = scale;
|
||||
await Submap.resample(oldstate, options);
|
||||
if (options.promoteTowns) {
|
||||
const groupName = "largetowns";
|
||||
moveAllBurgsToGroup("towns", groupName);
|
||||
changeRadius(rn(oldScale * 0.8, 2), groupName);
|
||||
changeFontSize(svg.select(`#labels #${groupName}`), rn(oldScale * 2, 2));
|
||||
invokeActiveZooming();
|
||||
}
|
||||
if (options.rescaleStyles) changeStyles(oldScale);
|
||||
} catch (error) {
|
||||
showSubmapErrorHandler(error);
|
||||
}
|
||||
|
||||
oldstate = null; // destroy old state to free memory
|
||||
|
||||
drawLayers();
|
||||
if (ThreeD.options.isOn) ThreeD.redraw();
|
||||
if ($("#worldConfigurator").is(":visible")) editWorld();
|
||||
}
|
||||
|
||||
function changeStyles(scale) {
|
||||
// resize burgIcons
|
||||
const burgIcons = [...byId("burgIcons").querySelectorAll("g")];
|
||||
for (const bi of burgIcons) {
|
||||
const newRadius = rn(minmax(bi.getAttribute("size") * scale, 0.2, 10), 2);
|
||||
changeRadius(newRadius, bi.id);
|
||||
const swAttr = bi.attributes["stroke-width"];
|
||||
swAttr.value = +swAttr.value * scale;
|
||||
}
|
||||
|
||||
// burglabels
|
||||
const burgLabels = [...byId("burgLabels").querySelectorAll("g")];
|
||||
for (const bl of burgLabels) {
|
||||
const size = +bl.dataset["size"];
|
||||
bl.dataset["size"] = Math.max(rn((size + size / scale) / 2, 2), 1) * scale;
|
||||
}
|
||||
|
||||
drawEmblems();
|
||||
}
|
||||
|
||||
function showSubmapErrorHandler(error) {
|
||||
ERROR && console.error(error);
|
||||
clearMainTip();
|
||||
|
||||
alertMessage.innerHTML = /* html */ `Map resampling failed: <br />You may retry after clearing stored data or contact us at discord.
|
||||
<p id="errorBox">${parseError(error)}</p>`;
|
||||
$("#alert").dialog({
|
||||
resizable: false,
|
||||
title: "Resampling error",
|
||||
width: "32em",
|
||||
buttons: {
|
||||
Ok: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
},
|
||||
position: {my: "center", at: "center", of: "svg"}
|
||||
});
|
||||
}
|
||||
|
||||
return {openSubmapMenu, openResampleMenu};
|
||||
})();
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
// module to control the Tools options (click to edit, to re-geenerate, tp add)
|
||||
|
||||
toolsContent.addEventListener("click", function (event) {
|
||||
if (customization) return tip("Please exit the customization mode first", false, "warning");
|
||||
if (customization) return tip("Please exit the customization mode first", false, "error");
|
||||
if (!["BUTTON", "I"].includes(event.target.tagName)) return;
|
||||
const button = event.target.id;
|
||||
|
||||
|
|
@ -70,8 +70,8 @@ toolsContent.addEventListener("click", function (event) {
|
|||
else if (button === "addRoute") createRoute();
|
||||
else if (button === "addMarker") toggleAddMarker();
|
||||
// click to create a new map buttons
|
||||
else if (button === "openSubmapMenu") UISubmap.openSubmapMenu();
|
||||
else if (button === "openResampleMenu") UISubmap.openResampleMenu();
|
||||
else if (button === "openSubmapTool") openSubmapTool();
|
||||
else if (button === "openTransformTool") openTransformTool();
|
||||
});
|
||||
|
||||
function processFeatureRegeneration(event, button) {
|
||||
|
|
@ -514,8 +514,8 @@ function regenerateEmblems() {
|
|||
|
||||
function regenerateReligions() {
|
||||
Religions.generate();
|
||||
if (layerIsOn("toggleReligions")) drawReligions();
|
||||
else toggleReligions();
|
||||
|
||||
layerIsOn("toggleReligions") ? drawReligions() : toggleReligions();
|
||||
refreshAllEditors();
|
||||
}
|
||||
|
||||
|
|
@ -685,28 +685,15 @@ function addRiverOnClick() {
|
|||
if (cells.h[i] < 20) return tip("Cannot create river in water cell", false, "error");
|
||||
if (cells.b[i]) return;
|
||||
|
||||
const {
|
||||
alterHeights,
|
||||
resolveDepressions,
|
||||
addMeandering,
|
||||
getRiverPath,
|
||||
getBasin,
|
||||
getName,
|
||||
getType,
|
||||
getWidth,
|
||||
getOffset,
|
||||
getApproximateLength,
|
||||
getNextId
|
||||
} = Rivers;
|
||||
const riverCells = [];
|
||||
let riverId = getNextId(rivers);
|
||||
let riverId = Rivers.getNextId(rivers);
|
||||
let parent = riverId;
|
||||
|
||||
const initialFlux = grid.cells.prec[cells.g[i]];
|
||||
cells.fl[i] = initialFlux;
|
||||
|
||||
const h = alterHeights();
|
||||
resolveDepressions(h);
|
||||
const h = Rivers.alterHeights();
|
||||
Rivers.resolveDepressions(h);
|
||||
|
||||
while (i) {
|
||||
cells.r[i] = riverId;
|
||||
|
|
@ -780,11 +767,19 @@ function addRiverOnClick() {
|
|||
const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2);
|
||||
const widthFactor =
|
||||
river?.widthFactor || (!parent || parent === riverId ? defaultWidthFactor * 1.2 : defaultWidthFactor);
|
||||
const meanderedPoints = addMeandering(riverCells);
|
||||
const sourceWidth = river?.sourceWidth || Rivers.getSourceWidth(cells.fl[source]);
|
||||
const meanderedPoints = Rivers.addMeandering(riverCells);
|
||||
|
||||
const discharge = cells.fl[mouth]; // m3 in second
|
||||
const length = getApproximateLength(meanderedPoints);
|
||||
const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor));
|
||||
const length = Rivers.getApproximateLength(meanderedPoints);
|
||||
const width = Rivers.getWidth(
|
||||
Rivers.getOffset({
|
||||
flux: discharge,
|
||||
pointIndex: meanderedPoints.length,
|
||||
widthFactor,
|
||||
startingWidth: sourceWidth
|
||||
})
|
||||
);
|
||||
|
||||
if (river) {
|
||||
river.source = source;
|
||||
|
|
@ -793,9 +788,9 @@ function addRiverOnClick() {
|
|||
river.width = width;
|
||||
river.cells = riverCells;
|
||||
} else {
|
||||
const basin = getBasin(parent);
|
||||
const name = getName(mouth);
|
||||
const type = getType({i: riverId, length, parent});
|
||||
const basin = Rivers.getBasin(parent);
|
||||
const name = Rivers.getName(mouth);
|
||||
const type = Rivers.getType({i: riverId, length, parent});
|
||||
|
||||
rivers.push({
|
||||
i: riverId,
|
||||
|
|
@ -805,7 +800,7 @@ function addRiverOnClick() {
|
|||
length,
|
||||
width,
|
||||
widthFactor,
|
||||
sourceWidth: 0,
|
||||
sourceWidth,
|
||||
parent,
|
||||
cells: riverCells,
|
||||
basin,
|
||||
|
|
@ -815,8 +810,7 @@ function addRiverOnClick() {
|
|||
}
|
||||
|
||||
// render river
|
||||
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
|
||||
const path = getRiverPath(meanderedPoints, widthFactor);
|
||||
const path = Rivers.getRiverPath(meanderedPoints, widthFactor, sourceWidth);
|
||||
const id = "river" + riverId;
|
||||
const riversG = viewbox.select("#rivers");
|
||||
riversG.append("path").attr("id", id).attr("d", path);
|
||||
|
|
|
|||
201
modules/ui/transform-tool.js
Normal file
201
modules/ui/transform-tool.js
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
"use strict";
|
||||
|
||||
async function openTransformTool() {
|
||||
const width = Math.min(400, window.innerWidth * 0.5);
|
||||
const previewScale = width / graphWidth;
|
||||
const height = graphHeight * previewScale;
|
||||
|
||||
let mouseIsDown = false;
|
||||
let mouseX = 0;
|
||||
let mouseY = 0;
|
||||
|
||||
resetInputs();
|
||||
loadPreview();
|
||||
|
||||
$("#transformTool").dialog({
|
||||
title: "Transform map",
|
||||
resizable: false,
|
||||
position: {my: "center", at: "center", of: "svg"},
|
||||
buttons: {
|
||||
Transform: function () {
|
||||
closeDialogs();
|
||||
transformMap();
|
||||
},
|
||||
Cancel: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (modules.openTransformTool) return;
|
||||
modules.openTransformTool = true;
|
||||
|
||||
// add listeners
|
||||
byId("transformToolBody").on("input", handleInput);
|
||||
byId("transformPreview")
|
||||
.on("mousedown", handleMousedown)
|
||||
.on("mouseup", _ => (mouseIsDown = false))
|
||||
.on("mousemove", handleMousemove)
|
||||
.on("wheel", handleWheel);
|
||||
|
||||
async function loadPreview() {
|
||||
byId("transformPreview").style.width = width + "px";
|
||||
byId("transformPreview").style.height = height + "px";
|
||||
|
||||
const options = {noWater: true, fullMap: true, noLabels: true, noScaleBar: true, noVignette: true, noIce: true};
|
||||
const url = await getMapURL("png", options);
|
||||
const SCALE = 4;
|
||||
|
||||
const img = new Image();
|
||||
img.src = url;
|
||||
img.onload = function () {
|
||||
const $canvas = byId("transformPreviewCanvas");
|
||||
$canvas.style.width = width + "px";
|
||||
$canvas.style.height = height + "px";
|
||||
$canvas.width = width * SCALE;
|
||||
$canvas.height = height * SCALE;
|
||||
$canvas.getContext("2d").drawImage(img, 0, 0, width * SCALE, height * SCALE);
|
||||
};
|
||||
}
|
||||
|
||||
function resetInputs() {
|
||||
byId("transformAngleInput").value = 0;
|
||||
byId("transformAngleOutput").value = "0";
|
||||
byId("transformMirrorH").checked = false;
|
||||
byId("transformMirrorV").checked = false;
|
||||
byId("transformScaleInput").value = 0;
|
||||
byId("transformScaleResult").value = 1;
|
||||
byId("transformShiftX").value = 0;
|
||||
byId("transformShiftY").value = 0;
|
||||
handleInput();
|
||||
|
||||
updateCellsNumber(byId("pointsInput").value);
|
||||
byId("transformPointsInput").oninput = e => updateCellsNumber(e.target.value);
|
||||
|
||||
function updateCellsNumber(value) {
|
||||
byId("transformPointsInput").value = value;
|
||||
const cells = cellsDensityMap[value];
|
||||
byId("transformPointsInput").dataset.cells = cells;
|
||||
const output = byId("transformPointsFormatted");
|
||||
output.value = cells / 1000 + "K";
|
||||
output.style.color = getCellsDensityColor(cells);
|
||||
}
|
||||
}
|
||||
|
||||
function handleInput() {
|
||||
const angle = (+byId("transformAngleInput").value / 180) * Math.PI;
|
||||
const shiftX = +byId("transformShiftX").value;
|
||||
const shiftY = +byId("transformShiftY").value;
|
||||
const mirrorH = byId("transformMirrorH").checked;
|
||||
const mirrorV = byId("transformMirrorV").checked;
|
||||
|
||||
const EXP = 1.0965;
|
||||
const scale = rn(EXP ** +byId("transformScaleInput").value, 2); // [0.1, 10]x
|
||||
byId("transformScaleResult").value = scale;
|
||||
|
||||
byId("transformPreviewCanvas").style.transform = `
|
||||
translate(${shiftX * previewScale}px, ${shiftY * previewScale}px)
|
||||
scale(${mirrorH ? -scale : scale}, ${mirrorV ? -scale : scale})
|
||||
rotate(${angle}rad)
|
||||
`;
|
||||
}
|
||||
|
||||
function handleMousedown(e) {
|
||||
mouseIsDown = true;
|
||||
const shiftX = +byId("transformShiftX").value;
|
||||
const shiftY = +byId("transformShiftY").value;
|
||||
mouseX = shiftX - e.clientX / previewScale;
|
||||
mouseY = shiftY - e.clientY / previewScale;
|
||||
}
|
||||
|
||||
function handleMousemove(e) {
|
||||
if (!mouseIsDown) return;
|
||||
e.preventDefault();
|
||||
|
||||
byId("transformShiftX").value = Math.round(mouseX + e.clientX / previewScale);
|
||||
byId("transformShiftY").value = Math.round(mouseY + e.clientY / previewScale);
|
||||
handleInput();
|
||||
}
|
||||
|
||||
function handleWheel(e) {
|
||||
const $scaleInput = byId("transformScaleInput");
|
||||
$scaleInput.value = $scaleInput.valueAsNumber - Math.sign(e.deltaY);
|
||||
handleInput();
|
||||
}
|
||||
|
||||
function transformMap() {
|
||||
INFO && console.group("transformMap");
|
||||
|
||||
const transformPointsValue = byId("transformPointsInput").value;
|
||||
const globalPointsValue = byId("pointsInput").value;
|
||||
if (transformPointsValue !== globalPointsValue) changeCellsDensity(transformPointsValue);
|
||||
|
||||
const [projection, inverse] = getProjection();
|
||||
|
||||
resetZoom(0);
|
||||
undraw();
|
||||
Resample.process({projection, inverse, scale: 1});
|
||||
drawLayers();
|
||||
|
||||
INFO && console.groupEnd("transformMap");
|
||||
}
|
||||
|
||||
function getProjection() {
|
||||
const centerX = graphWidth / 2;
|
||||
const centerY = graphHeight / 2;
|
||||
const shiftX = +byId("transformShiftX").value;
|
||||
const shiftY = +byId("transformShiftY").value;
|
||||
const angle = (+byId("transformAngleInput").value / 180) * Math.PI;
|
||||
const cos = Math.cos(angle);
|
||||
const sin = Math.sin(angle);
|
||||
const scale = +byId("transformScaleResult").value;
|
||||
const mirrorH = byId("transformMirrorH").checked;
|
||||
const mirrorV = byId("transformMirrorV").checked;
|
||||
|
||||
function project(x, y) {
|
||||
// center the point
|
||||
x -= centerX;
|
||||
y -= centerY;
|
||||
|
||||
// apply scale
|
||||
if (scale !== 1) {
|
||||
x *= scale;
|
||||
y *= scale;
|
||||
}
|
||||
|
||||
// apply rotation
|
||||
if (angle) [x, y] = [x * cos - y * sin, x * sin + y * cos];
|
||||
|
||||
// apply mirroring
|
||||
if (mirrorH) x = -x;
|
||||
if (mirrorV) y = -y;
|
||||
|
||||
// uncenter the point and apply shift
|
||||
return [x + centerX + shiftX, y + centerY + shiftY];
|
||||
}
|
||||
|
||||
function inverse(x, y) {
|
||||
// undo shift and center the point
|
||||
x -= centerX + shiftX;
|
||||
y -= centerY + shiftY;
|
||||
|
||||
// undo mirroring
|
||||
if (mirrorV) y = -y;
|
||||
if (mirrorH) x = -x;
|
||||
|
||||
// undo rotation
|
||||
if (angle !== 0) [x, y] = [x * cos + y * sin, -x * sin + y * cos];
|
||||
|
||||
// undo scale
|
||||
if (scale !== 1) {
|
||||
x /= scale;
|
||||
y /= scale;
|
||||
}
|
||||
|
||||
// uncenter the point
|
||||
return [x + centerX, y + centerY];
|
||||
}
|
||||
|
||||
return [project, inverse];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
"use strict";
|
||||
|
||||
function editZones() {
|
||||
closeDialogs();
|
||||
closeDialogs("#zonesEditor, .stable");
|
||||
if (!layerIsOn("toggleZones")) toggleZones();
|
||||
const body = byId("zonesBodySection");
|
||||
|
||||
|
|
@ -341,6 +341,8 @@ function editZones() {
|
|||
}
|
||||
|
||||
function toggleLegend() {
|
||||
if (legend.selectAll("*").size()) return clearLegend(); // hide legend
|
||||
|
||||
const filterBy = byId("zonesFilterType").value;
|
||||
const isFiltered = filterBy && filterBy !== "all";
|
||||
const visibleZones = pack.zones.filter(zone => !zone.hidden && (!isFiltered || zone.type === filterBy));
|
||||
|
|
|
|||
|
|
@ -209,11 +209,11 @@ window.Zones = (function () {
|
|||
const cost = [];
|
||||
const maxCells = rand(20, 40);
|
||||
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||
queue.queue({e: burg.cell, p: 0});
|
||||
const queue = new FlatQueue();
|
||||
queue.push({e: burg.cell, p: 0}, 0);
|
||||
|
||||
while (queue.length) {
|
||||
const next = queue.dequeue();
|
||||
const next = queue.pop();
|
||||
if (cells.burg[next.e] || cells.pop[next.e]) cellsArray.push(next.e);
|
||||
usedCells[next.e] = 1;
|
||||
|
||||
|
|
@ -224,7 +224,7 @@ window.Zones = (function () {
|
|||
|
||||
if (!cost[nextCellId] || p < cost[nextCellId]) {
|
||||
cost[nextCellId] = p;
|
||||
queue.queue({e: nextCellId, p});
|
||||
queue.push({e: nextCellId, p}, p);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -251,11 +251,11 @@ window.Zones = (function () {
|
|||
const cost = [];
|
||||
const maxCells = rand(5, 25);
|
||||
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||
queue.queue({e: burg.cell, p: 0});
|
||||
const queue = new FlatQueue();
|
||||
queue.push({e: burg.cell, p: 0}, 0);
|
||||
|
||||
while (queue.length) {
|
||||
const next = queue.dequeue();
|
||||
const next = queue.pop();
|
||||
if (cells.burg[next.e] || cells.pop[next.e]) cellsArray.push(next.e);
|
||||
usedCells[next.e] = 1;
|
||||
|
||||
|
|
@ -266,7 +266,7 @@ window.Zones = (function () {
|
|||
|
||||
if (!cost[e] || p < cost[e]) {
|
||||
cost[e] = p;
|
||||
queue.queue({e, p});
|
||||
queue.push({e, p}, p);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,3 +56,17 @@ function drawRouteConnections() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawPoint([x, y], {color = "red", radius = 0.5}) {
|
||||
debug.append("circle").attr("cx", x).attr("cy", y).attr("r", radius).attr("fill", color);
|
||||
}
|
||||
|
||||
function drawPath(points, {color = "red", width = 0.5}) {
|
||||
const lineGen = d3.line().curve(d3.curveBundle);
|
||||
debug
|
||||
.append("path")
|
||||
.attr("d", round(lineGen(points)))
|
||||
.attr("stroke", color)
|
||||
.attr("stroke-width", width)
|
||||
.attr("fill", "none");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -241,10 +241,8 @@ void (function addFindAll() {
|
|||
};
|
||||
|
||||
const tree_filter = function (x, y, radius) {
|
||||
var t = {x, y, x0: this._x0, y0: this._y0, x3: this._x1, y3: this._y1, quads: [], node: this._root};
|
||||
if (t.node) {
|
||||
t.quads.push(new Quad(t.node, t.x0, t.y0, t.x3, t.y3));
|
||||
}
|
||||
const t = {x, y, x0: this._x0, y0: this._y0, x3: this._x1, y3: this._y1, quads: [], node: this._root};
|
||||
if (t.node) t.quads.push(new Quad(t.node, t.x0, t.y0, t.x3, t.y3));
|
||||
radiusSearchInit(t, radius);
|
||||
|
||||
var i = 0;
|
||||
|
|
|
|||
|
|
@ -78,11 +78,10 @@ function getBorderPath(vertices, vertexChain, discontinue) {
|
|||
}
|
||||
|
||||
const operation = discontinued ? "M" : "L";
|
||||
const command = operation === lastOperation ? "" : operation;
|
||||
|
||||
discontinued = false;
|
||||
lastOperation = operation;
|
||||
|
||||
const command = operation === "L" && operation === lastOperation ? "" : operation;
|
||||
return ` ${command}${vertices.p[vertexId]}`;
|
||||
});
|
||||
|
||||
|
|
@ -177,3 +176,60 @@ function connectVertices({vertices, startingVertex, ofSameType, addToChecked, cl
|
|||
if (closeRing) chain.push(startingVertex);
|
||||
return chain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the shortest path between two cells using a cost-based pathfinding algorithm.
|
||||
* @param {number} start - The ID of the starting cell.
|
||||
* @param {(id: number) => boolean} isExit - A function that returns true if the cell is the exit cell.
|
||||
* @param {(current: number, next: number) => number} getCost - A function that returns the path cost from current cell to the next cell. Must return `Infinity` for impassable connections.
|
||||
* @returns {number[] | null} An array of cell IDs of the path from start to exit, or null if no path is found or start and exit are the same.
|
||||
*/
|
||||
function findPath(start, isExit, getCost) {
|
||||
if (isExit(start)) return null;
|
||||
|
||||
const from = [];
|
||||
const cost = [];
|
||||
const queue = new FlatQueue();
|
||||
queue.push(start, 0);
|
||||
|
||||
while (queue.length) {
|
||||
const currentCost = queue.peekValue();
|
||||
const current = queue.pop();
|
||||
|
||||
for (const next of pack.cells.c[current]) {
|
||||
if (isExit(next)) {
|
||||
from[next] = current;
|
||||
return restorePath(next, start, from);
|
||||
}
|
||||
|
||||
const nextCost = getCost(current, next);
|
||||
if (nextCost === Infinity) continue; // impassable cell
|
||||
const totalCost = currentCost + nextCost;
|
||||
|
||||
if (totalCost >= cost[next]) continue; // has cheaper path
|
||||
from[next] = current;
|
||||
cost[next] = totalCost;
|
||||
queue.push(next, totalCost);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// supplementary function for findPath
|
||||
function restorePath(exit, start, from) {
|
||||
const pathCells = [];
|
||||
|
||||
let current = exit;
|
||||
let prev = exit;
|
||||
|
||||
while (current !== start) {
|
||||
pathCells.push(current);
|
||||
prev = from[current];
|
||||
current = prev;
|
||||
}
|
||||
|
||||
pathCells.push(current);
|
||||
|
||||
return pathCells.reverse();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,14 @@ JSON.isValid = str => {
|
|||
}
|
||||
};
|
||||
|
||||
JSON.safeParse = str => {
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
function sanitizeId(string) {
|
||||
if (!string) throw new Error("No string provided");
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
*
|
||||
* Example: 1.102.2 -> Major version 1, Minor version 102, Patch version 2
|
||||
*/
|
||||
const VERSION = "1.106.0";
|
||||
const VERSION = "1.107.0";
|
||||
if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format or parsing function");
|
||||
|
||||
{
|
||||
|
|
@ -36,6 +36,7 @@ if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format o
|
|||
|
||||
<ul>
|
||||
<strong>Latest changes:</strong>
|
||||
<li>Submap and Transform tools rework</li>
|
||||
<li>Azgaar Bot to answer questions and provide help</li>
|
||||
<li>Labels: ability to set letter spacing</li>
|
||||
<li>Zones performance improvement</li>
|
||||
|
|
@ -46,7 +47,6 @@ if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format o
|
|||
<li>Configurable longitude</li>
|
||||
<li>Preview villages map</li>
|
||||
<li>Ability to render ocean heightmap</li>
|
||||
<li>Scale bar styling features</li>
|
||||
</ul>
|
||||
|
||||
<p>Join our <a href="${discord}" target="_blank">Discord server</a> and <a href="${reddit}" target="_blank">Reddit community</a> to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.</p>
|
||||
|
|
@ -58,7 +58,7 @@ if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format o
|
|||
width: "28em",
|
||||
position: {my: "center center-4em", at: "center", of: "svg"},
|
||||
buttons: {
|
||||
"Cleanup data": () => cleanupData(),
|
||||
"Clear cache": () => cleanupData(),
|
||||
"Don't show again": function () {
|
||||
$(this).dialog("close");
|
||||
localStorage.setItem("version", VERSION);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue