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

This commit is contained in:
Azgaar 2024-12-12 13:29:40 +01:00
commit e402120b8d
42 changed files with 1526 additions and 1067 deletions

2
.github/FUNDING.yml vendored
View file

@ -1,3 +1,3 @@
# These are supported funding model platforms # These are supported funding model platforms
github: Azgaar
patreon: Azgaar patreon: Azgaar

View file

@ -1081,8 +1081,14 @@
id="styleGridSizeFriendly" id="styleGridSizeFriendly"
data-tip="Distance between grid cell centers (in map scale)" data-tip="Distance between grid cell centers (in map scale)"
></output> ></output>
<a href="https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Scale-and-distance#grids" target="_blank"> <a
<span data-tip="Open wiki article scale and distance to know about grid scale" class="icon-info-circled pointer"></span> 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> </a>
</td> </td>
</tr> </tr>
@ -1631,10 +1637,10 @@
</td> </td>
<td>Cultures number</td> <td>Cultures number</td>
<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>
<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> </td>
</tr> </tr>
@ -1834,6 +1840,7 @@
<select id="azgaarAssistant" data-stored="azgaarAssistant"> <select id="azgaarAssistant" data-stored="azgaarAssistant">
<option value="show" selected>Show</option> <option value="show" selected>Show</option>
<option value="hide">Hide</option> <option value="hide">Hide</option>
</select>
</td> </td>
</tr> </tr>
@ -2179,8 +2186,8 @@
<div class="separator">Create</div> <div class="separator">Create</div>
<div class="grid"> <div class="grid">
<button id="openSubmapMenu" data-tip="Click to generate a submap from the current viewport">Submap</button> <button id="openSubmapTool" 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="openTransformTool" data-tip="Click to transform the map">Transform</button>
</div> </div>
</div> </div>
@ -2418,7 +2425,9 @@
<div id="exitCustomization"> <div id="exitCustomization">
<div data-tip="Drag to move the pane"> <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>
</div> </div>
@ -3498,7 +3507,11 @@
<button id="burgEditEmblem" data-tip="Edit emblem" class="icon-shield-alt"></button> <button id="burgEditEmblem" data-tip="Edit emblem" class="icon-shield-alt"></button>
<button id="burgSetPreviewLink" data-tip="Set custom burg map URL" class="icon-map-o"></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="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="burglLegend" data-tip="Edit free text notes (legend) for this burg" class="icon-edit"></button>
<button id="burgLock" class="icon-lock-open" onmouseover="showElementLockTip(event)"></button> <button id="burgLock" class="icon-lock-open" onmouseover="showElementLockTip(event)"></button>
<button <button
@ -4756,7 +4769,7 @@
<span style="margin-left: 2px">Names data: </span> <span style="margin-left: 2px">Names data: </span>
</div> </div>
<div id="namesbaseBody" style="margin-block: 2px"> <div id="namesbaseBody" style="margin-block: 2px; width: auto">
<textarea <textarea
id="namesbaseTextarea" id="namesbaseTextarea"
data-base="0" data-base="0"
@ -4765,6 +4778,7 @@
placeholder="Provide a names data: a comma separated list of source names" placeholder="Provide a names data: a comma separated list of source names"
autocorrect="off" autocorrect="off"
spellcheck="false" spellcheck="false"
style="resize: none"
></textarea> ></textarea>
<div> <div>
@ -4936,18 +4950,21 @@
>Model: >Model:
<select id="aiGeneratorModel"></select> <select id="aiGeneratorModel"></select>
</label> </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" <label for="aiGeneratorKey"
>Key: >Key:
<input id="aiGeneratorKey" placeholder="Enter OpenAI API key" class="icon-key" /> <input id="aiGeneratorKey" placeholder="Enter API key" class="icon-key" />
<a <button
href="https://platform.openai.com/account/api-keys" id="aiGeneratorKeyHelp"
target="_blank"
rel="noreferrer"
class="icon-help-circled" class="icon-help-circled"
style="text-decoration: none" 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"
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>
</label> </label>
</div> </div>
</div> </div>
@ -5275,9 +5292,7 @@
<div data-tip="Click to sort by culture name" class="sortable alphabetically" data-sortby="culture"> <div data-tip="Click to sort by culture name" class="sortable alphabetically" data-sortby="culture">
Culture Culture
</div> </div>
<div data-tip="Click to sort by culture group" class="sortable alphabetically" data-sortby="group"> <div data-tip="Click to sort by culture group" class="sortable alphabetically" data-sortby="group">Group</div>
Group
</div>
<div <div
data-tip="Click to sort by burg population" data-tip="Click to sort by burg population"
class="sortable icon-sort-number-down" class="sortable icon-sort-number-down"
@ -5285,7 +5300,9 @@
> >
Population Population
</div> </div>
<div data-tip="Click to sort by burg features" class="sortable alphabetically" data-sortby="features">Features&nbsp;</div> <div data-tip="Click to sort by burg features" class="sortable alphabetically" data-sortby="features">
Features&nbsp;
</div>
</div> </div>
<div id="burgsBody" class="table"></div> <div id="burgsBody" class="table"></div>
@ -5342,7 +5359,11 @@
<th data-tip="Type group name">Name</th> <th data-tip="Type group name">Name</th>
<th data-tip="Burg preview generator">Preview generator</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 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 biomes">Biomes</th>
<th data-tip="Select allowed states">States</th> <th data-tip="Select allowed states">States</th>
<th data-tip="Select allowed cultures">Cultures</th> <th data-tip="Select allowed cultures">Cultures</th>
@ -5765,6 +5786,83 @@
</div> </div>
</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="options3d" class="dialog stable" style="display: none">
<div id="options3dMesh" 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"> <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 id="tileStatus" style="font-style: italic"></div>
</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>
&nbsp;
<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"> <div id="alert" style="display: none" class="dialog">
<p id="alertMessage">Warning!</p> <p id="alertMessage">Warning!</p>
</div> </div>
@ -7798,7 +7797,9 @@
</symbol> </symbol>
<symbol id="icon-store" viewBox="0 0 616 512"> <symbol id="icon-store" viewBox="0 0 616 512">
<path d="M602 118.6L537.1 15C531.3 5.7 521 0 510 0H106C95 0 84.7 5.7 78.9 15L14 118.6c-33.5 53.5-3.8 127.9 58.8 136.4 4.5.6 9.1.9 13.7.9 29.6 0 55.8-13 73.8-33.1 18 20.1 44.3 33.1 73.8 33.1 29.6 0 55.8-13 73.8-33.1 18 20.1 44.3 33.1 73.8 33.1 29.6 0 55.8-13 73.8-33.1 18.1 20.1 44.3 33.1 73.8 33.1 4.7 0 9.2-.3 13.7-.9 62.8-8.4 92.6-82.8 59-136.4zM529.5 288c-10 0-19.9-1.5-29.5-3.8V384H116v-99.8c-9.6 2.2-19.5 3.8-29.5 3.8-6 0-12.1-.4-18-1.2-5.6-.8-11.1-2.1-16.4-3.6V480c0 17.7 14.3 32 32 32h448c17.7 0 32-14.3 32-32V283.2c-5.4 1.6-10.8 2.9-16.4 3.6-6.1.8-12.1 1.2-18.2 1.2z" /> <path
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>
<symbol id="icon-route" viewBox="0 0 512 512"> <symbol id="icon-route" viewBox="0 0 512 512">
@ -7866,32 +7867,57 @@
<pattern id="pattern_square" width="25" height="25" patternUnits="userSpaceOnUse" fill="none"> <pattern id="pattern_square" width="25" height="25" patternUnits="userSpaceOnUse" fill="none">
<path d="M 25 0 L 0 0 0 25" /> <path d="M 25 0 L 0 0 0 25" />
</pattern> </pattern>
<pattern id="pattern_pointyHex" width="25" height="43.4" patternUnits="userSpaceOnUse" fill="none"> <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" /> <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>
<pattern id="pattern_flatHex" width="43.4" height="25" patternUnits="userSpaceOnUse" fill="none"> <pattern id="pattern_flatHex" width="43.4" height="25" patternUnits="userSpaceOnUse" fill="none">
<path d="M 43.4,0 36.2,12.5 43.4,25 M 21.7,12.5 H 36.2 Z M 0,0 H 14.5 L 21.7,12.5 14.5,25 H 0" /> <path d="M 43.4,0 36.2,12.5 43.4,25 M 21.7,12.5 H 36.2 Z M 0,0 H 14.5 L 21.7,12.5 14.5,25 H 0" />
</pattern> </pattern>
<pattern id="pattern_square45deg" width="35.355" height="35.355" patternUnits="userSpaceOnUse" fill="none"> <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" /> <path d="M 0 0 L 35.355 35.355 M 0 35.355 L 35.355 0" />
</pattern> </pattern>
<pattern id="pattern_squareTruncated" width="25" height="25" patternUnits="userSpaceOnUse" fill="none"> <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>
<pattern id="pattern_squareTetrakis" width="25" height="25" patternUnits="userSpaceOnUse" fill="none"> <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>
<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>
<pattern id="pattern_triangleVertical" width="72.33" height="41.76" patternUnits="userSpaceOnUse" fill="none"> <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>
<pattern id="pattern_trihexagonal" width="25" height="43.4" patternUnits="userSpaceOnUse" fill="none"> <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" /> <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>
<pattern id="pattern_rhombille" width="82.5" height="50" patternUnits="userSpaceOnUse" fill="none"> <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> </pattern>
</g> </g>
@ -8051,26 +8077,25 @@
<script src="libs/jquery-ui.min.js"></script> <script src="libs/jquery-ui.min.js"></script>
<script src="versioning.js"></script> <script src="versioning.js"></script>
<script src="libs/d3.min.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/flatqueue.js"></script>
<script src="libs/delaunator.min.js"></script> <script src="libs/delaunator.min.js"></script>
<script src="libs/indexedDB.js?v=1.99.00"></script> <script src="libs/indexedDB.js?v=1.99.00"></script>
<script src="utils/shorthands.js?v=1.99.00"></script> <script src="utils/shorthands.js?v=1.106.0"></script>
<script src="utils/commonUtils.js?v=1.103.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/arrayUtils.js?v=1.99.00"></script>
<script src="utils/functionUtils.js?v=1.99.00"></script> <script src="utils/functionUtils.js?v=1.99.00"></script>
<script src="utils/colorUtils.js?v=1.99.00"></script> <script src="utils/colorUtils.js?v=1.99.00"></script>
<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/nodeUtils.js?v=1.99.00"></script>
<script src="utils/numberUtils.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/polyfills.js?v=1.99.00"></script>
<script src="utils/probabilityUtils.js?v=1.99.05"></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/languageUtils.js?v=1.99.00"></script>
<script src="utils/unitUtils.js?v=1.99.00"></script> <script src="utils/unitUtils.js?v=1.99.00"></script>
<script src="utils/pathUtils.js?v=1.105.6"></script> <script src="utils/pathUtils.js?v=1.106.0"></script>
<script defer src="utils/debugUtils.js?v=1.99.00"></script> <script defer src="utils/debugUtils.js?v=1.106.0"></script>
<script src="modules/voronoi.js"></script> <script src="modules/voronoi.js"></script>
<script src="config/heightmap-templates.js"></script> <script src="config/heightmap-templates.js"></script>
@ -8078,62 +8103,61 @@
<script src="modules/heightmap-generator.js?v=1.99.00"></script> <script src="modules/heightmap-generator.js?v=1.99.00"></script>
<script src="modules/features.js?v=1.104.0"></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/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/lakes.js?v=1.99.00"></script>
<script src="modules/biomes.js?v=1.99.00"></script> <script src="modules/biomes.js?v=1.99.00"></script>
<script src="modules/names-generator.js?v=1.87.14"></script> <script src="modules/names-generator.js?v=1.106.0"></script>
<script src="modules/cultures-generator.js?v=1.99.05"></script> <script src="modules/cultures-generator.js?v=1.106.0"></script>
<script src="modules/burgs-generator.js?v=1.105.7"></script> <script src="modules/burgs-and-states.js?v=1.106.0"></script>
<script src="modules/states-generator.js?v=1.105.7"></script> <script src="modules/provinces-generator.js?v=1.106.0"></script>
<script src="modules/provinces-generator.js?v=1.104.0"></script> <script src="modules/routes-generator.js?v=1.106.0"></script>
<script src="modules/routes-generator.js?v=1.104.10"></script> <script src="modules/religions-generator.js?v=1.106.0"></script>
<script src="modules/religions-generator.js?v=1.99.05"></script>
<script src="modules/military-generator.js?v=1.104.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/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/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/alea.min.js?v1.105.0"></script>
<script src="libs/polylabel.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/lineclip.min.js?v1.105.0"></script>
<script src="libs/simplify.js?v1.105.6"></script> <script src="libs/simplify.js?v1.105.6"></script>
<script src="modules/fonts.js?v=1.99.03"></script> <script src="modules/fonts.js?v=1.99.03"></script>
<script src="modules/ui/layers.js?v=1.101.00"></script> <script src="modules/ui/layers.js?v=1.106.0"></script>
<script src="modules/ui/measurers.js?v=1.99.00"></script> <script src="modules/ui/measurers.js?v=1.99.00"></script>
<script src="modules/ui/style-presets.js?v=1.100.00"></script> <script src="modules/ui/style-presets.js?v=1.100.00"></script>
<script src="modules/ui/general.js?v=1.100.00"></script> <script src="modules/ui/general.js?v=1.100.00"></script>
<script src="modules/ui/options.js?v=1.105.0"></script> <script src="modules/ui/options.js?v=1.106.0"></script>
<script src="main.js?v=1.105.2"></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/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/style.js?v=1.104.0"></script>
<script defer src="modules/ui/editors.js?v=1.105.2"></script> <script defer src="modules/ui/editors.js?v=1.105.23"></script>
<script defer src="modules/ui/tools.js?v=1.104.0"></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/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/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/provinces-editor.js?v=1.104.0"></script>
<script defer src="modules/ui/biomes-editor.js?v=1.99.05"></script> <script defer src="modules/ui/biomes-editor.js?v=1.99.05"></script>
<script defer src="modules/ui/namesbase-editor.js?v=1.99.00"></script> <script defer src="modules/ui/namesbase-editor.js?v=1.105.11"></script>
<script defer src="modules/ui/elevation-profile.js?v=1.99.00"></script> <script defer src="modules/ui/elevation-profile.js?v=1.99.00"></script>
<script defer src="modules/ui/temperature-graph.js?v=1.99.00"></script> <script defer src="modules/ui/temperature-graph.js?v=1.99.00"></script>
<script defer src="modules/ui/routes-editor.js?v=1.104.3"></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/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/route-group-editor.js?v=1.103.8"></script>
<script defer src="modules/ui/ice-editor.js?v=1.99.00"></script> <script defer src="modules/ui/ice-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/lakes-editor.js?v=1.99.00"></script> <script defer src="modules/ui/lakes-editor.js?v=1.106.0"></script>
<script defer src="modules/ui/coastline-editor.js?v=1.99.00"></script> <script defer src="modules/ui/coastline-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/labels-editor.js?v=1.101.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.99.00"></script> <script defer src="modules/ui/rivers-editor.js?v=1.106.0"></script>
<script defer src="modules/ui/rivers-creator.js?v=1.99.00"></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/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-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/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/units-editor.js?v=1.104.0"></script>
<script defer src="modules/ui/notes-editor.js?v=1.99.06"></script> <script defer src="modules/ui/notes-editor.js?v=1.99.06"></script>
<script defer src="modules/ui/ai-generator.js?v=1.99.09"></script> <script defer src="modules/ui/ai-generator.js?v=1.105.22"></script>
<script defer src="modules/ui/diplomacy-editor.js?v=1.99.00"></script> <script defer src="modules/ui/diplomacy-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/zones-editor.js?v=1.100.00"></script> <script defer src="modules/ui/zones-editor.js?v=1.105.20"></script>
<script defer src="modules/ui/burgs-overview.js?v=1.105.7"></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/routes-overview.js?v=1.104.3"></script>
<script defer src="modules/ui/rivers-overview.js?v=1.99.00"></script> <script defer src="modules/ui/rivers-overview.js?v=1.99.00"></script>
<script defer src="modules/ui/military-overview.js?v=1.99.00"></script> <script defer src="modules/ui/military-overview.js?v=1.99.00"></script>
@ -8144,17 +8168,18 @@
<script defer src="modules/ui/emblems-editor.js?v=1.99.00"></script> <script defer src="modules/ui/emblems-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/markers-editor.js?v=1.99.00"></script> <script defer src="modules/ui/markers-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/3d.js?v=1.99.00"></script> <script defer src="modules/ui/3d.js?v=1.99.00"></script>
<script defer src="modules/ui/submap.js?v=1.99.10"></script> <script defer src="modules/ui/submap-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/ui/hotkeys.js?v=1.104.0"></script>
<script defer src="modules/coa-renderer.js?v=1.99.00"></script> <script defer src="modules/coa-renderer.js?v=1.99.00"></script>
<script defer src="libs/rgbquant.min.js"></script> <script defer src="libs/rgbquant.min.js"></script>
<script defer src="libs/jquery.ui.touch-punch.min.js"></script> <script defer src="libs/jquery.ui.touch-punch.min.js"></script>
<script defer src="modules/io/save.js?v=1.100.00"></script> <script defer src="modules/io/save.js?v=1.100.00"></script>
<script defer src="modules/io/load.js?v=1.105.5"></script> <script defer src="modules/io/load.js?v=1.105.24"></script>
<script defer src="modules/io/cloud.js?v=1.99.00"></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/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-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-heightmap.js?v=1.104.0"></script>
<script defer src="modules/renderers/draw-markers.js?v=1.104.0"></script> <script defer src="modules/renderers/draw-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-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-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-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-labels.js?v=1.104.0"></script>
<script defer src="modules/renderers/draw-burg-icons.js?v=1.104.0"></script> <script defer src="modules/renderers/draw-burg-icons.js?v=1.104.0"></script>
</body> </body>

File diff suppressed because one or more lines are too long

19
main.js
View file

@ -4,7 +4,7 @@
// set debug options // set debug options
const PRODUCTION = location.hostname && location.hostname !== "localhost" && location.hostname !== "127.0.0.1"; 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 INFO = true;
const TIME = true; const TIME = true;
const WARN = true; const WARN = true;
@ -494,14 +494,6 @@ function resetZoom(d = 1000) {
svg.transition().duration(d).call(zoom.transform, d3.zoomIdentity); 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 // active zooming feature
function invokeActiveZooming() { function invokeActiveZooming() {
const isOptimized = shapeRendering.value === "optimizeSpeed"; const isOptimized = shapeRendering.value === "optimizeSpeed";
@ -728,10 +720,11 @@ function setSeed(precreatedSeed) {
function addLakesInDeepDepressions() { function addLakesInDeepDepressions() {
TIME && console.time("addLakesInDeepDepressions"); TIME && console.time("addLakesInDeepDepressions");
const elevationLimit = +byId("lakeElevationLimitOutput").value;
if (elevationLimit === 80) return;
const {cells, features} = grid; const {cells, features} = grid;
const {c, h, b} = cells; const {c, h, b} = cells;
const ELEVATION_LIMIT = +byId("lakeElevationLimitOutput").value;
if (ELEVATION_LIMIT === 80) return;
for (const i of cells.i) { for (const i of cells.i) {
if (b[i] || h[i] < 20) continue; if (b[i] || h[i] < 20) continue;
@ -740,7 +733,7 @@ function addLakesInDeepDepressions() {
if (h[i] > minHeight) continue; if (h[i] > minHeight) continue;
let deep = true; let deep = true;
const threshold = h[i] + ELEVATION_LIMIT; const threshold = h[i] + elevationLimit;
const queue = [i]; const queue = [i];
const checked = []; const checked = [];
checked[i] = true; checked[i] = true;
@ -926,7 +919,7 @@ function calculateTemperatures() {
const [, y] = grid.points[rowCellId]; const [, y] = grid.points[rowCellId];
const rowLatitude = mapCoordinates.latN - (y / graphHeight) * mapCoordinates.latT; // [90; -90] const rowLatitude = mapCoordinates.latN - (y / graphHeight) * mapCoordinates.latT; // [90; -90]
const tempSeaLevel = calculateSeaLevelTemp(rowLatitude); 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++) { for (let cellId = rowCellId; cellId < rowCellId + grid.cellsX; cellId++) {
const tempAltitudeDrop = getAltitudeTemperatureDrop(cells.h[cellId]); const tempAltitudeDrop = getAltitudeTemperatureDrop(cells.h[cellId]);

View file

@ -8,7 +8,10 @@ window.Cultures = (function () {
cells = pack.cells; cells = pack.cells;
const cultureIds = new Uint16Array(cells.i.length); // cell cultures 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 const populated = cells.i.filter(i => cells.s[i]); // populated cells
if (populated.length < count * 25) { if (populated.length < count * 25) {
@ -120,26 +123,26 @@ window.Cultures = (function () {
cultures.forEach(c => (c.base = c.base % nameBases.length)); cultures.forEach(c => (c.base = c.base % nameBases.length));
function selectCultures(culturesNumber) { function selectCultures(culturesNumber) {
let def = getDefault(culturesNumber); let defaultCultures = getDefault(culturesNumber);
const cultures = []; const cultures = [];
pack.cultures?.forEach(function (culture) { pack.cultures?.forEach(function (culture) {
if (culture.lock) cultures.push(culture); if (culture.lock && !culture.removed) cultures.push(culture);
}); });
if (!cultures.length) { if (!cultures.length) {
if (culturesNumber === def.length) return def; if (culturesNumber === defaultCultures.length) return defaultCultures;
if (def.every(d => d.odd === 1)) return def.splice(0, culturesNumber); 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 { do {
rnd = rand(def.length - 1); rnd = rand(defaultCultures.length - 1);
culture = def[rnd]; culture = defaultCultures[rnd];
i++; i++;
} while (i < 200 && !P(culture.odd)); } while (i < 200 && !P(culture.odd));
cultures.push(culture); cultures.push(culture);
def.splice(rnd, 1); defaultCultures.splice(rnd, 1);
} }
return cultures; return cultures;
} }
@ -515,7 +518,7 @@ window.Cultures = (function () {
TIME && console.time("expandCultures"); TIME && console.time("expandCultures");
const {cells, cultures} = pack; const {cells, cultures} = pack;
const queue = new PriorityQueue({comparator: (a, b) => a.priority - b.priority}); const queue = new FlatQueue();
const cost = []; const cost = [];
const neutralRate = byId("neutralRate")?.valueAsNumber || 1; const neutralRate = byId("neutralRate")?.valueAsNumber || 1;
@ -535,11 +538,11 @@ window.Cultures = (function () {
for (const culture of cultures) { for (const culture of cultures) {
if (!culture.i || culture.removed || culture.lock) continue; 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) { while (queue.length) {
const {cellId, priority, cultureId} = queue.dequeue(); const {cellId, priority, cultureId} = queue.pop();
const {type, expansionism} = cultures[cultureId]; const {type, expansionism} = cultures[cultureId];
cells.c[cellId].forEach(neibCellId => { cells.c[cellId].forEach(neibCellId => {
@ -563,7 +566,7 @@ window.Cultures = (function () {
if (!cost[neibCellId] || totalCost < cost[neibCellId]) { if (!cost[neibCellId] || totalCost < cost[neibCellId]) {
if (cells.pop[neibCellId] > 0) cells.culture[neibCellId] = cultureId; // assign culture to populated cell if (cells.pop[neibCellId] > 0) cells.culture[neibCellId] = cultureId; // assign culture to populated cell
cost[neibCellId] = totalCost; cost[neibCellId] = totalCost;
queue.queue({cellId: neibCellId, cultureId, priority: totalCost}); queue.push({cellId: neibCellId, cultureId, priority: totalCost}, totalCost);
} }
}); });
} }

View file

@ -943,7 +943,21 @@ export function resolveVersionConflicts(mapVersion) {
viewbox.select("#coastline").selectAll("path, use").remove(); viewbox.select("#coastline").selectAll("path, use").remove();
drawFeatures(); 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(); icons.selectAll("circle, use").remove();
const groups = Array.from(document.querySelectorAll("#burgIcons > g")).map(g => g.id); const groups = Array.from(document.querySelectorAll("#burgIcons > g")).map(g => g.id);

View file

@ -266,6 +266,7 @@ function getTypeOptions(type) {
function getBaseOptions(base) { function getBaseOptions(base) {
let options = ""; let options = "";
nameBases.forEach((n, i) => (options += `<option ${base === i ? "selected" : ""} value="${i}">${n.name}</option>`)); 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; return options;
} }
@ -344,10 +345,13 @@ function cultureChangeName() {
} }
function cultureRegenerateName() { function cultureRegenerateName() {
const culture = +this.parentNode.dataset.id; const cultureId = +this.parentNode.dataset.id;
const name = Names.getCultureShort(culture); 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; this.parentNode.querySelector("input.cultureName").value = name;
pack.cultures[culture].name = name; pack.cultures[cultureId].name = name;
} }
function cultureChangeExpansionism() { function cultureChangeExpansionism() {
@ -493,12 +497,15 @@ function cultureRegenerateBurgs() {
if (customization === 4) return; if (customization === 4) return;
const cultureId = +this.parentNode.dataset.id; const cultureId = +this.parentNode.dataset.id;
const cBurgs = pack.burgs.filter(b => b.culture === cultureId && !b.lock); const base = pack.cultures[cultureId].base;
cBurgs.forEach(b => { 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); b.name = Names.getCulture(cultureId);
labels.select("[data-id='" + b.i + "']").text(b.name); 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) { function removeCulture(cultureId) {
@ -848,14 +855,15 @@ async function uploadCulturesData() {
this.value = ""; this.value = "";
const csv = await file.text(); const csv = await file.text();
const data = d3.csvParse(csv, d => ({ const data = d3.csvParse(csv, d => ({
i: +d.Id,
name: d.Name, name: d.Name,
i: +d.Id,
color: d.Color, color: d.Color,
expansionism: +d.Expansionism, expansionism: +d.Expansionism,
type: d.Type, type: d.Type,
population: +d.Population, population: +d.Population,
emblemsShape: d["Emblems Shape"], emblemsShape: d["Emblems Shape"],
origins: d.Origins origins: d.Origins,
namesbase: d.Namesbase
})); }));
const {cultures, cells} = pack; const {cultures, cells} = pack;
@ -882,7 +890,7 @@ async function uploadCulturesData() {
culture.i culture.i
); );
} else { } 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); cultures.push(current);
} }
@ -902,6 +910,10 @@ async function uploadCulturesData() {
else current.type = "Generic"; 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) { function restoreOrigins(originsString) {
const originNames = originsString const originNames = originsString
.replaceAll('"', "") .replaceAll('"', "")
@ -917,12 +929,6 @@ async function uploadCulturesData() {
current.origins = originIds.filter(id => id !== null); current.origins = originIds.filter(id => id !== null);
if (!current.origins.length) current.origins = [0]; 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)); cultures.filter(c => c.removed).forEach(c => removeCulture(c.i));

View file

@ -583,4 +583,10 @@ James Benware
FortunesFaded FortunesFaded
breadsticks breadsticks
Murderbits Murderbits
Ben Jones`; Ben Jones
Marco Faltracco
L
silentArtifact
Keith Potter
Morgan Gilbert
Alengork Gamer`;

View file

@ -60,7 +60,7 @@ window.Cloud = (function () {
async save(fileName, contents) { async save(fileName, contents) {
const resp = await this.call("filesUpload", {path: "/" + 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; return true;
}, },
@ -104,7 +104,7 @@ window.Cloud = (function () {
// Callback function for auth window // Callback function for auth window
async setDropBoxToken(token) { async setDropBoxToken(token) {
DEBUG && console.info("Access token:", token); DEBUG.cloud && console.info("Access token:", token);
setToken(this.name, token); setToken(this.name, token);
await this.connect(token); await this.connect(token);
this.authWindow.close(); this.authWindow.close();
@ -131,7 +131,7 @@ window.Cloud = (function () {
allow_download: true allow_download: true
}; };
const resp = await this.call("sharingCreateSharedLinkWithSettings", {path, settings}); 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; return resp.result.url;
} }
}; };

View file

@ -13,7 +13,7 @@ async function quickLoad() {
async function loadFromDropbox() { async function loadFromDropbox() {
const mapPath = byId("loadFromDropboxSelect")?.value; 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); const blob = await Cloud.providers.dropbox.load(mapPath);
uploadMap(blob); uploadMap(blob);
} }
@ -96,6 +96,7 @@ function showUploadErrorMessage(error, URL, random) {
title: "Loading error", title: "Loading error",
width: "32em", width: "32em",
buttons: { buttons: {
"Clear cache": () => cleanupData(),
OK: function () { OK: function () {
$(this).dialog("close"); $(this).dialog("close");
} }
@ -152,11 +153,21 @@ async function uncompress(compressedData) {
async function parseLoadedResult(result) { async function parseLoadedResult(result) {
try { try {
const resultAsString = new TextDecoder().decode(result); const resultAsString = new TextDecoder().decode(result);
// data can be in FMG internal format or base64 encoded // data can be in FMG internal format or base64 encoded
const isDelimited = resultAsString.substring(0, 10).includes("|"); 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] || ""); const mapVersion = parseMapVersion(mapData[0].split("|")[0] || mapData[0] || "");
return {mapData, mapVersion}; return {mapData, mapVersion};
@ -195,6 +206,7 @@ function showUploadMessage(type, mapData, mapVersion) {
$("#alert").dialog({ $("#alert").dialog({
title, title,
buttons: { buttons: {
"Clear cache": () => cleanupData(),
OK: function () { OK: function () {
$(this).dialog("close"); $(this).dialog("close");
} }
@ -459,7 +471,7 @@ async function parseLoadedData(data, mapVersion) {
{ {
// dynamically import and run auto-update script // 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); resolveVersionConflicts(mapVersion);
} }
@ -735,6 +747,7 @@ async function parseLoadedData(data, mapVersion) {
title: "Loading error", title: "Loading error",
maxWidth: "50em", maxWidth: "50em",
buttons: { buttons: {
"Clear cache": () => cleanupData(),
"Select file": function () { "Select file": function () {
$(this).dialog("close"); $(this).dialog("close");
mapToLoad.click(); mapToLoad.click();

View file

@ -48,18 +48,28 @@ window.Names = (function () {
return chain; return chain;
}; };
// update chain for specific base const updateChain = i => {
const updateChain = i => (chains[i] = nameBases[i] || nameBases[i].b ? calculateChain(nameBases[i].b) : null); chains[i] = nameBases[i]?.b ? calculateChain(nameBases[i].b) : null;
};
// update chains for all used bases const clearChains = () => {
const clearChains = () => (chains = []); chains = [];
};
// generate name using Markov's chain // generate name using Markov's chain
const getBase = function (base, min, max, dupl) { const getBase = function (base, min, max, dupl) {
if (base === undefined) { if (base === undefined) return ERROR && console.error("Please define a base");
ERROR && console.error("Please define a base");
return; 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); if (!chains[base]) updateChain(base);
const data = chains[base]; const data = chains[base];
@ -141,16 +151,8 @@ window.Names = (function () {
// generate short name for base // generate short name for base
const getBaseShort = function (base) { const getBaseShort = function (base) {
if (nameBases[base] === undefined) { const min = nameBases[base] ? nameBases[base].min - 1 : null;
tip( const max = min ? Math.max(nameBases[base].max - 2, min) : null;
`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);
return getBase(base, min, max, "", 0); return getBase(base, min, max, "", 0);
}; };

View file

@ -77,18 +77,18 @@ window.Provinces = (function () {
}); });
// expand generated provinces // expand generated provinces
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); const queue = new FlatQueue();
const cost = []; const cost = [];
provinces.forEach(p => { provinces.forEach(p => {
if (!p.i || p.removed || isProvinceLocked(p)) return; if (!p.i || p.removed || isProvinceLocked(p)) return;
provinceIds[p.center] = p.i; 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; cost[p.center] = 1;
}); });
while (queue.length) { while (queue.length) {
const {e, p, province, state} = queue.dequeue(); const {e, p, province, state} = queue.pop();
cells.c[e].forEach(e => { cells.c[e].forEach(e => {
if (isProvinceCellLocked(e)) return; // do not overwrite cell of locked provinces 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 (!cost[e] || totalCost < cost[e]) {
if (land) provinceIds[e] = province; // assign province to a cell if (land) provinceIds[e] = province; // assign province to a cell
cost[e] = totalCost; 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 // expand province
const cost = []; const cost = [];
cost[center] = 1; cost[center] = 1;
queue.queue({e: center, p: 0}); queue.push({e: center, p: 0}, 0);
while (queue.length) { while (queue.length) {
const {e, p} = queue.dequeue(); const {e, p} = queue.pop();
cells.c[e].forEach(nextCellId => { cells.c[e].forEach(nextCellId => {
if (provinceIds[nextCellId]) return; if (provinceIds[nextCellId]) return;
@ -173,7 +173,7 @@ window.Provinces = (function () {
if (!cost[nextCellId] || totalCost < cost[nextCellId]) { if (!cost[nextCellId] || totalCost < cost[nextCellId]) {
if (land && cells.state[nextCellId] === s.i) provinceIds[nextCellId] = provinceId; // assign province to a cell if (land && cells.state[nextCellId] === s.i) provinceIds[nextCellId] = provinceId; // assign province to a cell
cost[nextCellId] = totalCost; 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 // check if there is a land way within the same state between two cells
function isPassable(from, to) { function isPassable(from, to) {
if (cells.f[from] !== cells.f[to]) return false; // on different islands if (cells.f[from] !== cells.f[to]) return false; // on different islands
const queue = [from], const passableQueue = [from],
used = new Uint8Array(cells.i.length), used = new Uint8Array(cells.i.length),
state = cells.state[from]; state = cells.state[from];
while (queue.length) { while (passableQueue.length) {
const current = queue.pop(); const current = passableQueue.pop();
if (current === to) return true; // way is found if (current === to) return true; // way is found
cells.c[current].forEach(c => { cells.c[current].forEach(c => {
if (used[c] || cells.h[c] < 20 || cells.state[c] !== state) return; if (used[c] || cells.h[c] < 20 || cells.state[c] !== state) return;
queue.push(c); passableQueue.push(c);
used[c] = 1; used[c] = 1;
}); });
} }

View file

@ -695,7 +695,7 @@ window.Religions = (function () {
const {cells, routes} = pack; const {cells, routes} = pack;
const religionIds = spreadFolkReligions(religions); const religionIds = spreadFolkReligions(religions);
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); const queue = new FlatQueue();
const cost = []; const cost = [];
// limit cost for organized religions growth // limit cost for organized religions growth
@ -705,14 +705,14 @@ window.Religions = (function () {
.filter(r => r.i && !r.lock && r.type !== "Folk" && !r.removed) .filter(r => r.i && !r.lock && r.type !== "Folk" && !r.removed)
.forEach(r => { .forEach(r => {
religionIds[r.center] = r.i; 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; cost[r.center] = 1;
}); });
const religionsMap = new Map(religions.map(r => [r.i, r])); const religionsMap = new Map(religions.map(r => [r.i, r]));
while (queue.length) { 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); const {culture, expansion, expansionism} = religionsMap.get(r);
cells.c[cellId].forEach(nextCell => { cells.c[cellId].forEach(nextCell => {
@ -732,7 +732,7 @@ window.Religions = (function () {
if (cells.culture[nextCell]) religionIds[nextCell] = r; // assign religion to cell if (cells.culture[nextCell]) religionIds[nextCell] = r; // assign religion to cell
cost[nextCell] = totalCost; cost[nextCell] = totalCost;
queue.queue({e: nextCell, p: totalCost, r, s: state}); queue.push({e: nextCell, p: totalCost, r, s: state}, totalCost);
} }
}); });
} }

View file

@ -55,7 +55,7 @@ function getFeaturePath(feature) {
const clippedPoints = clipPoly(simplifiedPoints, 1); const clippedPoints = clipPoly(simplifiedPoints, 1);
const lineGen = d3.line().curve(d3.curveBasisClosed); const lineGen = d3.line().curve(d3.curveBasisClosed);
const path = round(lineGen(clippedPoints)); const path = round(lineGen(clippedPoints)) + "Z";
return path; return path;
} }

View file

@ -14,11 +14,11 @@ function drawStateLabels(list) {
// increase step to 15 or 30 to make it faster and more horyzontal // increase step to 15 or 30 to make it faster and more horyzontal
// decrease step to 5 to improve accuracy // decrease step to 5 to improve accuracy
const ANGLE_STEP = 9; const ANGLE_STEP = 9;
const raycast = precalculateAngles(ANGLE_STEP); const angles = precalculateAngles(ANGLE_STEP);
const INITIAL_DISTANCE = 10; const LENGTH_START = 5;
const DISTANCE_STEP = 15; const LENGTH_STEP = 5;
const MAX_ITERATIONS = 100; const LENGTH_MAX = 300;
const labelPaths = getLabelPaths(); const labelPaths = getLabelPaths();
const letterLength = checkExampleLetterLength(); const letterLength = checkExampleLetterLength();
@ -35,87 +35,27 @@ function drawStateLabels(list) {
if (list && !list.includes(state.i)) continue; if (list && !list.includes(state.i)) continue;
const offset = getOffsetWidth(state.cells); const offset = getOffsetWidth(state.cells);
const maxLakeSize = state.cells / 50; const maxLakeSize = state.cells / 20;
const [x0, y0] = state.pole; const [x0, y0] = state.pole;
const offsetPoints = new Map( const rays = angles.map(({angle, dx, dy}) => {
(offset ? raycast : []).map(({angle, x: x1, y: y1}) => { const {length, x, y} = raycast({stateId: state.i, x0, y0, dx, dy, maxLakeSize, offset});
const [x, y] = [x0 + offset * x1, y0 + offset * y1]; return {angle, length, x, y};
return [angle, {x, y}]; });
}) const [ray1, ray2] = findBestRayPair(rays);
);
const distances = raycast.map(({angle, x: dx, y: dy, modifier}) => { const pathPoints = [[ray1.x, ray1.y], state.pole, [ray2.x, ray2.y]];
let distanceMin; if (ray1.x > ray2.x) pathPoints.reverse();
const distance1 = getMaxDistance(state.i, {x: x0, y: y0}, dx, dy, maxLakeSize);
if (offset) { if (DEBUG.stateLabels) {
const point2 = offsetPoints.get(angle - 90 < 0 ? angle + 270 : angle - 90); drawPoint(state.pole, {color: "black", radius: 1});
const distance2 = getMaxDistance(state.i, point2, dx, dy, maxLakeSize); drawPath(pathPoints, {color: "black", width: 0.2});
const point3 = offsetPoints.get(angle + 90 >= 360 ? angle - 270 : angle + 90);
const distance3 = getMaxDistance(state.i, point3, dx, dy, maxLakeSize);
distanceMin = Math.min(distance1, distance2, distance3);
} else {
distanceMin = distance1;
} }
const [x, y] = [x0 + distanceMin * dx, y0 + distanceMin * dy];
return {angle, distance: distanceMin * modifier, x, y};
});
const {
angle,
x: x1,
y: y1
} = distances.reduce(
(acc, {angle, distance, x, y}) => {
if (distance > acc.distance) return {angle, distance, x, y};
return acc;
},
{angle: 0, distance: 0, x: 0, y: 0}
);
const oppositeAngle = angle >= 180 ? angle - 180 : angle + 180;
const {x: x2, y: y2} = distances.reduce(
(acc, {angle, distance, x, y}) => {
const angleDif = getAnglesDif(angle, oppositeAngle);
const score = distance * getAngleModifier(angleDif);
if (score > acc.score) return {angle, score, x, y};
return acc;
},
{angle: 0, score: 0, x: 0, y: 0}
);
const pathPoints = [[x1, y1], state.pole, [x2, y2]];
if (x1 > x2) pathPoints.reverse();
labelPaths.push([state.i, pathPoints]); labelPaths.push([state.i, pathPoints]);
} }
return labelPaths; 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() { function checkExampleLetterLength() {
@ -129,7 +69,7 @@ function drawStateLabels(list) {
function drawLabelPath(letterLength) { function drawLabelPath(letterLength) {
const mode = options.stateLabelsMode || "auto"; 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 textGroup = d3.select("g#labels > g#states");
const pathGroup = d3.select("defs > g#deftemp > g#textPaths"); 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; const text = pathLength > state.fullName.length * 1.8 ? state.fullName : state.name;
textElement.innerHTML = `<tspan x="0">${text}</tspan>`; 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 + "%"); textElement.setAttribute("font-size", correctedRatio + "%");
} }
} }
// point offset to reduce label overlap with state borders
function getOffsetWidth(cellsNumber) { function getOffsetWidth(cellsNumber) {
if (cellsNumber < 80) return 0; if (cellsNumber < 40) return 0;
if (cellsNumber < 140) return 5; if (cellsNumber < 200) return 5;
if (cellsNumber < 200) return 15; return 10;
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
} }
function precalculateAngles(step) { function precalculateAngles(step) {
@ -228,38 +148,136 @@ function drawStateLabels(list) {
const RAD = Math.PI / 180; const RAD = Math.PI / 180;
for (let angle = 0; angle < 360; angle += step) { for (let angle = 0; angle < 360; angle += step) {
const x = Math.cos(angle * RAD); const dx = Math.cos(angle * RAD);
const y = Math.sin(angle * RAD); const dy = Math.sin(angle * RAD);
const angleDif = 90 - Math.abs((angle % 180) - 90); angles.push({angle, dx, dy});
const modifier = 1 - angleDif / 120; // [0.25, 1]
angles.push({angle, modifier, x, y});
} }
return angles; 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) { function getLinesAndRatio(mode, name, fullName, pathLength) {
// short name if (mode === "short") return getShortOneLine();
if (mode === "short" || (mode === "auto" && pathLength <= name.length)) { if (pathLength > fullName.length * 2) return getFullOneLine();
const lines = splitInTwo(name); return getFullTwoLines();
const longestLineLength = d3.max(lines.map(({length}) => length));
const ratio = pathLength / longestLineLength; function getShortOneLine() {
return [lines, minmax(rn(ratio * 60), 50, 150)]; const ratio = pathLength / name.length;
return [[name], minmax(rn(ratio * 60), 50, 150)];
} }
// full name: one line function getFullOneLine() {
if (pathLength > fullName.length * 2) { const ratio = pathLength / fullName.length;
const lines = [fullName]; return [[fullName], minmax(rn(ratio * 70), 70, 170)];
const ratio = pathLength / lines[0].length;
return [lines, minmax(rn(ratio * 70), 70, 170)];
} }
// full name: two lines function getFullTwoLines() {
const lines = splitInTwo(fullName); const lines = splitInTwo(fullName);
const longestLineLength = d3.max(lines.map(({length}) => length)); const longestLineLength = d3.max(lines.map(({length}) => length));
const ratio = pathLength / longestLineLength; const ratio = pathLength / longestLineLength;
return [lines, minmax(rn(ratio * 60), 70, 150)]; 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 // check whether multi-lined label is mostly inside the state. If no, replace it with short name label
function checkIfInsideState(textElement, angleRad, halfwidth, halfheight, stateIds, stateId) { function checkIfInsideState(textElement, angleRad, halfwidth, halfheight, stateIds, stateId) {

365
modules/resample.js Normal file
View 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};
})();

View file

@ -190,7 +190,15 @@ window.Rivers = (function () {
const meanderedPoints = addMeandering(riverCells); const meanderedPoints = addMeandering(riverCells);
const discharge = cells.fl[mouth]; // m3 in second const discharge = cells.fl[mouth]; // m3 in second
const length = getApproximateLength(meanderedPoints); 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({ pack.rivers.push({
i: riverId, i: riverId,
@ -200,7 +208,7 @@ window.Rivers = (function () {
length, length,
width, width,
widthFactor, widthFactor,
sourceWidth: 0, sourceWidth,
parent, parent,
cells: riverCells cells: riverCells
}); });
@ -306,59 +314,49 @@ window.Rivers = (function () {
// add points at 1/3 and 2/3 of a line between adjacents river cells // 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 addMeandering = function (riverCells, riverPoints = null, meandering = 0.5) {
const {fl, conf, h} = pack.cells; const {fl, h} = pack.cells;
const meandered = []; const meandered = [];
const lastStep = riverCells.length - 1; const lastStep = riverCells.length - 1;
const points = getRiverPoints(riverCells, riverPoints); const points = getRiverPoints(riverCells, riverPoints);
let step = h[riverCells[0]] < 20 ? 1 : 10; 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++) { for (let i = 0; i <= lastStep; i++, step++) {
const cell = riverCells[i]; const cell = riverCells[i];
const isLastCell = i === lastStep; const isLastCell = i === lastStep;
const [x1, y1] = points[i]; 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; if (isLastCell) break;
const nextCell = riverCells[i + 1]; const nextCell = riverCells[i + 1];
const [x2, y2] = points[i + 1]; const [x2, y2] = points[i + 1];
if (nextCell === -1) { if (nextCell === -1) {
meandered.push([x2, y2, fluxPrev]); meandered.push([x2, y2, fl[cell]]);
break; break;
} }
const dist2 = (x2 - x1) ** 2 + (y2 - y1) ** 2; // square distance between cells const dist2 = (x2 - x1) ** 2 + (y2 - y1) ** 2; // square distance between cells
if (dist2 <= 25 && riverCells.length >= 6) continue; 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 meander = meandering + 1 / step + Math.max(meandering - step / 100, 0);
const angle = Math.atan2(y2 - y1, x2 - x1); const angle = Math.atan2(y2 - y1, x2 - x1);
const sinMeander = Math.sin(angle) * meander; const sinMeander = Math.sin(angle) * meander;
const cosMeander = Math.cos(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 // 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 p1x = (x1 * 2 + x2) / 3 + -sinMeander;
const p1y = (y1 * 2 + y2) / 3 + cosMeander; const p1y = (y1 * 2 + y2) / 3 + cosMeander;
const p2x = (x1 + x2 * 2) / 3 + sinMeander / 2; const p2x = (x1 + x2 * 2) / 3 + sinMeander / 2;
const p2y = (y1 + y2 * 2) / 3 - cosMeander / 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, 0], [p2x, p2y, 0]);
meandered.push([p1x, p1y, p1fl], [p2x, p2y, p2fl]);
} else if (dist2 > 25 || riverCells.length < 6) { } else if (dist2 > 25 || riverCells.length < 6) {
// if dist is medium or river is small add 1 extra middlepoint // if dist is medium or river is small add 1 extra middlepoint
const p1x = (x1 + x2) / 2 + -sinMeander; const p1x = (x1 + x2) / 2 + -sinMeander;
const p1y = (y1 + y2) / 2 + cosMeander; const p1y = (y1 + y2) / 2 + cosMeander;
const p1fl = keepInitialFlux ? flux1 : (flux1 + flux2) / 2; meandered.push([p1x, p1y, 0]);
meandered.push([p1x, p1y, p1fl]);
} }
} }
@ -385,29 +383,36 @@ window.Rivers = (function () {
}; };
const FLUX_FACTOR = 500; const FLUX_FACTOR = 500;
const MAX_FLUX_WIDTH = 2; const MAX_FLUX_WIDTH = 1;
const LENGTH_FACTOR = 200; 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 LENGTH_PROGRESSION = [1, 1, 2, 3, 5, 8, 13, 21, 34].map(n => n / LENGTH_FACTOR);
const MAX_PROGRESSION = last(LENGTH_PROGRESSION); const MAX_PROGRESSION = last(LENGTH_PROGRESSION);
const getOffset = (flux, pointNumber, widthFactor, startingWidth = 0) => { const getOffset = ({flux, pointIndex, widthFactor, startingWidth}) => {
const fluxWidth = Math.min(flux ** 0.9 / FLUX_FACTOR, MAX_FLUX_WIDTH); if (pointIndex === 0) return startingWidth;
const lengthWidth = pointNumber * STEP_WIDTH + (LENGTH_PROGRESSION[pointNumber] || MAX_PROGRESSION);
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; 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) // 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 riverPointsLeft = [];
const riverPointsRight = []; const riverPointsRight = [];
let flux = 0;
for (let p = 0; p < points.length; p++) { for (let pointIndex = 0; pointIndex < points.length; pointIndex++) {
const [x0, y0] = points[p - 1] || points[p]; const [x0, y0] = points[pointIndex - 1] || points[pointIndex];
const [x1, y1, flux] = points[p]; const [x1, y1, pointFlux] = points[pointIndex];
const [x2, y2] = points[p + 1] || points[p]; 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 angle = Math.atan2(y0 - y2, x0 - x2);
const sinOffset = Math.sin(angle) * offset; const sinOffset = Math.sin(angle) * offset;
const cosOffset = Math.cos(angle) * offset; const cosOffset = Math.cos(angle) * offset;
@ -507,6 +512,7 @@ window.Rivers = (function () {
getBasin, getBasin,
getWidth, getWidth,
getOffset, getOffset,
getSourceWidth,
getApproximateLength, getApproximateLength,
getRiverPoints, getRiverPoints,
remove, remove,

View file

@ -1,6 +1,15 @@
const ROUTES_SHARP_ANGLE = 135; const ROUTES_SHARP_ANGLE = 135;
const ROUTES_VERY_SHARP_ANGLE = 115; 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 () { window.Routes = (function () {
function generate(lockedRoutes = []) { function generate(lockedRoutes = []) {
const {capitalsByFeature, burgsByFeature, portsByFeature} = sortBurgsByFeature(pack.burgs); const {capitalsByFeature, burgsByFeature, portsByFeature} = sortBurgsByFeature(pack.burgs);
@ -118,10 +127,9 @@ window.Routes = (function () {
} }
function findPathSegments({isWater, connections, start, exit}) { function findPathSegments({isWater, connections, start, exit}) {
const from = findPath(isWater, start, exit, connections); const getCost = createCostEvaluator({isWater, connections});
if (!from) return []; const pathCells = findPath(start, current => current === exit, getCost);
if (!pathCells) return [];
const pathCells = restorePath(start, exit, from);
const segments = getRouteSegments(pathCells, connections); const segments = getRouteSegments(pathCells, connections);
return segments; return segments;
} }
@ -172,6 +180,39 @@ window.Routes = (function () {
return routesMerged > 1 ? mergeRoutes(routes) : routes; return routesMerged > 1 ? mergeRoutes(routes) : routes;
} }
}
function createCostEvaluator({isWater, connections}) {
return isWater ? getWaterPathCost : getLandPathCost;
function getLandPathCost(current, next) {
if (pack.cells.h[next] < 20) return Infinity; // ignore water cells
const habitability = biomesData.habitability[pack.cells.biome[next]];
if (!habitability) return Infinity; // inhabitable cells are not passable (e.g. glacier)
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;
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) { function buildLinks(routes) {
const links = {}; const links = {};
@ -195,7 +236,6 @@ window.Routes = (function () {
return links; return links;
} }
}
function preparePointsArray() { function preparePointsArray() {
const {cells, burgs} = pack; const {cells, burgs} = pack;
@ -249,109 +289,6 @@ window.Routes = (function () {
return data; // [[x, y, cell], [x, y, cell]]; 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) { function getRouteSegments(pathCells, connections) {
const segments = []; const segments = [];
let segment = []; let segment = [];
@ -422,21 +359,16 @@ window.Routes = (function () {
// connect cell with routes system by land // connect cell with routes system by land
function connect(cellId) { 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 pointsArray = preparePointsArray();
const points = getPoints("trails", pathCells, pointsArray); const points = getPoints("trails", pathCells, pointsArray);
const feature = cells.f[cellId]; const feature = pack.cells.f[cellId];
const routeId = getNextId(); const routeId = getNextId();
const newRoute = {i: routeId, group: "trails", feature, points}; const newRoute = {i: routeId, group: "trails", feature, points};
routes.push(newRoute); pack.routes.push(newRoute);
for (let i = 0; i < pathCells.length; i++) { for (let i = 0; i < pathCells.length; i++) {
const cellId = pathCells[i]; const cellId = pathCells[i];
@ -446,43 +378,6 @@ window.Routes = (function () {
return newRoute; 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) { function addConnection(from, to, routeId) {
const routes = pack.cells.routes; const routes = pack.cells.routes;
@ -763,6 +658,7 @@ window.Routes = (function () {
return { return {
generate, generate,
buildLinks,
connect, connect,
isConnected, isConnected,
areConnected, areConnected,

View file

@ -50,7 +50,8 @@ window.States = (() => {
const {cells, states, cultures, burgs} = pack; const {cells, states, cultures, burgs} = pack;
cells.state = cells.state || new Uint16Array(cells.i.length); 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 cost = [];
const globalGrowthRate = byId("growthRate").valueAsNumber || 1; const globalGrowthRate = byId("growthRate").valueAsNumber || 1;
@ -71,12 +72,13 @@ window.States = (() => {
cells.state[capitalCell] = state.i; cells.state[capitalCell] = state.i;
const cultureCenter = cultures[state.culture].center; const cultureCenter = cultures[state.culture].center;
const b = cells.biome[cultureCenter]; // state native biome 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; cost[state.center] = 1;
} }
while (queue.length) { while (queue.length) {
const next = queue.dequeue(); const next = queue.pop();
const {e, p, s, b} = next; const {e, p, s, b} = next;
const {type, culture} = states[s]; const {type, culture} = states[s];
@ -99,7 +101,7 @@ window.States = (() => {
if (!cost[e] || totalCost < cost[e]) { if (!cost[e] || totalCost < cost[e]) {
if (cells.h[e] >= 20) cells.state[e] = s; // assign state to cell if (cells.h[e] >= 20) cells.state[e] = s; // assign state to cell
cost[e] = totalCost; cost[e] = totalCost;
queue.queue({e, p: totalCost, s, b}); queue.push({e, p: totalCost, s, b}, totalCost);
} }
}); });
} }

View file

@ -31,7 +31,6 @@ window.Submap = (function () {
seed = parentMap.seed; seed = parentMap.seed;
Math.random = aleaPRNG(seed); Math.random = aleaPRNG(seed);
INFO && console.group("SubMap with seed: " + seed); INFO && console.group("SubMap with seed: " + seed);
DEBUG && console.info("Using Options:", options);
applyGraphSize(); applyGraphSize();
grid = generateGrid(); grid = generateGrid();
@ -373,7 +372,7 @@ window.Submap = (function () {
b.removed = true; b.removed = true;
return; 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]; [b.x, b.y] = b.port ? getCloseToEdgePoint(newCell, neighbor) : cells.p[newCell];
if (b.port) b.port = cells.f[neighbor]; // copy feature number if (b.port) b.port = cells.f[neighbor]; // copy feature number
b.cell = newCell; b.cell = newCell;

View file

@ -1,9 +1,115 @@
"use strict"; "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."; 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(); updateValues();
$("#aiGenerator").dialog({ $("#aiGenerator").dialog({
@ -26,86 +132,56 @@ function geneateWithAi(defaultPrompt, onApply) {
} }
}); });
if (modules.geneateWithAi) return; if (modules.generateWithAi) return;
modules.geneateWithAi = true; modules.generateWithAi = true;
byId("aiGeneratorKeyHelp").on("click", function (e) {
const model = byId("aiGeneratorModel").value;
const provider = MODELS[model];
openURL(PROVIDERS[provider].keyLink);
});
function updateValues() { function updateValues() {
byId("aiGeneratorResult").value = ""; byId("aiGeneratorResult").value = "";
byId("aiGeneratorPrompt").value = defaultPrompt; 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"); const select = byId("aiGeneratorModel");
select.options.length = 0; select.options.length = 0;
GPT_MODELS.forEach(model => select.options.add(new Option(model, model))); Object.keys(MODELS).forEach(model => select.options.add(new Option(model, model)));
select.value = localStorage.getItem("fmg-ai-model") || GPT_MODELS[0]; 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) { async function generate(button) {
const key = byId("aiGeneratorKey").value; const key = byId("aiGeneratorKey").value;
if (!key) return tip("Please enter an OpenAI API key", true, "error", 4000); if (!key) return tip("Please enter an API key", true, "error", 4000);
localStorage.setItem("fmg-ai-kl", key);
const model = byId("aiGeneratorModel").value; const model = byId("aiGeneratorModel").value;
if (!model) return tip("Please select a model", true, "error", 4000); if (!model) return tip("Please select a model", true, "error", 4000);
localStorage.setItem("fmg-ai-model", model); localStorage.setItem("fmg-ai-model", model);
const provider = MODELS[model];
localStorage.setItem(`fmg-ai-kl-${provider}`, key);
const prompt = byId("aiGeneratorPrompt").value; const prompt = byId("aiGeneratorPrompt").value;
if (!prompt) return tip("Please enter a prompt", true, "error", 4000); 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 { try {
button.disabled = true; button.disabled = true;
const resultArea = byId("aiGeneratorResult"); const resultArea = byId("aiGeneratorResult");
resultArea.value = "";
resultArea.disabled = true; resultArea.disabled = true;
resultArea.value = "";
const onContent = content => (resultArea.value += content);
const response = await fetch("https://api.openai.com/v1/chat/completions", { await PROVIDERS[provider].generate({key, model, prompt, temperature, onContent});
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];
}
} catch (error) { } catch (error) {
return tip(error.message, true, "error", 4000); return tip(error.message, true, "error", 4000);
} finally { } finally {

View file

@ -1066,7 +1066,7 @@ async function editStates() {
async function editCultures() { async function editCultures() {
if (customization) return; 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(); Editor.open();
} }

View file

@ -122,7 +122,7 @@ function editLabel() {
function redrawLabelPath() { function redrawLabelPath() {
const path = byId("textPath_" + elSelected.attr("id")); const path = byId("textPath_" + elSelected.attr("id"));
lineGen.curve(d3.curveBundle.beta(1)); lineGen.curve(d3.curveNatural);
const points = []; const points = [];
debug debug
.select("#controlPoints") .select("#controlPoints")

View file

@ -15,7 +15,7 @@ function editLake() {
debug.append("g").attr("id", "vertices"); debug.append("g").attr("id", "vertices");
elSelected = d3.select(node); elSelected = d3.select(node);
updateLakeValues(); updateLakeValues();
selectLakeGroup(node); selectLakeGroup();
drawLakeVertices(); drawLakeVertices();
viewbox.on("touchmove mousemove", null); viewbox.on("touchmove mousemove", null);
@ -140,13 +140,13 @@ function editLake() {
lake.name = lakeName.value = Names.getBase(rand(nameBases.length - 1)); lake.name = lakeName.value = Names.getBase(rand(nameBases.length - 1));
} }
function selectLakeGroup(node) { function selectLakeGroup() {
const group = node.parentNode.id; const lake = getLake();
const select = byId("lakeGroup"); const select = byId("lakeGroup");
select.options.length = 0; // remove all options select.options.length = 0; // remove all options
lakes.selectAll("g").each(function () { lakes.selectAll("g").each(function () {
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));
}); });
} }

View file

@ -796,14 +796,12 @@ function drawRivers() {
TIME && console.time("drawRivers"); TIME && console.time("drawRivers");
rivers.selectAll("*").remove(); rivers.selectAll("*").remove();
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
const riverPaths = pack.rivers.map(({cells, points, i, widthFactor, sourceWidth}) => { const riverPaths = pack.rivers.map(({cells, points, i, widthFactor, sourceWidth}) => {
if (!cells || cells.length < 2) return; if (!cells || cells.length < 2) return;
if (points && points.length !== cells.length) { if (points && points.length !== cells.length) {
console.error( console.error(
`River ${i} has ${cells.length} cells, but only ${points.length} points defined.`, `River ${i} has ${cells.length} cells, but only ${points.length} points defined. Resetting points data`
"Resetting points data"
); );
points = undefined; points = undefined;
} }

View file

@ -41,7 +41,7 @@ function editNamesbase() {
$("#namesbaseEditor").dialog({ $("#namesbaseEditor").dialog({
title: "Namesbase Editor", title: "Namesbase Editor",
width: "auto", width: "60vw",
position: {my: "center", at: "center", of: "svg"} position: {my: "center", at: "center", of: "svg"}
}); });
@ -66,7 +66,7 @@ function editNamesbase() {
function updateExamples() { function updateExamples() {
const base = +document.getElementById("namesbaseSelect").value; const base = +document.getElementById("namesbaseSelect").value;
let examples = ""; let examples = "";
for (let i = 0; i < 10; i++) { for (let i = 0; i < 7; i++) {
const example = Names.getBase(base); const example = Names.getBase(base);
if (example === undefined) { if (example === undefined) {
examples = "Cannot generate examples. Please verify the data"; examples = "Cannot generate examples. Please verify the data";
@ -250,7 +250,7 @@ function editNamesbase() {
const [rawName, min, max, d, m, rawNames] = base.split("|"); const [rawName, min, max, d, m, rawNames] = base.split("|");
const name = rawName.replace(unsafe, ""); const name = rawName.replace(unsafe, "");
const names = rawNames.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(); createBasesList();

View file

@ -160,7 +160,7 @@ function editNotes(id, name) {
} }
}; };
geneateWithAi(prompt, onApply); generateWithAi(prompt, onApply);
} }
function downloadLegends() { function downloadLegends() {

View file

@ -332,16 +332,12 @@ const cellsDensityMap = {
function changeCellsDensity(value) { function changeCellsDensity(value) {
pointsInput.value = value; pointsInput.value = value;
const cells = cellsDensityMap[value] || 1000; const cells = cellsDensityMap[value] || pointsInput.dataset.cells;
pointsInput.dataset.cells = cells; pointsInput.dataset.cells = cells;
pointsOutputFormatted.value = getCellsDensityValue(cells); pointsOutputFormatted.value = cells / 1000 + "K";
pointsOutputFormatted.style.color = getCellsDensityColor(cells); pointsOutputFormatted.style.color = getCellsDensityColor(cells);
} }
function getCellsDensityValue(cells) {
return cells / 1000 + "K";
}
function getCellsDensityColor(cells) { function getCellsDensityColor(cells) {
return cells > 50000 ? "#b12117" : cells !== 10000 ? "#dfdf12" : "#053305"; 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 (key.slice(0, 5) === "style") applyOption(stylePreset, key, key.slice(5));
} }
if (stored("winds")) options.winds = localStorage.getItem("winds").split(",").map(Number); if (stored("winds")) options.winds = stored("winds").split(",").map(Number);
if (stored("temperatureEquator")) options.temperatureEquator = +localStorage.getItem("temperatureEquator"); if (stored("temperatureEquator")) options.temperatureEquator = +stored("temperatureEquator");
if (stored("temperatureNorthPole")) options.temperatureNorthPole = +localStorage.getItem("temperatureNorthPole"); if (stored("temperatureNorthPole")) options.temperatureNorthPole = +stored("temperatureNorthPole");
if (stored("temperatureSouthPole")) options.temperatureSouthPole = +localStorage.getItem("temperatureSouthPole"); if (stored("temperatureSouthPole")) options.temperatureSouthPole = +stored("temperatureSouthPole");
if (stored("military")) options.military = JSON.parse(stored("military")); if (stored("military")) options.military = JSON.parse(stored("military"));
if (stored("tooltipSize")) changeTooltipSize(stored("tooltipSize")); if (stored("tooltipSize")) changeTooltipSize(stored("tooltipSize"));

View file

@ -74,13 +74,10 @@ function createRiver() {
function addRiver() { function addRiver() {
const {rivers, cells} = pack; const {rivers, cells} = pack;
const {addMeandering, getApproximateLength, getWidth, getOffset, getName, getRiverPath, getBasin, getNextId} =
Rivers;
const riverCells = createRiver.cells; const riverCells = createRiver.cells;
if (riverCells.length < 2) return tip("Add at least 2 cells", false, "error"); 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; const parent = cells.r[last(riverCells)] || riverId;
riverCells.forEach(cell => { riverCells.forEach(cell => {
@ -89,17 +86,24 @@ function createRiver() {
const source = riverCells[0]; const source = riverCells[0];
const mouth = parent === riverId ? last(riverCells) : riverCells[riverCells.length - 2]; 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 defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2);
const widthFactor = 1.2 * defaultWidthFactor; const widthFactor = 1.2 * defaultWidthFactor;
const meanderedPoints = addMeandering(riverCells); const meanderedPoints = Rivers.addMeandering(riverCells);
const discharge = cells.fl[mouth]; // m3 in second const discharge = cells.fl[mouth]; // m3 in second
const length = getApproximateLength(meanderedPoints); const length = Rivers.getApproximateLength(meanderedPoints);
const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, sourceWidth)); const width = Rivers.getWidth(
const name = getName(mouth); Rivers.getOffset({
const basin = getBasin(parent); flux: discharge,
pointIndex: meanderedPoints.length,
widthFactor,
startingWidth: sourceWidth
})
);
const name = Rivers.getName(mouth);
const basin = Rivers.getBasin(parent);
rivers.push({ rivers.push({
i: riverId, i: riverId,
@ -118,13 +122,11 @@ function createRiver() {
}); });
const id = "river" + riverId; const id = "river" + riverId;
// render river
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
viewbox viewbox
.select("#rivers") .select("#rivers")
.append("path") .append("path")
.attr("id", id) .attr("id", id)
.attr("d", getRiverPath(meanderedPoints, widthFactor, sourceWidth)); .attr("d", Rivers.getRiverPath(meanderedPoints, widthFactor, sourceWidth));
editRiver(id); editRiver(id);
} }

View file

@ -86,10 +86,16 @@ function editRiver(id) {
} }
function updateRiverWidth(river) { function updateRiverWidth(river) {
const {addMeandering, getWidth, getOffset} = Rivers;
const {cells, discharge, widthFactor, sourceWidth} = river; const {cells, discharge, widthFactor, sourceWidth} = river;
const meanderedPoints = addMeandering(cells); const meanderedPoints = Rivers.addMeandering(cells);
river.width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, sourceWidth)); river.width = Rivers.getWidth(
Rivers.getOffset({
flux: discharge,
pointIndex: meanderedPoints.length,
widthFactor,
startingWidth: sourceWidth
})
);
const width = `${rn(river.width * distanceScale, 3)} ${distanceUnitInput.value}`; const width = `${rn(river.width * distanceScale, 3)} ${distanceUnitInput.value}`;
byId("riverWidth").value = width; byId("riverWidth").value = width;
@ -158,11 +164,9 @@ function editRiver(id) {
river.points = debug.selectAll("#controlPoints > *").data(); river.points = debug.selectAll("#controlPoints > *").data();
river.cells = river.points.map(([x, y]) => findCell(x, y)); 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)); 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); elSelected.attr("d", path);
updateRiverLength(river); updateRiverLength(river);

View file

@ -116,20 +116,20 @@ function selectStyleElement() {
if ( if (
[ [
"armies", "armies",
"routes",
"lakes",
"biomes", "biomes",
"borders", "borders",
"cults",
"relig",
"cells", "cells",
"coastline", "coastline",
"prec", "coordinates",
"cults",
"gridOverlay",
"ice", "ice",
"icons", "icons",
"coordinates", "lakes",
"zones", "prec",
"gridOverlay" "relig",
"routes",
"zones"
].includes(styleElement) ].includes(styleElement)
) { ) {
styleStroke.style.display = "block"; styleStroke.style.display = "block";
@ -140,7 +140,7 @@ function selectStyleElement() {
// stroke dash // stroke dash
if ( if (
["routes", "borders", "temperature", "legend", "population", "coordinates", "zones", "gridOverlay"].includes( ["borders", "cells", "coordinates", "gridOverlay", "legend", "population", "routes", "temperature", "zones"].includes(
styleElement styleElement
) )
) { ) {

95
modules/ui/submap-tool.js Normal file
View 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;
}
}
}

View file

@ -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};
})();

View file

@ -3,7 +3,7 @@
// module to control the Tools options (click to edit, to re-geenerate, tp add) // module to control the Tools options (click to edit, to re-geenerate, tp add)
toolsContent.addEventListener("click", function (event) { 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; if (!["BUTTON", "I"].includes(event.target.tagName)) return;
const button = event.target.id; const button = event.target.id;
@ -70,8 +70,8 @@ toolsContent.addEventListener("click", function (event) {
else if (button === "addRoute") createRoute(); else if (button === "addRoute") createRoute();
else if (button === "addMarker") toggleAddMarker(); else if (button === "addMarker") toggleAddMarker();
// click to create a new map buttons // click to create a new map buttons
else if (button === "openSubmapMenu") UISubmap.openSubmapMenu(); else if (button === "openSubmapTool") openSubmapTool();
else if (button === "openResampleMenu") UISubmap.openResampleMenu(); else if (button === "openTransformTool") openTransformTool();
}); });
function processFeatureRegeneration(event, button) { function processFeatureRegeneration(event, button) {
@ -514,8 +514,8 @@ function regenerateEmblems() {
function regenerateReligions() { function regenerateReligions() {
Religions.generate(); Religions.generate();
if (layerIsOn("toggleReligions")) drawReligions();
else toggleReligions(); layerIsOn("toggleReligions") ? drawReligions() : toggleReligions();
refreshAllEditors(); refreshAllEditors();
} }
@ -685,28 +685,15 @@ function addRiverOnClick() {
if (cells.h[i] < 20) return tip("Cannot create river in water cell", false, "error"); if (cells.h[i] < 20) return tip("Cannot create river in water cell", false, "error");
if (cells.b[i]) return; if (cells.b[i]) return;
const {
alterHeights,
resolveDepressions,
addMeandering,
getRiverPath,
getBasin,
getName,
getType,
getWidth,
getOffset,
getApproximateLength,
getNextId
} = Rivers;
const riverCells = []; const riverCells = [];
let riverId = getNextId(rivers); let riverId = Rivers.getNextId(rivers);
let parent = riverId; let parent = riverId;
const initialFlux = grid.cells.prec[cells.g[i]]; const initialFlux = grid.cells.prec[cells.g[i]];
cells.fl[i] = initialFlux; cells.fl[i] = initialFlux;
const h = alterHeights(); const h = Rivers.alterHeights();
resolveDepressions(h); Rivers.resolveDepressions(h);
while (i) { while (i) {
cells.r[i] = riverId; cells.r[i] = riverId;
@ -780,11 +767,19 @@ function addRiverOnClick() {
const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2); const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2);
const widthFactor = const widthFactor =
river?.widthFactor || (!parent || parent === riverId ? defaultWidthFactor * 1.2 : defaultWidthFactor); 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 discharge = cells.fl[mouth]; // m3 in second
const length = getApproximateLength(meanderedPoints); const length = Rivers.getApproximateLength(meanderedPoints);
const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor)); const width = Rivers.getWidth(
Rivers.getOffset({
flux: discharge,
pointIndex: meanderedPoints.length,
widthFactor,
startingWidth: sourceWidth
})
);
if (river) { if (river) {
river.source = source; river.source = source;
@ -793,9 +788,9 @@ function addRiverOnClick() {
river.width = width; river.width = width;
river.cells = riverCells; river.cells = riverCells;
} else { } else {
const basin = getBasin(parent); const basin = Rivers.getBasin(parent);
const name = getName(mouth); const name = Rivers.getName(mouth);
const type = getType({i: riverId, length, parent}); const type = Rivers.getType({i: riverId, length, parent});
rivers.push({ rivers.push({
i: riverId, i: riverId,
@ -805,7 +800,7 @@ function addRiverOnClick() {
length, length,
width, width,
widthFactor, widthFactor,
sourceWidth: 0, sourceWidth,
parent, parent,
cells: riverCells, cells: riverCells,
basin, basin,
@ -815,8 +810,7 @@ function addRiverOnClick() {
} }
// render river // render river
lineGen.curve(d3.curveCatmullRom.alpha(0.1)); const path = Rivers.getRiverPath(meanderedPoints, widthFactor, sourceWidth);
const path = getRiverPath(meanderedPoints, widthFactor);
const id = "river" + riverId; const id = "river" + riverId;
const riversG = viewbox.select("#rivers"); const riversG = viewbox.select("#rivers");
riversG.append("path").attr("id", id).attr("d", path); riversG.append("path").attr("id", id).attr("d", path);

View 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];
}
}

View file

@ -1,7 +1,7 @@
"use strict"; "use strict";
function editZones() { function editZones() {
closeDialogs(); closeDialogs("#zonesEditor, .stable");
if (!layerIsOn("toggleZones")) toggleZones(); if (!layerIsOn("toggleZones")) toggleZones();
const body = byId("zonesBodySection"); const body = byId("zonesBodySection");
@ -341,6 +341,8 @@ function editZones() {
} }
function toggleLegend() { function toggleLegend() {
if (legend.selectAll("*").size()) return clearLegend(); // hide legend
const filterBy = byId("zonesFilterType").value; const filterBy = byId("zonesFilterType").value;
const isFiltered = filterBy && filterBy !== "all"; const isFiltered = filterBy && filterBy !== "all";
const visibleZones = pack.zones.filter(zone => !zone.hidden && (!isFiltered || zone.type === filterBy)); const visibleZones = pack.zones.filter(zone => !zone.hidden && (!isFiltered || zone.type === filterBy));

View file

@ -209,11 +209,11 @@ window.Zones = (function () {
const cost = []; const cost = [];
const maxCells = rand(20, 40); const maxCells = rand(20, 40);
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); const queue = new FlatQueue();
queue.queue({e: burg.cell, p: 0}); queue.push({e: burg.cell, p: 0}, 0);
while (queue.length) { while (queue.length) {
const next = queue.dequeue(); const next = queue.pop();
if (cells.burg[next.e] || cells.pop[next.e]) cellsArray.push(next.e); if (cells.burg[next.e] || cells.pop[next.e]) cellsArray.push(next.e);
usedCells[next.e] = 1; usedCells[next.e] = 1;
@ -224,7 +224,7 @@ window.Zones = (function () {
if (!cost[nextCellId] || p < cost[nextCellId]) { if (!cost[nextCellId] || p < cost[nextCellId]) {
cost[nextCellId] = p; cost[nextCellId] = p;
queue.queue({e: nextCellId, p}); queue.push({e: nextCellId, p}, p);
} }
}); });
} }
@ -251,11 +251,11 @@ window.Zones = (function () {
const cost = []; const cost = [];
const maxCells = rand(5, 25); const maxCells = rand(5, 25);
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); const queue = new FlatQueue();
queue.queue({e: burg.cell, p: 0}); queue.push({e: burg.cell, p: 0}, 0);
while (queue.length) { while (queue.length) {
const next = queue.dequeue(); const next = queue.pop();
if (cells.burg[next.e] || cells.pop[next.e]) cellsArray.push(next.e); if (cells.burg[next.e] || cells.pop[next.e]) cellsArray.push(next.e);
usedCells[next.e] = 1; usedCells[next.e] = 1;
@ -266,7 +266,7 @@ window.Zones = (function () {
if (!cost[e] || p < cost[e]) { if (!cost[e] || p < cost[e]) {
cost[e] = p; cost[e] = p;
queue.queue({e, p}); queue.push({e, p}, p);
} }
}); });
} }

View file

@ -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");
}

View file

@ -241,10 +241,8 @@ void (function addFindAll() {
}; };
const tree_filter = function (x, y, radius) { 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}; const t = {x, y, x0: this._x0, y0: this._y0, x3: this._x1, y3: this._y1, quads: [], node: this._root};
if (t.node) { if (t.node) t.quads.push(new Quad(t.node, t.x0, t.y0, t.x3, t.y3));
t.quads.push(new Quad(t.node, t.x0, t.y0, t.x3, t.y3));
}
radiusSearchInit(t, radius); radiusSearchInit(t, radius);
var i = 0; var i = 0;

View file

@ -78,11 +78,10 @@ function getBorderPath(vertices, vertexChain, discontinue) {
} }
const operation = discontinued ? "M" : "L"; const operation = discontinued ? "M" : "L";
const command = operation === lastOperation ? "" : operation;
discontinued = false; discontinued = false;
lastOperation = operation; lastOperation = operation;
const command = operation === "L" && operation === lastOperation ? "" : operation;
return ` ${command}${vertices.p[vertexId]}`; return ` ${command}${vertices.p[vertexId]}`;
}); });
@ -177,3 +176,60 @@ function connectVertices({vertices, startingVertex, ofSameType, addToChecked, cl
if (closeRing) chain.push(startingVertex); if (closeRing) chain.push(startingVertex);
return chain; 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();
}

View file

@ -57,6 +57,14 @@ JSON.isValid = str => {
} }
}; };
JSON.safeParse = str => {
try {
return JSON.parse(str);
} catch (e) {
return null;
}
};
function sanitizeId(string) { function sanitizeId(string) {
if (!string) throw new Error("No string provided"); if (!string) throw new Error("No string provided");

View file

@ -12,7 +12,7 @@
* *
* Example: 1.102.2 -> Major version 1, Minor version 102, Patch version 2 * 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"); 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> <ul>
<strong>Latest changes:</strong> <strong>Latest changes:</strong>
<li>Submap and Transform tools rework</li>
<li>Azgaar Bot to answer questions and provide help</li> <li>Azgaar Bot to answer questions and provide help</li>
<li>Labels: ability to set letter spacing</li> <li>Labels: ability to set letter spacing</li>
<li>Zones performance improvement</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>Configurable longitude</li>
<li>Preview villages map</li> <li>Preview villages map</li>
<li>Ability to render ocean heightmap</li> <li>Ability to render ocean heightmap</li>
<li>Scale bar styling features</li>
</ul> </ul>
<p>Join our <a href="${discord}" target="_blank">Discord server</a> and <a href="${reddit}" target="_blank">Reddit community</a> to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.</p> <p>Join our <a href="${discord}" target="_blank">Discord server</a> and <a href="${reddit}" target="_blank">Reddit community</a> to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.</p>
@ -58,7 +58,7 @@ if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format o
width: "28em", width: "28em",
position: {my: "center center-4em", at: "center", of: "svg"}, position: {my: "center center-4em", at: "center", of: "svg"},
buttons: { buttons: {
"Cleanup data": () => cleanupData(), "Clear cache": () => cleanupData(),
"Don't show again": function () { "Don't show again": function () {
$(this).dialog("close"); $(this).dialog("close");
localStorage.setItem("version", VERSION); localStorage.setItem("version", VERSION);