Urquhart routes (#1072)

* feat: routes generation

* feat: routes rendering

* feat: searoutes fix, changing reGraph

* feat: searoute - change pathfinding algo

* feat: routes - cleanup code

* feat: routes - change data format

* feat: routes - add routes to json export

* feat: edit routes - start

* feat: edit routes - main

* feat: edit routes - EP

* feat: edit routes - remove route

* feat: route - generate names

* feat: route - continue

* Refactor route merging logic for improved performance

* feat: routes - show name in tooltip

* feat: routes - create route dialog

* feat: update data on control point remove

* feat: routes editor - split route

* feat: add join route functionality to routes editor

* feat: Add join route functionality to routes editor

* feat: Update join route tooltip in routes editor

* feat: routes overview - sort by length

* feat: routes overview - fix distanceScale value

* feat: routes overview - create route

* Refactor getMiddlePoint function to getCloseToEdgePoint

* feat: routes - change data format, fix issues

* feat: routes - regenerateRoutes

* feat: routes - add route on burg creation

* chore - remove merge conflict markers

* chore - remove merge conflict markers

* feat: routes name - no unnamed burg names

* feat: routes - lock routes

* fix: routes - split routes

* feat: routes - tip correction

* feat: routes - auto-update part 1

* feat: routes - return old rePacj logic to not break auto-update

* feat: routes - auto-update - add connections

---------

Co-authored-by: Azgaar <azgaar.fmg@yandex.com>
This commit is contained in:
Azgaar 2024-08-15 15:46:55 +02:00 committed by GitHub
parent c6dd331eb6
commit f19b891421
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 2462 additions and 1032 deletions

View file

@ -258,7 +258,6 @@ i.icon-lock {
cursor: pointer; cursor: pointer;
} }
#routeEditor > *,
#labelEditor div { #labelEditor div {
display: inline-block; display: inline-block;
} }
@ -1618,6 +1617,7 @@ div.states > .riverType {
#burgBody > div > div, #burgBody > div > div,
#riverBody > div, #riverBody > div,
#routeBody > div,
#lakeBody > div { #lakeBody > div {
padding: 0.1em; padding: 0.1em;
} }
@ -1625,6 +1625,7 @@ div.states > .riverType {
#riverBody div.label, #riverBody div.label,
#riverBody input, #riverBody input,
#riverBody select, #riverBody select,
#routeBody div.label,
#lakeBody div.label, #lakeBody div.label,
#lakeBody input, #lakeBody input,
#lakeBody select { #lakeBody select {
@ -1632,6 +1633,12 @@ div.states > .riverType {
width: 7em; width: 7em;
} }
#routeBody input,
#routeBody select {
display: inline-block;
width: 10em;
}
#stateNameEditor div.label, #stateNameEditor div.label,
#provinceNameEditor div.label, #provinceNameEditor div.label,
#regimentBody div.label, #regimentBody div.label,

View file

@ -138,7 +138,7 @@
} }
</style> </style>
<link rel="preload" href="index.css?v=1.98.01" as="style" onload="this.onload=null; this.rel='stylesheet'" /> <link rel="preload" href="index.css?v=1.99.00" as="style" onload="this.onload=null; this.rel='stylesheet'" />
<link rel="preload" href="icons.css" as="style" onload="this.onload=null; this.rel='stylesheet'" /> <link rel="preload" href="icons.css" as="style" onload="this.onload=null; this.rel='stylesheet'" />
<link rel="preload" href="libs/jquery-ui.css" as="style" onload="this.onload=null; this.rel='stylesheet'" /> <link rel="preload" href="libs/jquery-ui.css" as="style" onload="this.onload=null; this.rel='stylesheet'" />
</head> </head>
@ -610,6 +610,7 @@
id="toggleRoutes" id="toggleRoutes"
data-tip="Trade routes: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style" data-tip="Trade routes: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="U" data-shortcut="U"
class="buttonoff"
onclick="toggleRoutes(event)" onclick="toggleRoutes(event)"
> >
Ro<u>u</u>tes Ro<u>u</u>tes
@ -2119,6 +2120,9 @@
<button id="overviewRiversButton" data-tip="Click to open Rivers Overview" data-shortcut="Shift + V"> <button id="overviewRiversButton" data-tip="Click to open Rivers Overview" data-shortcut="Shift + V">
Rivers Rivers
</button> </button>
<button id="overviewRoutesButton" data-tip="Click to open Routes Overview" data-shortcut="Shift + U">
Routes
</button>
<button id="editStatesButton" data-tip="Click to open States Editor" data-shortcut="Shift + S"> <button id="editStatesButton" data-tip="Click to open States Editor" data-shortcut="Shift + S">
States States
</button> </button>
@ -2171,7 +2175,7 @@
<button id="regenerateRivers" data-tip="Click to regenerate all rivers (restore default state)"> <button id="regenerateRivers" data-tip="Click to regenerate all rivers (restore default state)">
Rivers Rivers
</button> </button>
<button id="regenerateRoutes" data-tip="Click to regenerate all routes">Routes</button> <button id="regenerateRoutes" data-tip="Click to regenerate all unlocked routes">Routes</button>
<button <button
id="regenerateStates" id="regenerateStates"
data-tip="Click to select new capitals and regenerate non-locked states. Emblems and military forces will be regenerated as well, burgs will remain as they are" data-tip="Click to select new capitals and regenerate non-locked states. Emblems and military forces will be regenerated as well, burgs will remain as they are"
@ -2216,7 +2220,7 @@
> >
River River
</button> </button>
<button id="addRoute" data-tip="Click on map to place a route" data-shortcut="Shift + 4">Route</button> <button id="addRoute" data-tip="Open route creation dialog" data-shortcut="Shift + 4">Route</button>
</div> </div>
<div class="separator">Show</div> <div class="separator">Show</div>
@ -2700,7 +2704,7 @@
data-tip="Provide a name for the new group" data-tip="Provide a name for the new group"
style="display: none; width: 10em" style="display: none; width: 10em"
/> />
<span id="labelGroupNew" data-tip="Create new group for this label" class="icon-plus pointer"></span> <span id="labelGroupNew" data-tip="Create a new group for this label" class="icon-plus pointer"></span>
<span <span
id="labelGroupRemove" id="labelGroupRemove"
data-tip="Remove the Group with all labels" data-tip="Remove the Group with all labels"
@ -2813,7 +2817,7 @@
<div id="riverBottom"> <div id="riverBottom">
<button <button
id="riverCreateSelectingCells" id="riverCreateSelectingCells"
data-tip="Create new river selecting river cells" data-tip="Create a new river selecting river cells"
class="icon-map-pin" class="icon-map-pin"
></button> ></button>
<button id="riverEditStyle" data-tip="Edit style for all rivers in Style Editor" class="icon-brush"></button> <button id="riverEditStyle" data-tip="Edit style for all rivers in Style Editor" class="icon-brush"></button>
@ -2857,7 +2861,7 @@
<div data-tip="Type to change lake type (group)"> <div data-tip="Type to change lake type (group)">
<div class="label" style="width: 4.8em">Type:</div> <div class="label" style="width: 4.8em">Type:</div>
<span id="lakeGroupRemove" data-tip="Remove the group" class="icon-trash-empty pointer"></span> <span id="lakeGroupRemove" data-tip="Remove the group" class="icon-trash-empty pointer"></span>
<span id="lakeGroupAdd" data-tip="Create new type (group) for the lake" class="icon-plus pointer"></span> <span id="lakeGroupAdd" data-tip="Create a new type (group) for the lake" class="icon-plus pointer"></span>
<select id="lakeGroup" data-tip="Select lake type (group)"></select> <select id="lakeGroup" data-tip="Select lake type (group)"></select>
<input <input
id="lakeGroupName" id="lakeGroupName"
@ -2949,35 +2953,78 @@
</div> </div>
<div id="routeEditor" class="dialog" style="display: none"> <div id="routeEditor" class="dialog" style="display: none">
<button id="routeGroupsShow" data-tip="Show the group selection" class="icon-tags"></button> <div id="routeBody" style="padding-bottom: 0.3em">
<div id="routeGroupsSelection" style="display: none"> <div>
<button id="routeGroupsHide" data-tip="Hide the group section" class="icon-tags"></button> <div class="label">Name:</div>
<select id="routeGroup" data-tip="Select a group for this route" style="width: 12em"></select> <input id="routeName" data-tip="Type to rename the route" autocorrect="off" spellcheck="false" />
<input <span data-tip="Speak the name. You can change voice and language in options" class="speaker">🔊</span>
id="routeGroupName" <span id="routeGenerateName" data-tip="Generate route name" class="icon-globe pointer"></span>
placeholder="new group name"
data-tip="Provide a name for the new group"
style="display: none; width: 12em"
/>
<span id="routeGroupAdd" data-tip="Create new group for this route" class="icon-plus pointer"></span>
<span
id="routeGroupRemove"
data-tip="Remove all routes of this group"
class="icon-trash-empty pointer"
></span>
</div> </div>
<button id="routeEditStyle" data-tip="Edit route group style in Style Editor" class="icon-brush"></button> <div data-tip="Select route group">
<button id="routeLength" data-tip="Route length in selected units">0</button> <div class="label">Group:</div>
<select id="routeGroup"></select>
<span id="routeGroupEdit" data-tip="Edit route groups" class="icon-pencil pointer"></span>
<span id="routeEditStyle" data-tip="Edit style for the route group" class="icon-brush pointer"></span>
</div>
<div data-tip="Route length in selected units">
<div class="label">Length:</div>
<input id="routeLength" disabled />
</div>
</div>
<div id="routeBottom">
<button
id="routeCreateSelectingCells"
data-tip="Create a new route selecting route cells"
class="icon-map-pin"
></button>
<button
id="routeJoin"
data-tip="Click to join the route to another route that starts or ends at the same cell"
class="icon-link"
></button>
<button
id="routeSplit"
data-tip="Click on a control point to split the route there"
class="icon-unlink"
></button>
<button <button
id="routeElevationProfile" id="routeElevationProfile"
data-tip="Show the elevation profile for the route" data-tip="Show the elevation profile for the route"
class="icon-chart-area" class="icon-chart-area"
></button> ></button>
<button id="routeSplit" data-tip="Click on a control point to split the route" class="icon-unlink"></button>
<button id="routeLegend" data-tip="Edit free text notes (legend) for the route" class="icon-edit"></button> <button id="routeLegend" data-tip="Edit free text notes (legend) for the route" class="icon-edit"></button>
<button id="routeNew" data-tip="Create new route clicking on map" class="icon-map-pin"></button> <button id="routeLock" class="icon-lock-open" onmouseover="showElementLockTip(event)"></button>
<button id="routeRemove" data-tip="Remove route" data-shortcut="Delete" class="icon-trash fastDelete"></button> <button
id="routeRemove"
data-tip="Remove route"
data-shortcut="Delete"
class="icon-trash fastDelete"
></button>
</div>
</div>
<div id="routeCreator" class="dialog" style="display: none">
<div>Click on map to add/remove route points</div>
<div id="routeCreatorBody" class="table" style="margin: 0.3em 0"></div>
<div id="routeCreatorBottom">
<button id="routeCreatorComplete" data-tip="Complete route creation" class="icon-check"></button>
<button id="routeCreatorCancel" data-tip="Cancel the creation" class="icon-cancel"></button>
<div style="display: inline-block">
Group:
<select id="routeCreatorGroupSelect"></select>
<span id="routeCreatorGroupEdit" data-tip="Edit route groups" class="icon-pencil pointer"></span>
</div>
</div>
</div>
<div id="routeGroupsEditor" class="dialog" style="display: none">
<div id="routeGroupsEditorBody" class="table" style="padding: 0.3em 0; width: 100%"></div>
<div id="routeGroupsEditorBottom">
<button id="routeGroupsEditorAdd" data-tip="Add route group" class="icon-plus"></button>
</div>
</div> </div>
<div id="iceEditor" class="dialog" style="display: none"> <div id="iceEditor" class="dialog" style="display: none">
@ -3004,7 +3051,11 @@
data-tip="Provide a name for the new group" data-tip="Provide a name for the new group"
style="display: none; width: 9em" style="display: none; width: 9em"
/> />
<span id="coastlineGroupAdd" data-tip="Create new group for this coastline" class="icon-plus pointer"></span> <span
id="coastlineGroupAdd"
data-tip="Create a new group for this coastline"
class="icon-plus pointer"
></span>
<span id="coastlineGroupRemove" data-tip="Remove the group" class="icon-trash-empty pointer"></span> <span id="coastlineGroupRemove" data-tip="Remove the group" class="icon-trash-empty pointer"></span>
</div> </div>
@ -3475,10 +3526,10 @@
<input <input
id="burgInputGroup" id="burgInputGroup"
placeholder="new group name" placeholder="new group name"
data-tip="Create new Group for the Burg" data-tip="Create a new Group for the Burg"
style="display: none; width: 10em" style="display: none; width: 10em"
/> />
<i id="burgAddGroup" data-tip="Create new group for the burg" class="icon-plus pointer"></i> <i id="burgAddGroup" data-tip="Create a new group for the burg" class="icon-plus pointer"></i>
<i id="burgRemoveGroup" data-tip="Remove selected burg group" class="icon-trash pointer"></i> <i id="burgRemoveGroup" data-tip="Remove selected burg group" class="icon-trash pointer"></i>
</div> </div>
@ -3627,7 +3678,7 @@
<div id="regimentBottom"> <div id="regimentBottom">
<button id="regimentAttack" data-tip="Attack foreign regiment" class="icon-target"></button> <button id="regimentAttack" data-tip="Attack foreign regiment" class="icon-target"></button>
<button id="regimentAdd" data-tip="Create new regiment or fleet" class="icon-user-plus"></button> <button id="regimentAdd" data-tip="Create a new regiment or fleet" class="icon-user-plus"></button>
<button id="regimentSplit" data-tip="Split regiment into 2 separate ones" class="icon-half"></button> <button id="regimentSplit" data-tip="Split regiment into 2 separate ones" class="icon-half"></button>
<button <button
id="regimentAttach" id="regimentAttach"
@ -5408,12 +5459,6 @@
Population Population
</div> </div>
<div data-tip="Click to sort by burg type" class="sortable alphabetically" data-sortby="type">Type&nbsp;</div> <div data-tip="Click to sort by burg type" class="sortable alphabetically" data-sortby="type">Type&nbsp;</div>
<div
id="burgsInvertLock"
style="color: #6e5e66"
data-tip="Click to invert lock for all burgs"
class="icon-lock pointer"
></div>
</div> </div>
<div id="burgsBody" class="table"></div> <div id="burgsBody" class="table"></div>
@ -5460,6 +5505,47 @@
</div> </div>
</div> </div>
<div id="routesOverview" class="dialog stable" style="display: none">
<div id="routesHeader" class="header" style="grid-template-columns: 17em 8em 8em">
<div data-tip="Click to sort by route name" class="sortable alphabetically" data-sortby="name">
Route&nbsp;
</div>
<div data-tip="Click to sort by route group" class="sortable alphabetically" data-sortby="group">
Group&nbsp;
</div>
<div data-tip="Click to sort by route length" class="sortable icon-sort-number-down" data-sortby="length">
Length&nbsp;
</div>
</div>
<div id="routesBody" class="table"></div>
<div id="routesFooter" class="totalLine">
<div data-tip="Routes number" style="margin-left: 4px">
Total routes:&nbsp;<span id="routesFooterNumber">0</span>
</div>
<div data-tip="Average length" style="margin-left: 12px">
Average length:&nbsp;<span id="routesFooterLength">0</span>
</div>
</div>
<div id="routesBottom">
<button id="routesOverviewRefresh" data-tip="Refresh the Editor" class="icon-cw"></button>
<button
id="routesCreateNew"
data-tip="Create a new route selecting route cells"
class="icon-map-pin"
></button>
<button
id="routesExport"
data-tip="Save routes-related data as a text file (.csv)"
class="icon-download"
></button>
<button id="routesLockAll" data-tip="Lock or unlock all routes" class="icon-lock"></button>
<button id="routesRemoveAll" data-tip="Remove all routes" class="icon-trash"></button>
</div>
</div>
<div id="riversOverview" class="dialog stable" style="display: none"> <div id="riversOverview" class="dialog stable" style="display: none">
<div id="riversHeader" class="header" style="grid-template-columns: 9em 4em 6em 6em 5em 9em"> <div id="riversHeader" class="header" style="grid-template-columns: 9em 4em 6em 6em 5em 9em">
<div data-tip="Click to sort by river name" class="sortable alphabetically" data-sortby="name"> <div data-tip="Click to sort by river name" class="sortable alphabetically" data-sortby="name">
@ -5506,7 +5592,7 @@
data-tip="Automatically add river starting from clicked cell. Hold Shift to add multiple" data-tip="Automatically add river starting from clicked cell. Hold Shift to add multiple"
class="icon-plus" class="icon-plus"
></button> ></button>
<button id="riverCreateNew" data-tip="Create new river selecting river cells" class="icon-map-pin"></button> <button id="riverCreateNew" data-tip="Create a new river selecting river cells" class="icon-map-pin"></button>
<button id="riversBasinHighlight" data-tip="Toggle basin highlight mode" class="icon-sitemap"></button> <button id="riversBasinHighlight" data-tip="Toggle basin highlight mode" class="icon-sitemap"></button>
<button <button
id="riversExport" id="riversExport"
@ -6211,7 +6297,7 @@
Data to be copied: heightmap, biomes, religions, population, precipitation, cultures, states, provinces, Data to be copied: heightmap, biomes, religions, population, precipitation, cultures, states, provinces,
military regiments military regiments
</p> </p>
<p>Data to be regenerated: zones, roads, rivers</p> <p>Data to be regenerated: zones, routes, rivers</p>
<p>Burgs may be remapped incorrectly, manual change is required</p> <p>Burgs may be remapped incorrectly, manual change is required</p>
<p>Keep data for:</p> <p>Keep data for:</p>
@ -8073,95 +8159,101 @@
<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/priority-queue.min.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.91.01"></script> <script src="libs/indexedDB.js?v=1.99.00"></script>
<script src="utils/shorthands.js"></script> <script src="utils/shorthands.js?v=1.99.00"></script>
<script src="utils/commonUtils.js?v=1.89.29"></script> <script src="utils/commonUtils.js?v=1.99.00"></script>
<script src="utils/arrayUtils.js"></script> <script src="utils/arrayUtils.js?v=1.99.00"></script>
<script src="utils/colorUtils.js"></script> <script src="utils/functionUtils.js?v=1.99.00"></script>
<script src="utils/graphUtils.js?v=1.96.00"></script> <script src="utils/colorUtils.js?v=1.99.00"></script>
<script src="utils/nodeUtils.js"></script> <script src="utils/graphUtils.js?v=1.99.00"></script>
<script src="utils/numberUtils.js?v=1.89.08"></script> <script src="utils/nodeUtils.js?v=1.99.00"></script>
<script src="utils/polyfills.js?v=1.95.03"></script> <script src="utils/numberUtils.js?v=1.99.00"></script>
<script src="utils/probabilityUtils.js?v=1.88.00"></script> <script src="utils/polyfills.js?v=1.99.00"></script>
<script src="utils/stringUtils.js"></script> <script src="utils/probabilityUtils.js?v=1.99.00"></script>
<script src="utils/languageUtils.js"></script> <script src="utils/stringUtils.js?v=1.99.00"></script>
<script src="utils/unitUtils.js?v=1.87.00"></script> <script src="utils/languageUtils.js?v=1.99.00"></script>
<script src="utils/unitUtils.js?v=1.99.00"></script>
<script defer src="utils/debugUtils.js?v=1.99.00"></script>
<script src="modules/voronoi.js"></script> <script src="modules/voronoi.js"></script>
<script src="config/heightmap-templates.js"></script> <script src="config/heightmap-templates.js"></script>
<script src="config/precreated-heightmaps.js"></script> <script src="config/precreated-heightmaps.js"></script>
<script src="modules/heightmap-generator.js?v=1.88.00"></script> <script src="modules/heightmap-generator.js?v=1.99.00"></script>
<script src="modules/ocean-layers.js?v=1.98.04"></script> <script src="modules/ocean-layers.js?v=1.99.00"></script>
<script src="modules/river-generator.js?v=1.89.13"></script> <script src="modules/river-generator.js?v=1.99.00"></script>
<script src="modules/lakes.js?v=1.98.06"></script> <script src="modules/lakes.js?v=1.99.00"></script>
<script src="modules/biomes.js"></script> <script src="modules/biomes.js?v=1.99.00"></script>
<script src="modules/names-generator.js?v=1.87.14"></script> <script src="modules/names-generator.js?v=1.87.14"></script>
<script src="modules/cultures-generator.js?v=1.96.05"></script> <script src="modules/cultures-generator.js?v=1.99.00"></script>
<script src="modules/renderers/state-labels.js?v=1.96.04"></script> <script src="modules/renderers/state-labels.js?v=1.96.04"></script>
<script src="modules/burgs-and-states.js?v=1.97.07"></script> <script src="modules/burgs-and-states.js?v=1.99.00"></script>
<script src="modules/routes-generator.js"></script> <script src="modules/routes-generator.js?v=1.99.00"></script>
<script src="modules/religions-generator.js?v=1.93.08"></script> <script src="modules/religions-generator.js?v=1.99.00"></script>
<script src="modules/military-generator.js?v=1.97.14"></script> <script src="modules/military-generator.js?v=1.99.00"></script>
<script src="modules/markers-generator.js?v=1.93.04"></script> <script src="modules/markers-generator.js?v=1.99.00"></script>
<script src="modules/coa-generator.js?v=1.91.05"></script> <script src="modules/coa-generator.js?v=1.99.00"></script>
<script src="modules/submap.js?v=1.96.00"></script> <script src="modules/submap.js?v=1.99.00"></script>
<script src="libs/polylabel.min.js"></script> <script src="libs/polylabel.min.js"></script>
<script src="libs/lineclip.min.js"></script> <script src="libs/lineclip.min.js"></script>
<script src="libs/alea.min.js"></script> <script src="libs/alea.min.js"></script>
<script src="modules/fonts.js?v=1.89.18"></script> <script src="modules/fonts.js?v=1.99.00"></script>
<script src="modules/ui/layers.js?v=1.96.00"></script> <script src="modules/ui/layers.js?v=1.99.00"></script>
<script src="modules/ui/measurers.js?v=1.96.00"></script> <script src="modules/ui/measurers.js?v=1.99.00"></script>
<script src="modules/ui/stylePresets.js?v=1.96.00"></script> <script src="modules/ui/stylePresets.js?v=1.99.00"></script>
<script src="modules/ui/general.js?v=1.98.01"></script> <script src="modules/ui/general.js?v=1.99.00"></script>
<script src="modules/ui/options.js?v=1.98.04"></script> <script src="modules/ui/options.js?v=1.99.00"></script>
<script src="main.js?v=1.98.07"></script> <script src="main.js?v=1.99.00"></script>
<script defer src="modules/relief-icons.js"></script> <script defer src="modules/relief-icons.js?v=1.99.00"></script>
<script defer src="modules/ui/style.js?v=1.96.00"></script> <script defer src="modules/ui/style.js?v=1.99.00"></script>
<script defer src="modules/ui/editors.js?v=1.97.12"></script> <script defer src="modules/ui/editors.js?v=1.99.00"></script>
<script defer src="modules/ui/tools.js?v=1.97.12"></script> <script defer src="modules/ui/tools.js?v=1.99.00"></script>
<script defer src="modules/ui/world-configurator.js?v=1.98.01"></script> <script defer src="modules/ui/world-configurator.js?v=1.99.00"></script>
<script defer src="modules/ui/heightmap-editor.js?v=1.96.00"></script> <script defer src="modules/ui/heightmap-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/provinces-editor.js?v=1.96.00"></script> <script defer src="modules/ui/provinces-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/biomes-editor.js?v=1.91.05"></script> <script defer src="modules/ui/biomes-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/namesbase-editor.js?v=1.95.02"></script> <script defer src="modules/ui/namesbase-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/elevation-profile.js?v=1.97.10"></script> <script defer src="modules/ui/elevation-profile.js?v=1.99.00"></script>
<script defer src="modules/ui/temperature-graph.js?v=1.90.03"></script> <script defer src="modules/ui/temperature-graph.js?v=1.99.00"></script>
<script defer src="modules/ui/routes-editor.js?v=1.89.04"></script> <script defer src="modules/ui/routes-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/ice-editor.js?v=1.89.08"></script> <script defer src="modules/ui/routes-creator.js?v=1.99.00"></script>
<script defer src="modules/ui/lakes-editor.js?v=1.87.10"></script> <script defer src="modules/ui/route-group-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/coastline-editor.js"></script> <script defer src="modules/ui/ice-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/labels-editor.js?v=1.92.00"></script> <script defer src="modules/ui/lakes-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/rivers-editor.js"></script> <script defer src="modules/ui/coastline-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/rivers-creator.js?v=1.89.13"></script> <script defer src="modules/ui/labels-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/relief-editor.js"></script> <script defer src="modules/ui/rivers-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/burg-editor.js?v=1.97.00"></script> <script defer src="modules/ui/rivers-creator.js?v=1.99.00"></script>
<script defer src="modules/ui/units-editor.js?v=1.96.00"></script> <script defer src="modules/ui/relief-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/notes-editor.js?v=1.97.09"></script> <script defer src="modules/ui/burg-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/diplomacy-editor.js?v=1.88.04"></script> <script defer src="modules/ui/units-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/zones-editor.js?v=1.97.13"></script> <script defer src="modules/ui/notes-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/burgs-overview.js?v=1.97.00"></script> <script defer src="modules/ui/diplomacy-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/rivers-overview.js"></script> <script defer src="modules/ui/zones-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/military-overview.js?v=1.97.15"></script> <script defer src="modules/ui/burgs-overview.js?v=1.99.00"></script>
<script defer src="modules/ui/regiments-overview.js?v=1.89.20"></script> <script defer src="modules/ui/routes-overview.js?v=1.99.00"></script>
<script defer src="modules/ui/markers-overview.js?v=1.89.38"></script> <script defer src="modules/ui/rivers-overview.js?v=1.99.00"></script>
<script defer src="modules/ui/regiment-editor.js?v=1.97.14"></script> <script defer src="modules/ui/military-overview.js?v=1.99.00"></script>
<script defer src="modules/ui/battle-screen.js"></script> <script defer src="modules/ui/regiments-overview.js?v=1.99.00"></script>
<script defer src="modules/ui/emblems-editor.js?v=1.91.00"></script> <script defer src="modules/ui/markers-overview.js?v=1.99.00"></script>
<script defer src="modules/ui/markers-editor.js"></script> <script defer src="modules/ui/regiment-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/3d.js?v=1.94.03"></script> <script defer src="modules/ui/battle-screen.js?v=1.99.00"></script>
<script defer src="modules/ui/submap.js?v=1.96.00"></script> <script defer src="modules/ui/emblems-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/hotkeys.js?v=1.95.00"></script> <script defer src="modules/ui/markers-editor.js?v=1.99.00"></script>
<script defer src="modules/coa-renderer.js?v=1.94.00"></script> <script defer src="modules/ui/3d.js?v=1.99.00"></script>
<script defer src="modules/ui/submap.js?v=1.99.00"></script>
<script defer src="modules/ui/hotkeys.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.98.01"></script> <script defer src="modules/io/save.js?v=1.99.00"></script>
<script defer src="modules/io/load.js?v=1.98.06"></script> <script defer src="modules/io/load.js?v=1.99.00"></script>
<script defer src="modules/io/cloud.js?v=1.96.00"></script> <script defer src="modules/io/cloud.js?v=1.99.00"></script>
<script defer src="modules/io/export.js?v=1.98.05"></script> <script defer src="modules/io/export.js?v=1.99.00"></script>
<!-- Web Components --> <!-- Web Components -->
<script defer src="components/fill-box.js"></script> <script defer src="components/fill-box.js"></script>

57
libs/flatqueue.js Normal file
View file

@ -0,0 +1,57 @@
!(function (t, s) {
"object" == typeof exports && "undefined" != typeof module
? (module.exports = s())
: "function" == typeof define && define.amd
? define(s)
: ((t = "undefined" != typeof globalThis ? globalThis : t || self).FlatQueue = s());
})(this, function () {
"use strict";
return class {
constructor() {
(this.ids = []), (this.values = []), (this.length = 0);
}
clear() {
this.length = 0;
}
push(t, s) {
let i = this.length++;
for (; i > 0; ) {
const t = (i - 1) >> 1,
e = this.values[t];
if (s >= e) break;
(this.ids[i] = this.ids[t]), (this.values[i] = e), (i = t);
}
(this.ids[i] = t), (this.values[i] = s);
}
pop() {
if (0 === this.length) return;
const t = this.ids[0];
if ((this.length--, this.length > 0)) {
const t = (this.ids[0] = this.ids[this.length]),
s = (this.values[0] = this.values[this.length]),
i = this.length >> 1;
let e = 0;
for (; e < i; ) {
let t = 1 + (e << 1);
const i = t + 1;
let h = this.ids[t],
l = this.values[t];
const n = this.values[i];
if ((i < this.length && n < l && ((t = i), (h = this.ids[i]), (l = n)), l >= s)) break;
(this.ids[e] = h), (this.values[e] = l), (e = t);
}
(this.ids[e] = t), (this.values[e] = s);
}
return t;
}
peek() {
if (0 !== this.length) return this.ids[0];
}
peekValue() {
if (0 !== this.length) return this.values[0];
}
shrink() {
this.ids.length = this.values.length = this.length;
}
};
});

32
main.js
View file

@ -647,6 +647,7 @@ async function generate(options) {
Cultures.generate(); Cultures.generate();
Cultures.expand(); Cultures.expand();
BurgsAndStates.generate(); BurgsAndStates.generate();
Routes.generate();
Religions.generate(); Religions.generate();
BurgsAndStates.defineStateForms(); BurgsAndStates.defineStateForms();
BurgsAndStates.generateProvinces(); BurgsAndStates.generateProvinces();
@ -1175,10 +1176,11 @@ function reGraph() {
for (const i of gridCells.i) { for (const i of gridCells.i) {
const height = gridCells.h[i]; const height = gridCells.h[i];
const type = gridCells.t[i]; const type = gridCells.t[i];
if (height < 20 && type !== -1 && type !== -2) continue; // exclude all deep ocean points if (height < 20 && type !== -1 && type !== -2) continue; // exclude all deep ocean points
if (type === -2 && (i % 4 === 0 || features[gridCells.f[i]].type === "lake")) continue; // exclude non-coastal lake points if (type === -2 && (i % 4 === 0 || features[gridCells.f[i]].type === "lake")) continue; // exclude non-coastal lake points
const [x, y] = points[i];
const [x, y] = points[i];
addNewPoint(i, x, y, height); addNewPoint(i, x, y, height);
// add additional points for cells along coast // add additional points for cells along coast
@ -1406,8 +1408,8 @@ function reMarkFeatures() {
queue[0] = cells.f.findIndex(f => !f); // find unmarked cell queue[0] = cells.f.findIndex(f => !f); // find unmarked cell
} }
// markupPackLand markup(pack.cells, 3, 1, 0); // markupPackLand
markup(pack.cells, 3, 1, 0); markup(pack.cells, -2, -1, -10); // markupPackWater
function defineHaven(i) { function defineHaven(i) {
const water = cells.c[i].filter(c => cells.h[c] < 20); const water = cells.c[i].filter(c => cells.h[c] < 20);
@ -1645,9 +1647,10 @@ function addZones(number = 1) {
const burg = ra(burgs.filter(b => !used[b.cell] && b.i && !b.removed)); // random burg const burg = ra(burgs.filter(b => !used[b.cell] && b.i && !b.removed)); // random burg
if (!burg) return; if (!burg) return;
const cellsArray = [], const cellsArray = [];
cost = [], const cost = [];
power = rand(20, 37); const power = rand(20, 37);
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
queue.queue({e: burg.cell, p: 0}); queue.queue({e: burg.cell, p: 0});
@ -1656,15 +1659,14 @@ function addZones(number = 1) {
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);
used[next.e] = 1; used[next.e] = 1;
cells.c[next.e].forEach(function (e) { cells.c[next.e].forEach(nextCellId => {
const r = cells.road[next.e]; const c = Routes.getRoute(next.e, nextCellId) ? 5 : 100;
const c = r ? Math.max(10 - r, 1) : 100;
const p = next.p + c; const p = next.p + c;
if (p > power) return; if (p > power) return;
if (!cost[e] || p < cost[e]) { if (!cost[nextCellId] || p < cost[nextCellId]) {
cost[e] = p; cost[nextCellId] = p;
queue.queue({e, p}); queue.queue({e: nextCellId, p});
} }
}); });
} }
@ -1785,10 +1787,10 @@ function addZones(number = 1) {
} }
function addAvalanche() { function addAvalanche() {
const roads = cells.i.filter(i => !used[i] && cells.road[i] && cells.h[i] >= 70); const routes = cells.i.filter(i => !used[i] && Routes.isConnected(i) && cells.h[i] >= 70);
if (!roads.length) return; if (!routes.length) return;
const cell = +ra(roads); const cell = +ra(routes);
const cellsArray = [], const cellsArray = [],
queue = [cell], queue = [cell],
power = rand(3, 15); power = rand(3, 15);

View file

@ -1,24 +1,8 @@
"use strict"; "use strict";
window.Biomes = (function () {
const MIN_LAND_HEIGHT = 20; const MIN_LAND_HEIGHT = 20;
const names = [
"Marine",
"Hot desert",
"Cold desert",
"Savanna",
"Grassland",
"Tropical seasonal forest",
"Temperate deciduous forest",
"Tropical rainforest",
"Temperate rainforest",
"Taiga",
"Tundra",
"Glacier",
"Wetland"
];
window.Biomes = (function () {
const getDefault = () => { const getDefault = () => {
const name = [ const name = [
"Marine", "Marine",
@ -52,7 +36,7 @@ window.Biomes = (function () {
"#0b9131" "#0b9131"
]; ];
const habitability = [0, 4, 10, 22, 30, 50, 100, 80, 90, 12, 4, 0, 12]; const habitability = [0, 4, 10, 22, 30, 50, 100, 80, 90, 12, 4, 0, 12];
const iconsDensity = [0, 3, 2, 120, 120, 120, 120, 150, 150, 100, 5, 0, 150]; const iconsDensity = [0, 3, 2, 120, 120, 120, 120, 150, 150, 100, 5, 0, 250];
const icons = [ const icons = [
{}, {},
{dune: 3, cactus: 6, deadTree: 1}, {dune: 3, cactus: 6, deadTree: 1},

View file

@ -6,27 +6,20 @@ window.BurgsAndStates = (() => {
const n = cells.i.length; const n = cells.i.length;
cells.burg = new Uint16Array(n); // cell burg cells.burg = new Uint16Array(n); // cell burg
cells.road = new Uint16Array(n); // cell road power
cells.crossroad = new Uint16Array(n); // cell crossroad power
const burgs = (pack.burgs = placeCapitals()); const burgs = (pack.burgs = placeCapitals());
pack.states = createStates(); pack.states = createStates();
const capitalRoutes = Routes.getRoads();
placeTowns(); placeTowns();
expandStates(); expandStates();
normalizeStates(); normalizeStates();
const townRoutes = Routes.getTrails();
specifyBurgs(); specifyBurgs();
const oceanRoutes = Routes.getSearoutes();
collectStatistics(); collectStatistics();
assignColors(); assignColors();
generateCampaigns(); generateCampaigns();
generateDiplomacy(); generateDiplomacy();
Routes.draw(capitalRoutes, townRoutes, oceanRoutes);
drawBurgs(); drawBurgs();
function placeCapitals() { function placeCapitals() {
@ -138,9 +131,8 @@ window.BurgsAndStates = (() => {
while (burgsAdded < burgsNumber && spacing > 1) { while (burgsAdded < burgsNumber && spacing > 1) {
for (let i = 0; burgsAdded < burgsNumber && i < sorted.length; i++) { for (let i = 0; burgsAdded < burgsNumber && i < sorted.length; i++) {
if (cells.burg[sorted[i]]) continue; if (cells.burg[sorted[i]]) continue;
const cell = sorted[i], const cell = sorted[i];
x = cells.p[cell][0], const [x, y] = cells.p[cell];
y = cells.p[cell][1];
const s = spacing * gauss(1, 0.3, 0.2, 2, 2); // randomize to make placement not uniform const s = spacing * gauss(1, 0.3, 0.2, 2, 2); // randomize to make placement not uniform
if (burgsTree.find(x, y, s) !== undefined) continue; // to close to existing burg if (burgsTree.find(x, y, s) !== undefined) continue; // to close to existing burg
const burg = burgs.length; const burg = burgs.length;
@ -183,12 +175,12 @@ window.BurgsAndStates = (() => {
} else b.port = 0; } else b.port = 0;
// define burg population (keep urbanization at about 10% rate) // define burg population (keep urbanization at about 10% rate)
b.population = rn(Math.max((cells.s[i] + cells.road[i] / 2) / 8 + b.i / 1000 + (i % 100) / 1000, 0.1), 3); b.population = rn(Math.max(cells.s[i] / 8 + b.i / 1000 + (i % 100) / 1000, 0.1), 3);
if (b.capital) b.population = rn(b.population * 1.3, 3); // increase capital population if (b.capital) b.population = rn(b.population * 1.3, 3); // increase capital population
if (b.port) { if (b.port) {
b.population = b.population * 1.3; // increase port population b.population = b.population * 1.3; // increase port population
const [x, y] = getMiddlePoint(i, haven); const [x, y] = getCloseToEdgePoint(i, haven);
b.x = x; b.x = x;
b.y = y; b.y = y;
} }
@ -229,6 +221,23 @@ window.BurgsAndStates = (() => {
TIME && console.timeEnd("specifyBurgs"); TIME && console.timeEnd("specifyBurgs");
}; };
function getCloseToEdgePoint(cell1, cell2) {
const {cells, vertices} = pack;
const [x0, y0] = cells.p[cell1];
const commonVertices = cells.v[cell1].filter(vertex => vertices.c[vertex].some(cell => cell === cell2));
const [x1, y1] = vertices.p[commonVertices[0]];
const [x2, y2] = vertices.p[commonVertices[1]];
const xEdge = (x1 + x2) / 2;
const yEdge = (y1 + y2) / 2;
const x = rn(x0 + 0.95 * (xEdge - x0), 2);
const y = rn(y0 + 0.95 * (yEdge - y0), 2);
return [x, y];
}
const getType = (i, port) => { const getType = (i, port) => {
const cells = pack.cells; const cells = pack.cells;
if (port) return "Naval"; if (port) return "Naval";
@ -244,11 +253,11 @@ window.BurgsAndStates = (() => {
return "Generic"; return "Generic";
}; };
const defineBurgFeatures = newburg => { const defineBurgFeatures = burg => {
const {cells} = pack; const {cells} = pack;
pack.burgs pack.burgs
.filter(b => (newburg ? b.i == newburg.i : b.i && !b.removed && !b.lock)) .filter(b => (burg ? b.i == burg.i : b.i && !b.removed && !b.lock))
.forEach(b => { .forEach(b => {
const pop = b.population; const pop = b.population;
b.citadel = Number(b.capital || (pop > 50 && P(0.75)) || (pop > 15 && P(0.5)) || P(0.1)); b.citadel = Number(b.capital || (pop > 50 && P(0.75)) || (pop > 15 && P(0.5)) || P(0.1));

View file

@ -860,4 +860,59 @@ export function resolveVersionConflicts(version) {
shiftCompass(); shiftCompass();
} }
} }
if (version < 1.99) {
// v1.99.00 changed routes generation algorithm and data format
delete cells.road;
delete cells.crossroad;
pack.routes = [];
const POINT_DISTANCE = grid.spacing * 0.75;
routes.selectAll("g").each(function () {
const group = this.id;
if (!group) return;
for (const node of this.querySelectorAll("path")) {
const totalLength = node.getTotalLength();
if (!totalLength) debugger;
const increment = totalLength / Math.ceil(totalLength / POINT_DISTANCE);
const points = [];
for (let i = 0; i <= totalLength + 0.1; i += increment) {
const point = node.getPointAtLength(i);
const x = rn(point.x, 2);
const y = rn(point.y, 2);
const cellId = findCell(x, y);
points.push([x, y, cellId]);
}
if (points.length < 2) return;
const secondCellId = points[1][2];
const feature = pack.cells.f[secondCellId];
pack.routes.push({i: pack.routes.length, group, feature, points});
}
});
routes.selectAll("path").remove();
if (layerIsOn("toggleRoutes")) drawRoutes();
const links = (pack.cells.routes = {});
for (const route of pack.routes) {
for (let i = 0; i < route.points.length - 1; i++) {
const cellId = route.points[i][2];
const nextCellId = route.points[i + 1][2];
if (cellId !== nextCellId) {
if (!links[cellId]) links[cellId] = {};
links[cellId][nextCellId] = route.i;
if (!links[nextCellId]) links[nextCellId] = {};
links[nextCellId][cellId] = route.i;
}
}
}
}
} }

View file

@ -966,7 +966,7 @@ function dragStateBrush() {
const p = d3.mouse(this); const p = d3.mouse(this);
moveCircle(p[0], p[1], r); moveCircle(p[0], p[1], r);
const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)]; const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1])];
const selection = found.filter(isLand); const selection = found.filter(isLand);
if (selection) changeStateForSelection(selection); if (selection) changeStateForSelection(selection);
}); });

View file

@ -52,7 +52,8 @@ function getMinimalDataJson() {
provinces: pack.provinces, provinces: pack.provinces,
religions: pack.religions, religions: pack.religions,
rivers: pack.rivers, rivers: pack.rivers,
markers: pack.markers markers: pack.markers,
routes: pack.routes
}; };
return JSON.stringify({info, settings, mapCoordinates, pack: packData, biomesData, notes, nameBases}); return JSON.stringify({info, settings, mapCoordinates, pack: packData, biomesData, notes, nameBases});
} }
@ -85,7 +86,7 @@ function getMapInfo() {
function getSettings() { function getSettings() {
return { return {
distanceUnit: distanceUnitInput.value, distanceUnit: distanceUnitInput.value,
distanceScale: distanceScaleInput.value, distanceScale,
areaUnit: areaUnit.value, areaUnit: areaUnit.value,
heightUnit: heightUnit.value, heightUnit: heightUnit.value,
heightExponent: heightExponentInput.value, heightExponent: heightExponentInput.value,
@ -106,7 +107,7 @@ function getSettings() {
} }
function getPackCellsData() { function getPackCellsData() {
const dataArrays = { const data = {
v: pack.cells.v, v: pack.cells.v,
c: pack.cells.c, c: pack.cells.c,
p: pack.cells.p, p: pack.cells.p,
@ -125,8 +126,7 @@ function getPackCellsData() {
pop: Array.from(pack.cells.pop), pop: Array.from(pack.cells.pop),
culture: Array.from(pack.cells.culture), culture: Array.from(pack.cells.culture),
burg: Array.from(pack.cells.burg), burg: Array.from(pack.cells.burg),
road: Array.from(pack.cells.road), routes: pack.cells.routes,
crossroad: Array.from(pack.cells.crossroad),
state: Array.from(pack.cells.state), state: Array.from(pack.cells.state),
religion: Array.from(pack.cells.religion), religion: Array.from(pack.cells.religion),
province: Array.from(pack.cells.province) province: Array.from(pack.cells.province)
@ -135,29 +135,28 @@ function getPackCellsData() {
return { return {
cells: Array.from(pack.cells.i).map(cellId => ({ cells: Array.from(pack.cells.i).map(cellId => ({
i: cellId, i: cellId,
v: dataArrays.v[cellId], v: data.v[cellId],
c: dataArrays.c[cellId], c: data.c[cellId],
p: dataArrays.p[cellId], p: data.p[cellId],
g: dataArrays.g[cellId], g: data.g[cellId],
h: dataArrays.h[cellId], h: data.h[cellId],
area: dataArrays.area[cellId], area: data.area[cellId],
f: dataArrays.f[cellId], f: data.f[cellId],
t: dataArrays.t[cellId], t: data.t[cellId],
haven: dataArrays.haven[cellId], haven: data.haven[cellId],
harbor: dataArrays.harbor[cellId], harbor: data.harbor[cellId],
fl: dataArrays.fl[cellId], fl: data.fl[cellId],
r: dataArrays.r[cellId], r: data.r[cellId],
conf: dataArrays.conf[cellId], conf: data.conf[cellId],
biome: dataArrays.biome[cellId], biome: data.biome[cellId],
s: dataArrays.s[cellId], s: data.s[cellId],
pop: dataArrays.pop[cellId], pop: data.pop[cellId],
culture: dataArrays.culture[cellId], culture: data.culture[cellId],
burg: dataArrays.burg[cellId], burg: data.burg[cellId],
road: dataArrays.road[cellId], routes: data.routes[cellId],
crossroad: dataArrays.crossroad[cellId], state: data.state[cellId],
state: dataArrays.state[cellId], religion: data.religion[cellId],
religion: dataArrays.religion[cellId], province: data.province[cellId]
province: dataArrays.province[cellId]
})), })),
vertices: Array.from(pack.vertices.p).map((_, vertexId) => ({ vertices: Array.from(pack.vertices.p).map((_, vertexId) => ({
i: vertexId, i: vertexId,
@ -172,7 +171,8 @@ function getPackCellsData() {
provinces: pack.provinces, provinces: pack.provinces,
religions: pack.religions, religions: pack.religions,
rivers: pack.rivers, rivers: pack.rivers,
markers: pack.markers markers: pack.markers,
routes: pack.routes
}; };
} }

View file

@ -1,5 +1,3 @@
import {rollups} from "../../../utils/functionUtils.js";
const entitiesMap = { const entitiesMap = {
states: { states: {
label: "State", label: "State",

View file

@ -471,17 +471,23 @@ function saveGeoJSON_Cells() {
} }
function saveGeoJSON_Routes() { function saveGeoJSON_Routes() {
const json = {type: "FeatureCollection", features: []}; const {cells, burgs} = pack;
let points = cells.p.map(([x, y], cellId) => {
routes.selectAll("g > path").each(function () { const burgId = cells.burg[cellId];
const coordinates = getRoutePoints(this); if (burgId) return [burgs[burgId].x, burgs[burgId].y];
const id = this.id; return [x, y];
const type = this.parentElement.id;
const feature = {type: "Feature", geometry: {type: "LineString", coordinates}, properties: {id, type}};
json.features.push(feature);
}); });
const features = pack.routes.map(route => {
const coordinates = route.points || getRoutePoints(route, points);
return {
type: "Feature",
geometry: {type: "LineString", coordinates},
properties: {id: route.id, group: route.group}
};
});
const json = {type: "FeatureCollection", features};
const fileName = getFileName("Routes") + ".geojson"; const fileName = getFileName("Routes") + ".geojson";
downloadFile(JSON.stringify(json), fileName, "application/json"); downloadFile(JSON.stringify(json), fileName, "application/json");
} }
@ -525,17 +531,6 @@ function getCellCoordinates(vertices) {
return [coordinates.concat([coordinates[0]])]; return [coordinates.concat([coordinates[0]])];
} }
function getRoutePoints(node) {
let points = [];
const l = node.getTotalLength();
const increment = l / Math.ceil(l / 2);
for (let i = 0; i <= l; i += increment) {
const p = node.getPointAtLength(i);
points.push(getCoordinates(p.x, p.y, 4));
}
return points;
}
function getRiverPoints(node) { function getRiverPoints(node) {
let points = []; let points = [];
const l = node.getTotalLength() / 2; // half-length const l = node.getTotalLength() / 2; // half-length

View file

@ -379,6 +379,7 @@ async function parseLoadedData(data, mapVersion) {
pack.provinces = data[30] ? JSON.parse(data[30]) : [0]; pack.provinces = data[30] ? JSON.parse(data[30]) : [0];
pack.rivers = data[32] ? JSON.parse(data[32]) : []; pack.rivers = data[32] ? JSON.parse(data[32]) : [];
pack.markers = data[35] ? JSON.parse(data[35]) : []; pack.markers = data[35] ? JSON.parse(data[35]) : [];
pack.routes = data[37] ? JSON.parse(data[37]) : [];
const cells = pack.cells; const cells = pack.cells;
cells.biome = Uint8Array.from(data[16].split(",")); cells.biome = Uint8Array.from(data[16].split(","));
@ -388,12 +389,13 @@ async function parseLoadedData(data, mapVersion) {
cells.fl = Uint16Array.from(data[20].split(",")); cells.fl = Uint16Array.from(data[20].split(","));
cells.pop = Float32Array.from(data[21].split(",")); cells.pop = Float32Array.from(data[21].split(","));
cells.r = Uint16Array.from(data[22].split(",")); cells.r = Uint16Array.from(data[22].split(","));
cells.road = Uint16Array.from(data[23].split(",")); // data[23] for deprecated cells.road
cells.s = Uint16Array.from(data[24].split(",")); cells.s = Uint16Array.from(data[24].split(","));
cells.state = Uint16Array.from(data[25].split(",")); cells.state = Uint16Array.from(data[25].split(","));
cells.religion = data[26] ? Uint16Array.from(data[26].split(",")) : new Uint16Array(cells.i.length); cells.religion = data[26] ? Uint16Array.from(data[26].split(",")) : new Uint16Array(cells.i.length);
cells.province = data[27] ? Uint16Array.from(data[27].split(",")) : new Uint16Array(cells.i.length); cells.province = data[27] ? Uint16Array.from(data[27].split(",")) : new Uint16Array(cells.i.length);
cells.crossroad = data[28] ? Uint16Array.from(data[28].split(",")) : new Uint16Array(cells.i.length); // data[28] for deprecated cells.crossroad
cells.routes = data[36] ? JSON.parse(data[36]) : {};
if (data[31]) { if (data[31]) {
const namesDL = data[31].split("/"); const namesDL = data[31].split("/");
@ -461,7 +463,7 @@ async function parseLoadedData(data, mapVersion) {
{ {
// dynamically import and run auto-update script // dynamically import and run auto-update script
const versionNumber = parseFloat(params[0]); const versionNumber = parseFloat(params[0]);
const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.98.06"); const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.99.00");
resolveVersionConflicts(versionNumber); resolveVersionConflicts(versionNumber);
} }

View file

@ -44,7 +44,7 @@ function prepareMapData() {
const params = [version, license, dateString, seed, graphWidth, graphHeight, mapId].join("|"); const params = [version, license, dateString, seed, graphWidth, graphHeight, mapId].join("|");
const settings = [ const settings = [
distanceUnitInput.value, distanceUnitInput.value,
distanceScaleInput.value, distanceScale,
areaUnit.value, areaUnit.value,
heightUnit.value, heightUnit.value,
heightExponentInput.value, heightExponentInput.value,
@ -98,6 +98,8 @@ function prepareMapData() {
const provinces = JSON.stringify(pack.provinces); const provinces = JSON.stringify(pack.provinces);
const rivers = JSON.stringify(pack.rivers); const rivers = JSON.stringify(pack.rivers);
const markers = JSON.stringify(pack.markers); const markers = JSON.stringify(pack.markers);
const cellRoutes = JSON.stringify(pack.cells.routes);
const routes = JSON.stringify(pack.routes);
// store name array only if not the same as default // store name array only if not the same as default
const defaultNB = Names.getNameBases(); const defaultNB = Names.getNameBases();
@ -136,19 +138,21 @@ function prepareMapData() {
pack.cells.fl, pack.cells.fl,
pop, pop,
pack.cells.r, pack.cells.r,
pack.cells.road, [], // deprecated pack.cells.road
pack.cells.s, pack.cells.s,
pack.cells.state, pack.cells.state,
pack.cells.religion, pack.cells.religion,
pack.cells.province, pack.cells.province,
pack.cells.crossroad, [], // deprecated pack.cells.crossroad
religions, religions,
provinces, provinces,
namesData, namesData,
rivers, rivers,
rulersString, rulersString,
fonts, fonts,
markers markers,
cellRoutes,
routes
].join("\r\n"); ].join("\r\n");
return mapData; return mapData;
} }

View file

@ -27,7 +27,7 @@ window.Markers = (function () {
{type: "water-sources", icon: "💧", min: 1, each: 1000, multiplier: 1, list: listWaterSources, add: addWaterSource}, {type: "water-sources", icon: "💧", min: 1, each: 1000, multiplier: 1, list: listWaterSources, add: addWaterSource},
{type: "mines", icon: "⛏️", dx: 48, px: 13, min: 1, each: 15, multiplier: 1, list: listMines, add: addMine}, {type: "mines", icon: "⛏️", dx: 48, px: 13, min: 1, each: 15, multiplier: 1, list: listMines, add: addMine},
{type: "bridges", icon: "🌉", px: 14, min: 1, each: 5, multiplier: 1, list: listBridges, add: addBridge}, {type: "bridges", icon: "🌉", px: 14, min: 1, each: 5, multiplier: 1, list: listBridges, add: addBridge},
{type: "inns", icon: "🍻", px: 14, min: 1, each: 100, multiplier: 1, list: listInns, add: addInn}, {type: "inns", icon: "🍻", px: 14, min: 1, each: 10, multiplier: 1, list: listInns, add: addInn},
{type: "lighthouses", icon: "🚨", px: 14, min: 1, each: 2, multiplier: 1, list: listLighthouses, add: addLighthouse}, {type: "lighthouses", icon: "🚨", px: 14, min: 1, each: 2, multiplier: 1, list: listLighthouses, add: addLighthouse},
{type: "waterfalls", icon: "⟱", dy: 54, px: 16, min: 1, each: 5, multiplier: 1, list: listWaterfalls, add: addWaterfall}, {type: "waterfalls", icon: "⟱", dy: 54, px: 16, min: 1, each: 5, multiplier: 1, list: listWaterfalls, add: addWaterfall},
{type: "battlefields", icon: "⚔️", dy: 52, min: 50, each: 700, multiplier: 1, list: listBattlefields, add: addBattlefield}, {type: "battlefields", icon: "⚔️", dy: 52, min: 50, each: 700, multiplier: 1, list: listBattlefields, add: addBattlefield},
@ -279,7 +279,8 @@ window.Markers = (function () {
} }
function listInns({cells}) { function listInns({cells}) {
return cells.i.filter(i => !occupied[i] && cells.h[i] >= 20 && cells.road[i] > 4 && cells.pop[i] > 10); const crossRoads = cells.i.filter(i => !occupied[i] && cells.pop[i] > 5 && Routes.isCrossroad(i));
return crossRoads;
} }
function addInn(id, cell) { function addInn(id, cell) {
@ -542,7 +543,7 @@ window.Markers = (function () {
function listLighthouses({cells}) { function listLighthouses({cells}) {
return cells.i.filter( return cells.i.filter(
i => !occupied[i] && cells.harbor[i] > 6 && cells.c[i].some(c => cells.h[c] < 20 && cells.road[c]) i => !occupied[i] && cells.harbor[i] > 6 && cells.c[i].some(c => cells.h[c] < 20 && Routes.isConnected(c))
); );
} }
@ -642,7 +643,7 @@ window.Markers = (function () {
function listSeaMonsters({cells, features}) { function listSeaMonsters({cells, features}) {
return cells.i.filter( return cells.i.filter(
i => !occupied[i] && cells.h[i] < 20 && cells.road[i] && features[cells.f[i]].type === "ocean" i => !occupied[i] && cells.h[i] < 20 && Routes.isConnected(i) && features[cells.f[i]].type === "ocean"
); );
} }
@ -792,7 +793,7 @@ window.Markers = (function () {
cells.religion[i] && cells.religion[i] &&
cells.biome[i] === 1 && cells.biome[i] === 1 &&
cells.pop[i] > 1 && cells.pop[i] > 1 &&
cells.road[i] Routes.isConnected(i)
); );
} }
@ -807,7 +808,7 @@ window.Markers = (function () {
} }
function listBrigands({cells}) { function listBrigands({cells}) {
return cells.i.filter(i => !occupied[i] && cells.culture[i] && cells.road[i] > 4); return cells.i.filter(i => !occupied[i] && cells.culture[i] && Routes.hasRoad(i));
} }
function addBrigands(id, cell) { function addBrigands(id, cell) {
@ -867,7 +868,7 @@ window.Markers = (function () {
// Pirates spawn on sea routes // Pirates spawn on sea routes
function listPirates({cells}) { function listPirates({cells}) {
return cells.i.filter(i => !occupied[i] && cells.h[i] < 20 && cells.road[i]); return cells.i.filter(i => !occupied[i] && cells.h[i] < 20 && Routes.isConnected(i));
} }
function addPirates(id, cell) { function addPirates(id, cell) {
@ -961,7 +962,7 @@ window.Markers = (function () {
} }
function listCircuses({cells}) { function listCircuses({cells}) {
return cells.i.filter(i => !occupied[i] && cells.culture[i] && cells.h[i] >= 20 && pack.cells.road[i]); return cells.i.filter(i => !occupied[i] && cells.culture[i] && cells.h[i] >= 20 && Routes.isConnected(i));
} }
function addCircuse(id, cell) { function addCircuse(id, cell) {
@ -1254,16 +1255,16 @@ window.Markers = (function () {
const name = `${toponym} ${type}`; const name = `${toponym} ${type}`;
const legend = ra([ const legend = ra([
"A foreboding necropolis shrouded in perpetual darkness, where eerie whispers echo through the winding corridors and spectral guardians stand watch over the tombs of long-forgotten souls", "A foreboding necropolis shrouded in perpetual darkness, where eerie whispers echo through the winding corridors and spectral guardians stand watch over the tombs of long-forgotten souls.",
"A towering necropolis adorned with macabre sculptures and guarded by formidable undead sentinels. Its ancient halls house the remains of fallen heroes, entombed alongside their cherished relics", "A towering necropolis adorned with macabre sculptures and guarded by formidable undead sentinels. Its ancient halls house the remains of fallen heroes, entombed alongside their cherished relics.",
"This ethereal necropolis seems suspended between the realms of the living and the dead. Wisps of mist dance around the tombstones, while haunting melodies linger in the air, commemorating the departed", "This ethereal necropolis seems suspended between the realms of the living and the dead. Wisps of mist dance around the tombstones, while haunting melodies linger in the air, commemorating the departed.",
"Rising from the desolate landscape, this sinister necropolis is a testament to necromantic power. Its skeletal spires cast ominous shadows, concealing forbidden knowledge and arcane secrets", "Rising from the desolate landscape, this sinister necropolis is a testament to necromantic power. Its skeletal spires cast ominous shadows, concealing forbidden knowledge and arcane secrets.",
"An eerie necropolis where nature intertwines with death. Overgrown tombstones are entwined by thorny vines, and mournful spirits wander among the fading petals of once-vibrant flowers", "An eerie necropolis where nature intertwines with death. Overgrown tombstones are entwined by thorny vines, and mournful spirits wander among the fading petals of once-vibrant flowers.",
"A labyrinthine necropolis where each step echoes with haunting murmurs. The walls are adorned with ancient runes, and restless spirits guide or hinder those who dare to delve into its depths", "A labyrinthine necropolis where each step echoes with haunting murmurs. The walls are adorned with ancient runes, and restless spirits guide or hinder those who dare to delve into its depths.",
"This cursed necropolis is veiled in perpetual twilight, perpetuating a sense of impending doom. Dark enchantments shroud the tombs, and the moans of anguished souls resound through its crumbling halls", "This cursed necropolis is veiled in perpetual twilight, perpetuating a sense of impending doom. Dark enchantments shroud the tombs, and the moans of anguished souls resound through its crumbling halls.",
"A sprawling necropolis built within a labyrinthine network of catacombs. Its halls are lined with countless alcoves, each housing the remains of the departed, while the distant sound of rattling bones fills the air", "A sprawling necropolis built within a labyrinthine network of catacombs. Its halls are lined with countless alcoves, each housing the remains of the departed, while the distant sound of rattling bones fills the air.",
"A desolate necropolis where an eerie stillness reigns. Time seems frozen amidst the decaying mausoleums, and the silence is broken only by the whispers of the wind and the rustle of tattered banners", "A desolate necropolis where an eerie stillness reigns. Time seems frozen amidst the decaying mausoleums, and the silence is broken only by the whispers of the wind and the rustle of tattered banners.",
"A foreboding necropolis perched atop a jagged cliff, overlooking a desolate wasteland. Its towering walls harbor restless spirits, and the imposing gates bear the marks of countless battles and ancient curses" "A foreboding necropolis perched atop a jagged cliff, overlooking a desolate wasteland. Its towering walls harbor restless spirits, and the imposing gates bear the marks of countless battles and ancient curses."
]); ]);
notes.push({id, name, legend}); notes.push({id, name, legend});

View file

@ -692,7 +692,7 @@ window.Religions = (function () {
// growth algorithm to assign cells to religions // growth algorithm to assign cells to religions
function expandReligions(religions) { function expandReligions(religions) {
const cells = pack.cells; 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 PriorityQueue({comparator: (a, b) => a.p - b.p});
@ -700,8 +700,6 @@ window.Religions = (function () {
const maxExpansionCost = (cells.i.length / 20) * neutralInput.value; // limit cost for organized religions growth const maxExpansionCost = (cells.i.length / 20) * neutralInput.value; // limit cost for organized religions growth
const biomePassageCost = cellId => biomesData.cost[cells.biome[cellId]];
religions religions
.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 => {
@ -712,11 +710,6 @@ window.Religions = (function () {
const religionsMap = new Map(religions.map(r => [r.i, r])); const religionsMap = new Map(religions.map(r => [r.i, r]));
const isMainRoad = cellId => cells.road[cellId] - cells.crossroad[cellId] > 4;
const isTrail = cellId => cells.h[cellId] > 19 && cells.road[cellId] - cells.crossroad[cellId] === 1;
const isSeaRoute = cellId => cells.h[cellId] < 20 && cells.road[cellId];
const isWater = cellId => cells.h[cellId] < 20;
while (queue.length) { while (queue.length) {
const {e: cellId, p, r, s: state} = queue.dequeue(); const {e: cellId, p, r, s: state} = queue.dequeue();
const {culture, expansion, expansionism} = religionsMap.get(r); const {culture, expansion, expansionism} = religionsMap.get(r);
@ -728,7 +721,7 @@ window.Religions = (function () {
const cultureCost = culture !== cells.culture[nextCell] ? 10 : 0; const cultureCost = culture !== cells.culture[nextCell] ? 10 : 0;
const stateCost = state !== cells.state[nextCell] ? 10 : 0; const stateCost = state !== cells.state[nextCell] ? 10 : 0;
const passageCost = getPassageCost(nextCell); const passageCost = getPassageCost(cellId, nextCell);
const cellCost = cultureCost + stateCost + passageCost; const cellCost = cultureCost + stateCost + passageCost;
const totalCost = p + 10 + cellCost / expansionism; const totalCost = p + 10 + cellCost / expansionism;
@ -745,11 +738,18 @@ window.Religions = (function () {
return religionIds; return religionIds;
function getPassageCost(cellId) { function getPassageCost(cellId, nextCellId) {
if (isWater(cellId)) return isSeaRoute ? 50 : 500; const route = Routes.getRoute(cellId, nextCellId);
if (isMainRoad(cellId)) return 1; if (isWater(cellId)) return route ? 50 : 500;
const biomeCost = biomePassageCost(cellId);
return isTrail(cellId) ? biomeCost / 1.5 : biomeCost; const biomePassageCost = biomesData.cost[cells.biome[nextCellId]];
if (route) {
if (route.group === "roads") return 1;
return biomePassageCost / 3; // trails and other routes
}
return biomePassageCost;
} }
} }

View file

@ -1,269 +1,743 @@
const ROUTES_SHARP_ANGLE = 135;
const ROUTES_VERY_SHARP_ANGLE = 115;
window.Routes = (function () { window.Routes = (function () {
const getRoads = function () { function generate(lockedRoutes = []) {
TIME && console.time("generateMainRoads"); const {capitalsByFeature, burgsByFeature, portsByFeature} = sortBurgsByFeature(pack.burgs);
const cells = pack.cells;
const burgs = pack.burgs.filter(b => b.i && !b.removed);
const capitals = burgs.filter(b => b.capital).sort((a, b) => a.population - b.population);
if (capitals.length < 2) return []; // not enough capitals to build main roads const connections = new Map();
const paths = []; // array to store path segments lockedRoutes.forEach(route => addConnections(route.points.map(p => p[2])));
for (const b of capitals) { const mainRoads = generateMainRoads();
const connect = capitals.filter(c => c.feature === b.feature && c !== b); const trails = generateTrails();
for (const t of connect) { const seaRoutes = generateSeaRoutes();
const [from, exit] = findLandPath(b.cell, t.cell, true);
const segments = restorePath(b.cell, exit, "main", from);
segments.forEach(s => paths.push(s));
}
}
cells.i.forEach(i => (cells.s[i] += cells.road[i] / 2)); // add roads to suitability score pack.routes = createRoutesData(lockedRoutes);
TIME && console.timeEnd("generateMainRoads"); pack.cells.routes = buildLinks(pack.routes);
return paths;
function sortBurgsByFeature(burgs) {
const burgsByFeature = {};
const capitalsByFeature = {};
const portsByFeature = {};
const addBurg = (object, feature, burg) => {
if (!object[feature]) object[feature] = [];
object[feature].push(burg);
}; };
const getTrails = function () { for (const burg of burgs) {
TIME && console.time("generateTrails"); if (burg.i && !burg.removed) {
const cells = pack.cells; const {feature, capital, port} = burg;
const burgs = pack.burgs.filter(b => b.i && !b.removed); addBurg(burgsByFeature, feature, burg);
if (capital) addBurg(capitalsByFeature, feature, burg);
if (burgs.length < 2) return []; // not enough burgs to build trails if (port) addBurg(portsByFeature, port, burg);
}
let paths = []; // array to store path segments }
for (const f of pack.features.filter(f => f.land)) {
const isle = burgs.filter(b => b.feature === f.i); // burgs on island return {burgsByFeature, capitalsByFeature, portsByFeature};
if (isle.length < 2) continue; }
isle.forEach(function (b, i) { function generateMainRoads() {
let path = []; TIME && console.time("generateMainRoads");
if (!i) { const mainRoads = [];
// build trail from the first burg on island
// to the farthest one on the same island or the closest road for (const [key, featureCapitals] of Object.entries(capitalsByFeature)) {
const farthest = d3.scan(isle, (a, c) => (c.y - b.y) ** 2 + (c.x - b.x) ** 2 - ((a.y - b.y) ** 2 + (a.x - b.x) ** 2)); const points = featureCapitals.map(burg => [burg.x, burg.y]);
const to = isle[farthest].cell; const urquhartEdges = calculateUrquhartEdges(points);
if (cells.road[to]) return; urquhartEdges.forEach(([fromId, toId]) => {
const [from, exit] = findLandPath(b.cell, to, true); const start = featureCapitals[fromId].cell;
path = restorePath(b.cell, exit, "small", from); const exit = featureCapitals[toId].cell;
} else {
// build trail from all other burgs to the closest road on the same island const segments = findPathSegments({isWater: false, connections, start, exit});
if (cells.road[b.cell]) return; for (const segment of segments) {
const [from, exit] = findLandPath(b.cell, null, true); addConnections(segment);
if (exit === null) return; mainRoads.push({feature: Number(key), cells: segment});
path = restorePath(b.cell, exit, "small", from); }
});
}
TIME && console.timeEnd("generateMainRoads");
return mainRoads;
}
function generateTrails() {
TIME && console.time("generateTrails");
const trails = [];
for (const [key, featureBurgs] of Object.entries(burgsByFeature)) {
const points = featureBurgs.map(burg => [burg.x, burg.y]);
const urquhartEdges = calculateUrquhartEdges(points);
urquhartEdges.forEach(([fromId, toId]) => {
const start = featureBurgs[fromId].cell;
const exit = featureBurgs[toId].cell;
const segments = findPathSegments({isWater: false, connections, start, exit});
for (const segment of segments) {
addConnections(segment);
trails.push({feature: Number(key), cells: segment});
} }
if (path) paths = paths.concat(path);
}); });
} }
TIME && console.timeEnd("generateTrails"); TIME && console.timeEnd("generateTrails");
return paths; return trails;
};
const getSearoutes = function () {
TIME && console.time("generateSearoutes");
const {cells, burgs, features} = pack;
const allPorts = burgs.filter(b => b.port > 0 && !b.removed);
if (!allPorts.length) return [];
const bodies = new Set(allPorts.map(b => b.port)); // water features with ports
let paths = []; // array to store path segments
const connected = []; // store cell id of connected burgs
bodies.forEach(f => {
const ports = allPorts.filter(b => b.port === f); // all ports on the same feature
if (!ports.length) return;
if (features[f]?.border) addOverseaRoute(f, ports[0]);
// get inner-map routes
for (let s = 0; s < ports.length; s++) {
const source = ports[s].cell;
if (connected[source]) continue;
for (let t = s + 1; t < ports.length; t++) {
const target = ports[t].cell;
if (connected[target]) continue;
const [from, exit, passable] = findOceanPath(target, source, true);
if (!passable) continue;
const path = restorePath(target, exit, "ocean", from);
paths = paths.concat(path);
connected[source] = 1;
connected[target] = 1;
} }
function generateSeaRoutes() {
TIME && console.time("generateSeaRoutes");
const seaRoutes = [];
for (const [featureId, featurePorts] of Object.entries(portsByFeature)) {
const points = featurePorts.map(burg => [burg.x, burg.y]);
const urquhartEdges = calculateUrquhartEdges(points);
urquhartEdges.forEach(([fromId, toId]) => {
const start = featurePorts[fromId].cell;
const exit = featurePorts[toId].cell;
const segments = findPathSegments({isWater: true, connections, start, exit});
for (const segment of segments) {
addConnections(segment);
seaRoutes.push({feature: Number(featureId), cells: segment});
} }
}); });
}
function addOverseaRoute(f, port) { TIME && console.timeEnd("generateSeaRoutes");
const {x, y, cell: source} = port; return seaRoutes;
const dist = p => Math.abs(p[0] - x) + Math.abs(p[1] - y); }
const [x1, y1] = [
[0, y],
[x, 0],
[graphWidth, y],
[x, graphHeight]
].sort((a, b) => dist(a) - dist(b))[0];
const target = findCell(x1, y1);
if (cells.f[target] === f && cells.h[target] < 20) { function addConnections(segment) {
const [from, exit, passable] = findOceanPath(target, source, true); for (let i = 0; i < segment.length; i++) {
const cellId = segment[i];
if (passable) { const nextCellId = segment[i + 1];
const path = restorePath(target, exit, "ocean", from); if (nextCellId) {
paths = paths.concat(path); connections.set(`${cellId}-${nextCellId}`, true);
last(path).push([x1, y1]); connections.set(`${nextCellId}-${cellId}`, true);
} }
} }
} }
TIME && console.timeEnd("generateSearoutes"); function findPathSegments({isWater, connections, start, exit}) {
return paths; const from = findPath(isWater, start, exit, connections);
}; if (!from) return [];
const draw = function (main, small, water) { const pathCells = restorePath(start, exit, from);
TIME && console.time("drawRoutes"); const segments = getRouteSegments(pathCells, connections);
return segments;
}
function createRoutesData(routes) {
const pointsArray = preparePointsArray();
for (const {feature, cells, merged} of mergeRoutes(mainRoads)) {
if (merged) continue;
const points = getPoints("roads", cells, pointsArray);
routes.push({i: routes.length, group: "roads", feature, points});
}
for (const {feature, cells, merged} of mergeRoutes(trails)) {
if (merged) continue;
const points = getPoints("trails", cells, pointsArray);
routes.push({i: routes.length, group: "trails", feature, points});
}
for (const {feature, cells, merged} of mergeRoutes(seaRoutes)) {
if (merged) continue;
const points = getPoints("searoutes", cells, pointsArray);
routes.push({i: routes.length, group: "searoutes", feature, points});
}
return routes;
}
// merge routes so that the last cell of one route is the first cell of the next route
function mergeRoutes(routes) {
let routesMerged = 0;
for (let i = 0; i < routes.length; i++) {
const thisRoute = routes[i];
if (thisRoute.merged) continue;
for (let j = i + 1; j < routes.length; j++) {
const nextRoute = routes[j];
if (nextRoute.merged) continue;
if (nextRoute.cells.at(0) === thisRoute.cells.at(-1)) {
routesMerged++;
thisRoute.cells = thisRoute.cells.concat(nextRoute.cells.slice(1));
nextRoute.merged = true;
}
}
}
return routesMerged > 1 ? mergeRoutes(routes) : routes;
}
function buildLinks(routes) {
const links = {};
for (const {points, i: routeId} of routes) {
const cells = points.map(p => p[2]);
for (let i = 0; i < cells.length - 1; i++) {
const cellId = cells[i];
const nextCellId = cells[i + 1];
if (cellId !== nextCellId) {
if (!links[cellId]) links[cellId] = {};
links[cellId][nextCellId] = routeId;
if (!links[nextCellId]) links[nextCellId] = {};
links[nextCellId][cellId] = routeId;
}
}
}
return links;
}
}
function preparePointsArray() {
const {cells, burgs} = pack; const {cells, burgs} = pack;
const {burg, p} = cells; return cells.p.map(([x, y], cellId) => {
const burgId = cells.burg[cellId];
if (burgId) return [burgs[burgId].x, burgs[burgId].y];
return [x, y];
});
}
const getBurgCoords = b => [burgs[b].x, burgs[b].y]; function getPoints(group, cells, points) {
const getPathPoints = cells => cells.map(i => (Array.isArray(i) ? i : burg[i] ? getBurgCoords(burg[i]) : p[i])); const data = cells.map(cellId => [...points[cellId], cellId]);
const getPath = segment => round(lineGen(getPathPoints(segment)), 1);
const getPathsHTML = (paths, type) => paths.map((path, i) => `<path id="${type}${i}" d="${getPath(path)}" />`).join("");
lineGen.curve(d3.curveCatmullRom.alpha(0.1)); // resolve sharp angles
roads.html(getPathsHTML(main, "road")); if (group !== "searoutes") {
trails.html(getPathsHTML(small, "trail")); for (let i = 1; i < cells.length - 1; i++) {
const cellId = cells[i];
if (pack.cells.burg[cellId]) continue;
lineGen.curve(d3.curveBundle.beta(1)); const [prevX, prevY] = data[i - 1];
searoutes.html(getPathsHTML(water, "searoute")); const [currX, currY] = data[i];
const [nextX, nextY] = data[i + 1];
TIME && console.timeEnd("drawRoutes"); const dAx = prevX - currX;
const dAy = prevY - currY;
const dBx = nextX - currX;
const dBy = nextY - currY;
const angle = Math.abs((Math.atan2(dAx * dBy - dAy * dBx, dAx * dBx + dAy * dBy) * 180) / Math.PI);
if (angle < ROUTES_SHARP_ANGLE) {
const middleX = (prevX + nextX) / 2;
const middleY = (prevY + nextY) / 2;
let newX, newY;
if (angle < ROUTES_VERY_SHARP_ANGLE) {
newX = rn((currX + middleX * 2) / 3, 2);
newY = rn((currY + middleY * 2) / 3, 2);
} else {
newX = rn((currX + middleX) / 2, 2);
newY = rn((currY + middleY) / 2, 2);
}
if (findCell(newX, newY) === cellId) {
data[i] = [newX, newY, cellId];
points[cellId] = [data[i][0], data[i][1]]; // change cell coordinate for all routes
}
}
}
}
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
}; };
const regenerate = function () { function findPath(isWater, start, exit, connections) {
routes.selectAll("path").remove(); const {temp} = grid.cells;
pack.cells.road = new Uint16Array(pack.cells.i.length); const {cells} = pack;
pack.cells.crossroad = new Uint16Array(pack.cells.i.length);
const main = getRoads();
const small = getTrails();
const water = getSearoutes();
draw(main, small, water);
};
return {getRoads, getTrails, getSearoutes, draw, regenerate}; const from = [];
const cost = [];
const queue = new FlatQueue();
queue.push(start, 0);
// Find a land path to a specific cell (exit), to a closest road (toRoad), or to all reachable cells (null, null) return isWater ? findWaterPath() : findLandPath();
function findLandPath(start, exit = null, toRoad = null) {
const cells = pack.cells; function findLandPath() {
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); while (queue.length) {
const cost = [], const priority = queue.peekValue();
from = []; const next = queue.pop();
queue.queue({e: start, p: 0});
for (const neibCellId of cells.c[next]) {
if (neibCellId === exit) {
from[neibCellId] = next;
return from;
}
if (cells.h[neibCellId] < 20) continue; // ignore water cells
const habitability = biomesData.habitability[cells.biome[neibCellId]];
if (!habitability) continue; // inhabitable cells are not passable (eg. lava, glacier)
const distanceCost = dist2(cells.p[next], cells.p[neibCellId]);
const habitabilityModifier = 1 + Math.max(100 - habitability, 0) / 1000; // [1, 1.1];
const heightModifier = 1 + Math.max(cells.h[neibCellId] - 25, 25) / 25; // [1, 3];
const connectionModifier = connections.has(`${next}-${neibCellId}`) ? 1 : 2;
const burgModifier = cells.burg[neibCellId] ? 1 : 3;
const cellsCost = distanceCost * habitabilityModifier * heightModifier * connectionModifier * burgModifier;
const totalCost = priority + cellsCost;
if (totalCost >= cost[neibCellId]) continue;
from[neibCellId] = next;
cost[neibCellId] = totalCost;
queue.push(neibCellId, totalCost);
}
}
return null; // path is not found
}
function findWaterPath() {
while (queue.length) {
const priority = queue.peekValue();
const next = queue.pop();
for (const neibCellId of cells.c[next]) {
if (neibCellId === exit) {
from[neibCellId] = next;
return from;
}
if (cells.h[neibCellId] >= 20) continue; // ignore land cells
if (temp[cells.g[neibCellId]] < MIN_PASSABLE_SEA_TEMP) continue; // ignore too cold cells
const distanceCost = dist2(cells.p[next], cells.p[neibCellId]);
const typeModifier = TYPE_MODIFIERS[cells.t[neibCellId]] || TYPE_MODIFIERS.default;
const connectionModifier = connections.has(`${next}-${neibCellId}`) ? 1 : 2;
const cellsCost = distanceCost * typeModifier * connectionModifier;
const totalCost = priority + cellsCost;
if (totalCost >= cost[neibCellId]) continue;
from[neibCellId] = next;
cost[neibCellId] = totalCost;
queue.push(neibCellId, totalCost);
}
}
return null; // path is not found
}
}
function restorePath(start, end, from) {
const cells = [];
let current = end;
let prev = end;
while (current !== start) {
cells.push(current);
prev = from[current];
current = prev;
}
cells.push(current);
return cells;
}
function getRouteSegments(pathCells, connections) {
const segments = [];
let segment = [];
for (let i = 0; i < pathCells.length; i++) {
const cellId = pathCells[i];
const nextCellId = pathCells[i + 1];
const isConnected = connections.has(`${cellId}-${nextCellId}`) || connections.has(`${nextCellId}-${cellId}`);
if (isConnected) {
if (segment.length) {
// segment stepped into existing segment
segment.push(pathCells[i]);
segments.push(segment);
segment = [];
}
continue;
}
segment.push(pathCells[i]);
}
if (segment.length > 1) segments.push(segment);
return segments;
}
// Urquhart graph is obtained by removing the longest edge from each triangle in the Delaunay triangulation
// this gives us an aproximation of a desired road network, i.e. connections between burgs
// code from https://observablehq.com/@mbostock/urquhart-graph
function calculateUrquhartEdges(points) {
const score = (p0, p1) => dist2(points[p0], points[p1]);
const {halfedges, triangles} = Delaunator.from(points);
const n = triangles.length;
const removed = new Uint8Array(n);
const edges = [];
for (let e = 0; e < n; e += 3) {
const p0 = triangles[e],
p1 = triangles[e + 1],
p2 = triangles[e + 2];
const p01 = score(p0, p1),
p12 = score(p1, p2),
p20 = score(p2, p0);
removed[
p20 > p01 && p20 > p12
? Math.max(e + 2, halfedges[e + 2])
: p12 > p01 && p12 > p20
? Math.max(e + 1, halfedges[e + 1])
: Math.max(e, halfedges[e])
] = 1;
}
for (let e = 0; e < n; ++e) {
if (e > halfedges[e] && !removed[e]) {
const t0 = triangles[e];
const t1 = triangles[e % 3 === 2 ? e - 2 : e + 1];
edges.push([t0, t1]);
}
}
return edges;
}
// connect cell with routes system by land
function connect(cellId) {
if (isConnected(cellId)) return;
const {cells, routes} = pack;
const path = findConnectionPath(cellId);
if (!path) return;
const pathCells = restorePath(...path);
const pointsArray = preparePointsArray();
const points = getPoints("trails", pathCells, pointsArray);
const feature = cells.f[cellId];
const routeId = Math.max(...routes.map(route => route.i)) + 1;
const newRoute = {i: routeId, group: "trails", feature, points};
routes.push(newRoute);
for (let i = 0; i < pathCells.length; i++) {
const cellId = pathCells[i];
const nextCellId = pathCells[i + 1];
if (nextCellId) addConnection(cellId, nextCellId, routeId);
}
return newRoute;
function findConnectionPath(start) {
const from = [];
const cost = [];
const queue = new FlatQueue();
queue.push(start, 0);
while (queue.length) { while (queue.length) {
const next = queue.dequeue(), const priority = queue.peekValue();
n = next.e, const next = queue.pop();
p = next.p;
if (toRoad && cells.road[n]) return [from, n];
for (const c of cells.c[n]) { for (const neibCellId of cells.c[next]) {
if (cells.h[c] < 20) continue; // ignore water cells if (isConnected(neibCellId)) {
const stateChangeCost = cells.state && cells.state[c] !== cells.state[n] ? 400 : 0; // trails tend to lay within the same state from[neibCellId] = next;
const habitability = biomesData.habitability[cells.biome[c]]; return [start, neibCellId, from];
if (!habitability) continue; // avoid inhabitable cells (eg. lava, glacier)
const habitedCost = habitability ? Math.max(100 - habitability, 0) : 400; // routes tend to lay within populated areas
const heightChangeCost = Math.abs(cells.h[c] - cells.h[n]) * 10; // routes tend to avoid elevation changes
const heightCost = cells.h[c] > 80 ? cells.h[c] : 0; // routes tend to avoid mountainous areas
const cellCoast = 10 + stateChangeCost + habitedCost + heightChangeCost + heightCost;
const totalCost = p + (cells.road[c] || cells.burg[c] ? cellCoast / 3 : cellCoast);
if (from[c] || totalCost >= cost[c]) continue;
from[c] = n;
if (c === exit) return [from, exit];
cost[c] = totalCost;
queue.queue({e: c, p: totalCost});
}
}
return [from, exit];
} }
function restorePath(start, end, type, from) { if (cells.h[neibCellId] < 20) continue; // ignore water cells
const cells = pack.cells; const habitability = biomesData.habitability[cells.biome[neibCellId]];
const path = []; // to store all segments; if (!habitability) continue; // inhabitable cells are not passable (eg. lava, glacier)
let segment = [],
current = end,
prev = end;
const score = type === "main" ? 5 : 1; // to increase road score at cell
if (type === "ocean" || !cells.road[prev]) segment.push(end); const distanceCost = dist2(cells.p[next], cells.p[neibCellId]);
if (!cells.road[prev]) cells.road[prev] = score; 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];
for (let i = 0, limit = 1000; i < limit; i++) { const cellsCost = distanceCost * habitabilityModifier * heightModifier;
if (!from[current]) break; const totalCost = priority + cellsCost;
current = from[current];
if (cells.road[current]) { if (totalCost >= cost[neibCellId]) continue;
if (segment.length) { from[neibCellId] = next;
segment.push(current); cost[neibCellId] = totalCost;
path.push(segment); queue.push(neibCellId, totalCost);
if (segment[0] !== end) {
cells.road[segment[0]] += score;
cells.crossroad[segment[0]] += score;
}
if (current !== start) {
cells.road[current] += score;
cells.crossroad[current] += score;
} }
} }
segment = [];
prev = current; return null; // path is not found
} else {
if (prev) segment.push(prev);
prev = null;
segment.push(current);
} }
cells.road[current] += score; function addConnection(from, to, routeId) {
if (current === start) break; const routes = pack.cells.routes;
if (!routes[from]) routes[from] = {};
routes[from][to] = routeId;
if (!routes[to]) routes[to] = {};
routes[to][from] = routeId;
}
} }
if (segment.length > 1) path.push(segment); // utility functions
function isConnected(cellId) {
const {routes} = pack.cells;
return routes[cellId] && Object.keys(routes[cellId]).length > 0;
}
function areConnected(from, to) {
const routeId = pack.cells.routes[from]?.[to];
return routeId !== undefined;
}
function getRoute(from, to) {
const routeId = pack.cells.routes[from]?.[to];
return routeId === undefined ? null : pack.routes[routeId];
}
function hasRoad(cellId) {
const connections = pack.cells.routes[cellId];
if (!connections) return false;
return Object.values(connections).some(routeId => pack.routes[routeId].group === "roads");
}
function isCrossroad(cellId) {
const connections = pack.cells.routes[cellId];
if (!connections) return false;
return (
Object.keys(connections).length > 3 ||
Object.values(connections).filter(routeId => pack.routes[routeId].group === "roads").length > 2
);
}
// name generator data
const models = {
roads: {burg_suffix: 3, prefix_suffix: 6, the_descriptor_prefix_suffix: 2, the_descriptor_burg_suffix: 1},
trails: {burg_suffix: 8, prefix_suffix: 1, the_descriptor_burg_suffix: 1},
searoutes: {burg_suffix: 4, prefix_suffix: 2, the_descriptor_prefix_suffix: 1}
};
const prefixes = [
"King",
"Queen",
"Military",
"Old",
"New",
"Ancient",
"Royal",
"Imperial",
"Great",
"Grand",
"High",
"Silver",
"Dragon",
"Shadow",
"Star",
"Mystic",
"Whisper",
"Eagle",
"Golden",
"Crystal",
"Enchanted",
"Frost",
"Moon",
"Sun",
"Thunder",
"Phoenix",
"Sapphire",
"Celestial",
"Wandering",
"Echo",
"Twilight",
"Crimson",
"Serpent",
"Iron",
"Forest",
"Flower",
"Whispering",
"Eternal",
"Frozen",
"Rain",
"Luminous",
"Stardust",
"Arcane",
"Glimmering",
"Jade",
"Ember",
"Azure",
"Gilded",
"Divine",
"Shadowed",
"Cursed",
"Moonlit",
"Sable",
"Everlasting",
"Amber",
"Nightshade",
"Wraith",
"Scarlet",
"Platinum",
"Whirlwind",
"Obsidian",
"Ethereal",
"Ghost",
"Spike",
"Dusk",
"Raven",
"Spectral",
"Burning",
"Verdant",
"Copper",
"Velvet",
"Falcon",
"Enigma",
"Glowing",
"Silvered",
"Molten",
"Radiant",
"Astral",
"Wild",
"Flame",
"Amethyst",
"Aurora",
"Shadowy",
"Solar",
"Lunar",
"Whisperwind",
"Fading",
"Titan",
"Dawn",
"Crystalline",
"Jeweled",
"Sylvan",
"Twisted",
"Ebon",
"Thorn",
"Cerulean",
"Halcyon",
"Infernal",
"Storm",
"Eldritch",
"Sapphire",
"Crimson",
"Tranquil",
"Paved"
];
const descriptors = [
"Great",
"Shrouded",
"Sacred",
"Fabled",
"Frosty",
"Winding",
"Echoing",
"Serpentine",
"Breezy",
"Misty",
"Rustic",
"Silent",
"Cobbled",
"Cracked",
"Shaky",
"Obscure"
];
const suffixes = {
roads: {road: 7, route: 3, way: 2, highway: 1},
trails: {trail: 4, path: 1, track: 1, pass: 1},
searoutes: {"sea route": 5, lane: 2, passage: 1, seaway: 1}
};
function generateName({group, points}) {
if (points.length < 4) return "Unnamed route segment";
const model = rw(models[group]);
const suffix = rw(suffixes[group]);
const burgName = getBurgName();
if (model === "burg_suffix" && burgName) return `${burgName} ${suffix}`;
if (model === "prefix_suffix") return `${ra(prefixes)} ${suffix}`;
if (model === "the_descriptor_prefix_suffix") return `The ${ra(descriptors)} ${ra(prefixes)} ${suffix}`;
if (model === "the_descriptor_burg_suffix" && burgName) return `The ${ra(descriptors)} ${burgName} ${suffix}`;
return "Unnamed route";
function getBurgName() {
const priority = [points.at(-1), points.at(0), points.slice(1, -1).reverse()];
for (const [_x, _y, cellId] of priority) {
const burgId = pack.cells.burg[cellId];
if (burgId) return getAdjective(pack.burgs[burgId].name);
}
return null;
}
}
const ROUTE_CURVES = {
roads: d3.curveCatmullRom.alpha(0.1),
trails: d3.curveCatmullRom.alpha(0.1),
searoutes: d3.curveCatmullRom.alpha(0.5),
default: d3.curveCatmullRom.alpha(0.1)
};
function getPath({group, points}) {
const lineGen = d3.line();
lineGen.curve(ROUTE_CURVES[group] || ROUTE_CURVES.default);
const path = round(lineGen(points.map(p => [p[0], p[1]])), 1);
return path; return path;
} }
// find water paths function getLength(routeId) {
function findOceanPath(start, exit = null, toRoute = null) { const path = routes.select("#route" + routeId).node();
const cells = pack.cells, return path.getTotalLength();
temp = grid.cells.temp; }
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const cost = [],
from = [];
queue.queue({e: start, p: 0});
while (queue.length) { function remove(route) {
const next = queue.dequeue(), const routes = pack.cells.routes;
n = next.e,
p = next.p;
if (toRoute && n !== start && cells.road[n]) return [from, n, true];
for (const c of cells.c[n]) { for (const point of route.points) {
if (c === exit) { const from = point[2];
from[c] = n;
return [from, exit, true];
}
if (cells.h[c] >= 20) continue; // ignore land cells
if (temp[cells.g[c]] <= -5) continue; // ignore cells with term <= -5
const dist2 = (cells.p[c][1] - cells.p[n][1]) ** 2 + (cells.p[c][0] - cells.p[n][0]) ** 2;
const totalCost = p + (cells.road[c] ? 1 + dist2 / 2 : dist2 + (cells.t[c] ? 1 : 100));
if (from[c] || totalCost >= cost[c]) continue; for (const [to, routeId] of Object.entries(routes[from])) {
(from[c] = n), (cost[c] = totalCost); if (routeId === route.i) {
queue.queue({e: c, p: totalCost}); delete routes[from][to];
delete routes[to][from];
} }
} }
return [from, exit, false];
} }
pack.routes = pack.routes.filter(r => r.i !== route.i);
viewbox
.select("#routes")
.select("#route" + route.i)
.remove();
}
return {
generate,
connect,
isConnected,
areConnected,
getRoute,
hasRoad,
isCrossroad,
generateName,
getPath,
getLength,
remove
};
})(); })();

View file

@ -145,8 +145,6 @@ window.Submap = (function () {
cells.state = new Uint16Array(pn); cells.state = new Uint16Array(pn);
cells.burg = new Uint16Array(pn); cells.burg = new Uint16Array(pn);
cells.religion = new Uint16Array(pn); cells.religion = new Uint16Array(pn);
cells.road = new Uint16Array(pn);
cells.crossroad = new Uint16Array(pn);
cells.province = new Uint16Array(pn); cells.province = new Uint16Array(pn);
stage("Resampling culture, state and religion map."); stage("Resampling culture, state and religion map.");
@ -272,8 +270,8 @@ window.Submap = (function () {
BurgsAndStates.drawBurgs(); BurgsAndStates.drawBurgs();
stage("Regenerating road network."); stage("Regenerating routes network.");
Routes.regenerate(); regenerateRoutes();
drawStates(); drawStates();
drawBorders(); drawBorders();
@ -397,7 +395,7 @@ window.Submap = (function () {
return; return;
} }
DEBUG && console.info(`Moving ${b.name} from ${cityCell} to ${newCell} near ${neighbor}.`); DEBUG && console.info(`Moving ${b.name} from ${cityCell} to ${newCell} near ${neighbor}.`);
[b.x, b.y] = b.port ? getMiddlePoint(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;
if (b.port && !isWater(pack, neighbor)) console.error("betrayal! negihbor must be water!", b); if (b.port && !isWater(pack, neighbor)) console.error("betrayal! negihbor must be water!", b);
@ -409,6 +407,23 @@ window.Submap = (function () {
}); });
} }
function getCloseToEdgePoint(cell1, cell2) {
const {cells, vertices} = pack;
const [x0, y0] = cells.p[cell1];
const commonVertices = cells.v[cell1].filter(vertex => vertices.c[vertex].some(cell => cell === cell2));
const [x1, y1] = vertices.p[commonVertices[0]];
const [x2, y2] = vertices.p[commonVertices[1]];
const xEdge = (x1 + x2) / 2;
const yEdge = (y1 + y2) / 2;
const x = rn(x0 + 0.95 * (xEdge - x0), 2);
const y = rn(y0 + 0.95 * (yEdge - y0), 2);
return [x, y];
}
// export // export
return {resample, findNearest}; return {resample, findNearest};
})(); })();

View file

@ -37,12 +37,22 @@ class Battle {
// add listeners // add listeners
document.getElementById("battleType").addEventListener("click", ev => this.toggleChange(ev)); document.getElementById("battleType").addEventListener("click", ev => this.toggleChange(ev));
document.getElementById("battleType").nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changeType(ev)); document
document.getElementById("battleNameShow").addEventListener("click", () => Battle.prototype.context.showNameSection()); .getElementById("battleType")
document.getElementById("battleNamePlace").addEventListener("change", ev => (Battle.prototype.context.place = ev.target.value)); .nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changeType(ev));
document
.getElementById("battleNameShow")
.addEventListener("click", () => Battle.prototype.context.showNameSection());
document
.getElementById("battleNamePlace")
.addEventListener("change", ev => (Battle.prototype.context.place = ev.target.value));
document.getElementById("battleNameFull").addEventListener("change", ev => Battle.prototype.context.changeName(ev)); document.getElementById("battleNameFull").addEventListener("change", ev => Battle.prototype.context.changeName(ev));
document.getElementById("battleNameCulture").addEventListener("click", () => Battle.prototype.context.generateName("culture")); document
document.getElementById("battleNameRandom").addEventListener("click", () => Battle.prototype.context.generateName("random")); .getElementById("battleNameCulture")
.addEventListener("click", () => Battle.prototype.context.generateName("culture"));
document
.getElementById("battleNameRandom")
.addEventListener("click", () => Battle.prototype.context.generateName("random"));
document.getElementById("battleNameHide").addEventListener("click", this.hideNameSection); document.getElementById("battleNameHide").addEventListener("click", this.hideNameSection);
document.getElementById("battleAddRegiment").addEventListener("click", this.addSide); document.getElementById("battleAddRegiment").addEventListener("click", this.addSide);
document.getElementById("battleRoll").addEventListener("click", () => Battle.prototype.context.randomize()); document.getElementById("battleRoll").addEventListener("click", () => Battle.prototype.context.randomize());
@ -52,11 +62,19 @@ class Battle {
document.getElementById("battleWiki").addEventListener("click", () => wiki("Battle-Simulator")); document.getElementById("battleWiki").addEventListener("click", () => wiki("Battle-Simulator"));
document.getElementById("battlePhase_attackers").addEventListener("click", ev => this.toggleChange(ev)); document.getElementById("battlePhase_attackers").addEventListener("click", ev => this.toggleChange(ev));
document.getElementById("battlePhase_attackers").nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changePhase(ev, "attackers")); document
.getElementById("battlePhase_attackers")
.nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changePhase(ev, "attackers"));
document.getElementById("battlePhase_defenders").addEventListener("click", ev => this.toggleChange(ev)); document.getElementById("battlePhase_defenders").addEventListener("click", ev => this.toggleChange(ev));
document.getElementById("battlePhase_defenders").nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changePhase(ev, "defenders")); document
document.getElementById("battleDie_attackers").addEventListener("click", () => Battle.prototype.context.rollDie("attackers")); .getElementById("battlePhase_defenders")
document.getElementById("battleDie_defenders").addEventListener("click", () => Battle.prototype.context.rollDie("defenders")); .nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changePhase(ev, "defenders"));
document
.getElementById("battleDie_attackers")
.addEventListener("click", () => Battle.prototype.context.rollDie("attackers"));
document
.getElementById("battleDie_defenders")
.addEventListener("click", () => Battle.prototype.context.rollDie("defenders"));
} }
defineType() { defineType() {
@ -82,8 +100,12 @@ class Battle {
document.getElementById("battleType").className = "icon-button-" + this.type; document.getElementById("battleType").className = "icon-button-" + this.type;
const sideSpecific = document.getElementById("battlePhases_" + this.type + "_attackers"); const sideSpecific = document.getElementById("battlePhases_" + this.type + "_attackers");
const attackers = sideSpecific ? sideSpecific.content : document.getElementById("battlePhases_" + this.type).content; const attackers = sideSpecific
const defenders = sideSpecific ? document.getElementById("battlePhases_" + this.type + "_defenders").content : attackers; ? sideSpecific.content
: document.getElementById("battlePhases_" + this.type).content;
const defenders = sideSpecific
? document.getElementById("battlePhases_" + this.type + "_defenders").content
: attackers;
document.getElementById("battlePhase_attackers").nextElementSibling.innerHTML = ""; document.getElementById("battlePhase_attackers").nextElementSibling.innerHTML = "";
document.getElementById("battlePhase_defenders").nextElementSibling.innerHTML = ""; document.getElementById("battlePhase_defenders").nextElementSibling.innerHTML = "";
@ -139,26 +161,37 @@ class Battle {
regiment.survivors = Object.assign({}, regiment.u); regiment.survivors = Object.assign({}, regiment.u);
const state = pack.states[regiment.state]; const state = pack.states[regiment.state];
const distance = (Math.hypot(this.y - regiment.by, this.x - regiment.bx) * distanceScaleInput.value) | 0; // distance between regiment and its base const distance = (Math.hypot(this.y - regiment.by, this.x - regiment.bx) * distanceScale) | 0; // distance between regiment and its base
const color = state.color[0] === "#" ? state.color : "#999"; const color = state.color[0] === "#" ? state.color : "#999";
const icon = `<svg width="1.4em" height="1.4em" style="margin-bottom: -.6em; stroke: #333"> const icon = `<svg width="1.4em" height="1.4em" style="margin-bottom: -.6em; stroke: #333">
<rect x="0" y="0" width="100%" height="100%" fill="${color}"></rect> <rect x="0" y="0" width="100%" height="100%" fill="${color}"></rect>
<text x="0" y="1.04em" style="">${regiment.icon}</text></svg>`; <text x="0" y="1.04em" style="">${regiment.icon}</text></svg>`;
const body = `<tbody id="battle${state.i}-${regiment.i}">`; const body = `<tbody id="battle${state.i}-${regiment.i}">`;
let initial = `<tr class="battleInitial"><td>${icon}</td><td class="regiment" data-tip="${regiment.name}">${regiment.name.slice(0, 24)}</td>`; let initial = `<tr class="battleInitial"><td>${icon}</td><td class="regiment" data-tip="${
let casualties = `<tr class="battleCasualties"><td></td><td data-tip="${state.fullName}">${state.fullName.slice(0, 26)}</td>`; regiment.name
}">${regiment.name.slice(0, 24)}</td>`;
let casualties = `<tr class="battleCasualties"><td></td><td data-tip="${state.fullName}">${state.fullName.slice(
0,
26
)}</td>`;
let survivors = `<tr class="battleSurvivors"><td></td><td data-tip="Supply line length, affects morale">Distance to base: ${distance} ${distanceUnitInput.value}</td>`; let survivors = `<tr class="battleSurvivors"><td></td><td data-tip="Supply line length, affects morale">Distance to base: ${distance} ${distanceUnitInput.value}</td>`;
for (const u of options.military) { for (const u of options.military) {
initial += `<td data-tip="Initial forces" style="width: 2.5em; text-align: center">${regiment.u[u.name] || 0}</td>`; initial += `<td data-tip="Initial forces" style="width: 2.5em; text-align: center">${
regiment.u[u.name] || 0
}</td>`;
casualties += `<td data-tip="Casualties" style="width: 2.5em; text-align: center; color: red">0</td>`; casualties += `<td data-tip="Casualties" style="width: 2.5em; text-align: center; color: red">0</td>`;
survivors += `<td data-tip="Survivors" style="width: 2.5em; text-align: center; color: green">${regiment.u[u.name] || 0}</td>`; survivors += `<td data-tip="Survivors" style="width: 2.5em; text-align: center; color: green">${
regiment.u[u.name] || 0
}</td>`;
} }
initial += `<td data-tip="Initial forces" style="width: 2.5em; text-align: center">${regiment.a || 0}</td></tr>`; initial += `<td data-tip="Initial forces" style="width: 2.5em; text-align: center">${regiment.a || 0}</td></tr>`;
casualties += `<td data-tip="Casualties" style="width: 2.5em; text-align: center; color: red">0</td></tr>`; casualties += `<td data-tip="Casualties" style="width: 2.5em; text-align: center; color: red">0</td></tr>`;
survivors += `<td data-tip="Survivors" style="width: 2.5em; text-align: center; color: green">${regiment.a || 0}</td></tr>`; survivors += `<td data-tip="Survivors" style="width: 2.5em; text-align: center; color: green">${
regiment.a || 0
}</td></tr>`;
const div = side === "attackers" ? battleAttackers : battleDefenders; const div = side === "attackers" ? battleAttackers : battleDefenders;
div.innerHTML += body + initial + casualties + survivors + "</tbody>"; div.innerHTML += body + initial + casualties + survivors + "</tbody>";
@ -173,17 +206,23 @@ class Battle {
.filter(s => s.military && !s.removed) .filter(s => s.military && !s.removed)
.map(s => s.military) .map(s => s.military)
.flat(); .flat();
const distance = reg => rn(Math.hypot(context.y - reg.y, context.x - reg.x) * distanceScaleInput.value) + " " + distanceUnitInput.value; const distance = reg =>
const isAdded = reg => context.defenders.regiments.some(r => r === reg) || context.attackers.regiments.some(r => r === reg); rn(Math.hypot(context.y - reg.y, context.x - reg.x) * distanceScale) + " " + distanceUnitInput.value;
const isAdded = reg =>
context.defenders.regiments.some(r => r === reg) || context.attackers.regiments.some(r => r === reg);
body.innerHTML = regiments body.innerHTML = regiments
.map(r => { .map(r => {
const s = pack.states[r.state], const s = pack.states[r.state],
added = isAdded(r), added = isAdded(r),
dist = added ? "0 " + distanceUnitInput.value : distance(r); dist = added ? "0 " + distanceUnitInput.value : distance(r);
return `<div ${added ? "class='inactive'" : ""} data-s=${s.i} data-i=${r.i} data-state=${s.name} data-regiment=${r.name} return `<div ${added ? "class='inactive'" : ""} data-s=${s.i} data-i=${r.i} data-state=${
s.name
} data-regiment=${r.name}
data-total=${r.a} data-distance=${dist} data-tip="Click to select regiment"> data-total=${r.a} data-distance=${dist} data-tip="Click to select regiment">
<svg width=".9em" height=".9em" style="margin-bottom:-1px; stroke: #333"><rect x="0" y="0" width="100%" height="100%" fill="${s.color}" ></svg> <svg width=".9em" height=".9em" style="margin-bottom:-1px; stroke: #333"><rect x="0" y="0" width="100%" height="100%" fill="${
s.color
}" ></svg>
<div style="width:6em">${s.name.slice(0, 11)}</div> <div style="width:6em">${s.name.slice(0, 11)}</div>
<div style="width:1.2em">${r.icon}</div> <div style="width:1.2em">${r.icon}</div>
<div style="width:13em">${r.name.slice(0, 24)}</div> <div style="width:13em">${r.name.slice(0, 24)}</div>
@ -267,7 +306,10 @@ class Battle {
} }
generateName(type) { generateName(type) {
const place = type === "culture" ? Names.getCulture(pack.cells.culture[this.cell], null, null, "") : Names.getBase(rand(nameBases.length - 1)); const place =
type === "culture"
? Names.getCulture(pack.cells.culture[this.cell], null, null, "")
: Names.getBase(rand(nameBases.length - 1));
document.getElementById("battleNamePlace").value = this.place = place; document.getElementById("battleNamePlace").value = this.place = place;
document.getElementById("battleNameFull").value = this.name = this.defineName(); document.getElementById("battleNameFull").value = this.name = this.defineName();
$("#battleScreen").dialog({title: this.name}); $("#battleScreen").dialog({title: this.name});
@ -286,35 +328,161 @@ class Battle {
calculateStrength(side) { calculateStrength(side) {
const scheme = { const scheme = {
// field battle phases // field battle phases
skirmish: {melee: 0.2, ranged: 2.4, mounted: 0.1, machinery: 3, naval: 1, armored: 0.2, aviation: 1.8, magical: 1.8}, // ranged excel skirmish: {
melee: 0.2,
ranged: 2.4,
mounted: 0.1,
machinery: 3,
naval: 1,
armored: 0.2,
aviation: 1.8,
magical: 1.8
}, // ranged excel
melee: {melee: 2, ranged: 1.2, mounted: 1.5, machinery: 0.5, naval: 0.2, armored: 2, aviation: 0.8, magical: 0.8}, // melee excel melee: {melee: 2, ranged: 1.2, mounted: 1.5, machinery: 0.5, naval: 0.2, armored: 2, aviation: 0.8, magical: 0.8}, // melee excel
pursue: {melee: 1, ranged: 1, mounted: 4, machinery: 0.05, naval: 1, armored: 1, aviation: 1.5, magical: 0.6}, // mounted excel pursue: {melee: 1, ranged: 1, mounted: 4, machinery: 0.05, naval: 1, armored: 1, aviation: 1.5, magical: 0.6}, // mounted excel
retreat: {melee: 0.1, ranged: 0.01, mounted: 0.5, machinery: 0.01, naval: 0.2, armored: 0.1, aviation: 0.8, magical: 0.05}, // reduced retreat: {
melee: 0.1,
ranged: 0.01,
mounted: 0.5,
machinery: 0.01,
naval: 0.2,
armored: 0.1,
aviation: 0.8,
magical: 0.05
}, // reduced
// naval battle phases // naval battle phases
shelling: {melee: 0, ranged: 0.2, mounted: 0, machinery: 2, naval: 2, armored: 0, aviation: 0.1, magical: 0.5}, // naval and machinery excel shelling: {melee: 0, ranged: 0.2, mounted: 0, machinery: 2, naval: 2, armored: 0, aviation: 0.1, magical: 0.5}, // naval and machinery excel
boarding: {melee: 1, ranged: 0.5, mounted: 0.5, machinery: 0, naval: 0.5, armored: 0.4, aviation: 0, magical: 0.2}, // melee excel boarding: {
melee: 1,
ranged: 0.5,
mounted: 0.5,
machinery: 0,
naval: 0.5,
armored: 0.4,
aviation: 0,
magical: 0.2
}, // melee excel
chase: {melee: 0, ranged: 0.15, mounted: 0, machinery: 1, naval: 1, armored: 0, aviation: 0.15, magical: 0.5}, // reduced chase: {melee: 0, ranged: 0.15, mounted: 0, machinery: 1, naval: 1, armored: 0, aviation: 0.15, magical: 0.5}, // reduced
withdrawal: {melee: 0, ranged: 0.02, mounted: 0, machinery: 0.5, naval: 0.1, armored: 0, aviation: 0.1, magical: 0.3}, // reduced withdrawal: {
melee: 0,
ranged: 0.02,
mounted: 0,
machinery: 0.5,
naval: 0.1,
armored: 0,
aviation: 0.1,
magical: 0.3
}, // reduced
// siege phases // siege phases
blockade: {melee: 0.25, ranged: 0.25, mounted: 0.2, machinery: 0.5, naval: 0.2, armored: 0.1, aviation: 0.25, magical: 0.25}, // no active actions blockade: {
sheltering: {melee: 0.3, ranged: 0.5, mounted: 0.2, machinery: 0.5, naval: 0.2, armored: 0.1, aviation: 0.25, magical: 0.25}, // no active actions melee: 0.25,
ranged: 0.25,
mounted: 0.2,
machinery: 0.5,
naval: 0.2,
armored: 0.1,
aviation: 0.25,
magical: 0.25
}, // no active actions
sheltering: {
melee: 0.3,
ranged: 0.5,
mounted: 0.2,
machinery: 0.5,
naval: 0.2,
armored: 0.1,
aviation: 0.25,
magical: 0.25
}, // no active actions
sortie: {melee: 2, ranged: 0.5, mounted: 1.2, machinery: 0.2, naval: 0.1, armored: 0.5, aviation: 1, magical: 1}, // melee excel sortie: {melee: 2, ranged: 0.5, mounted: 1.2, machinery: 0.2, naval: 0.1, armored: 0.5, aviation: 1, magical: 1}, // melee excel
bombardment: {melee: 0.2, ranged: 0.5, mounted: 0.2, machinery: 3, naval: 1, armored: 0.5, aviation: 1, magical: 1}, // machinery excel bombardment: {
storming: {melee: 1, ranged: 0.6, mounted: 0.5, machinery: 1, naval: 0.1, armored: 0.1, aviation: 0.5, magical: 0.5}, // melee excel melee: 0.2,
ranged: 0.5,
mounted: 0.2,
machinery: 3,
naval: 1,
armored: 0.5,
aviation: 1,
magical: 1
}, // machinery excel
storming: {
melee: 1,
ranged: 0.6,
mounted: 0.5,
machinery: 1,
naval: 0.1,
armored: 0.1,
aviation: 0.5,
magical: 0.5
}, // melee excel
defense: {melee: 2, ranged: 3, mounted: 1, machinery: 1, naval: 0.1, armored: 1, aviation: 0.5, magical: 1}, // ranged excel defense: {melee: 2, ranged: 3, mounted: 1, machinery: 1, naval: 0.1, armored: 1, aviation: 0.5, magical: 1}, // ranged excel
looting: {melee: 1.6, ranged: 1.6, mounted: 0.5, machinery: 0.2, naval: 0.02, armored: 0.2, aviation: 0.1, magical: 0.3}, // melee excel looting: {
surrendering: {melee: 0.1, ranged: 0.1, mounted: 0.05, machinery: 0.01, naval: 0.01, armored: 0.02, aviation: 0.01, magical: 0.03}, // reduced melee: 1.6,
ranged: 1.6,
mounted: 0.5,
machinery: 0.2,
naval: 0.02,
armored: 0.2,
aviation: 0.1,
magical: 0.3
}, // melee excel
surrendering: {
melee: 0.1,
ranged: 0.1,
mounted: 0.05,
machinery: 0.01,
naval: 0.01,
armored: 0.02,
aviation: 0.01,
magical: 0.03
}, // reduced
// ambush phases // ambush phases
surprise: {melee: 2, ranged: 2.4, mounted: 1, machinery: 1, naval: 1, armored: 1, aviation: 0.8, magical: 1.2}, // increased surprise: {melee: 2, ranged: 2.4, mounted: 1, machinery: 1, naval: 1, armored: 1, aviation: 0.8, magical: 1.2}, // increased
shock: {melee: 0.5, ranged: 0.5, mounted: 0.5, machinery: 0.4, naval: 0.3, armored: 0.1, aviation: 0.4, magical: 0.5}, // reduced shock: {
melee: 0.5,
ranged: 0.5,
mounted: 0.5,
machinery: 0.4,
naval: 0.3,
armored: 0.1,
aviation: 0.4,
magical: 0.5
}, // reduced
// langing phases // langing phases
landing: {melee: 0.8, ranged: 0.6, mounted: 0.6, machinery: 0.5, naval: 0.5, armored: 0.5, aviation: 0.5, magical: 0.6}, // reduced landing: {
flee: {melee: 0.1, ranged: 0.01, mounted: 0.5, machinery: 0.01, naval: 0.5, armored: 0.1, aviation: 0.2, magical: 0.05}, // reduced melee: 0.8,
waiting: {melee: 0.05, ranged: 0.5, mounted: 0.05, machinery: 0.5, naval: 2, armored: 0.05, aviation: 0.5, magical: 0.5}, // reduced ranged: 0.6,
mounted: 0.6,
machinery: 0.5,
naval: 0.5,
armored: 0.5,
aviation: 0.5,
magical: 0.6
}, // reduced
flee: {
melee: 0.1,
ranged: 0.01,
mounted: 0.5,
machinery: 0.01,
naval: 0.5,
armored: 0.1,
aviation: 0.2,
magical: 0.05
}, // reduced
waiting: {
melee: 0.05,
ranged: 0.5,
mounted: 0.05,
machinery: 0.5,
naval: 2,
armored: 0.05,
aviation: 0.5,
magical: 0.5
}, // reduced
// air battle phases // air battle phases
maneuvering: {melee: 0, ranged: 0.1, mounted: 0, machinery: 0.2, naval: 0, armored: 0, aviation: 1, magical: 0.2}, // aviation maneuvering: {melee: 0, ranged: 0.1, mounted: 0, machinery: 0.2, naval: 0, armored: 0, aviation: 1, magical: 0.2}, // aviation
@ -324,7 +492,8 @@ class Battle {
const forces = this.getJoinedForces(this[side].regiments); const forces = this.getJoinedForces(this[side].regiments);
const phase = this[side].phase; const phase = this[side].phase;
const adjuster = Math.max(populationRate / 10, 10); // population adjuster, by default 100 const adjuster = Math.max(populationRate / 10, 10); // population adjuster, by default 100
this[side].power = d3.sum(options.military.map(u => (forces[u.name] || 0) * u.power * scheme[phase][u.type])) / adjuster; this[side].power =
d3.sum(options.military.map(u => (forces[u.name] || 0) * u.power * scheme[phase][u.type])) / adjuster;
const UIvalue = this[side].power ? Math.max(this[side].power | 0, 1) : 0; const UIvalue = this[side].power ? Math.max(this[side].power | 0, 1) : 0;
document.getElementById("battlePower_" + side).innerHTML = UIvalue; document.getElementById("battlePower_" + side).innerHTML = UIvalue;
} }
@ -723,11 +892,13 @@ class Battle {
const status = battleStatus[+P(0.7)]; const status = battleStatus[+P(0.7)];
const result = `The ${this.getTypeName(this.type)} ended in ${status}`; const result = `The ${this.getTypeName(this.type)} ended in ${status}`;
const legend = `${this.name} took place in ${options.year} ${options.eraShort}. It was fought between ${getSide(this.attackers.regiments, 1)} and ${getSide( const legend = `${this.name} took place in ${options.year} ${options.eraShort}. It was fought between ${getSide(
this.defenders.regiments, this.attackers.regiments,
0 1
)}. ${result}. )} and ${getSide(this.defenders.regiments, 0)}. ${result}.
\r\nAttackers losses: ${getLosses(this.attackers.casualties)}%, defenders losses: ${getLosses(this.defenders.casualties)}%`; \r\nAttackers losses: ${getLosses(this.attackers.casualties)}%, defenders losses: ${getLosses(
this.defenders.casualties
)}%`;
notes.push({id: `marker${i}`, name: this.name, legend}); notes.push({id: `marker${i}`, name: this.name, legend});
tip(`${this.name} is over. ${result}`, true, "success", 4000); tip(`${this.name} is over. ${result}`, true, "success", 4000);

View file

@ -390,7 +390,7 @@ function editBiomes() {
const p = d3.mouse(this); const p = d3.mouse(this);
moveCircle(p[0], p[1], r); moveCircle(p[0], p[1], r);
const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)]; const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1])];
const selection = found.filter(isLand); const selection = found.filter(isLand);
if (selection) changeBiomeForSelection(selection); if (selection) changeBiomeForSelection(selection);
}); });

View file

@ -36,7 +36,6 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) {
}); });
byId("burgsLockAll").addEventListener("click", toggleLockAll); byId("burgsLockAll").addEventListener("click", toggleLockAll);
byId("burgsRemoveAll").addEventListener("click", triggerAllBurgsRemove); byId("burgsRemoveAll").addEventListener("click", triggerAllBurgsRemove);
byId("burgsInvertLock").addEventListener("click", invertLock);
function refreshBurgsEditor() { function refreshBurgsEditor() {
updateFilter(); updateFilter();
@ -279,7 +278,8 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) {
function addBurgOnClick() { function addBurgOnClick() {
const point = d3.mouse(this); const point = d3.mouse(this);
const cell = findCell(point[0], point[1]); const cell = findCell(...point);
if (pack.cells.h[cell] < 20) if (pack.cells.h[cell] < 20)
return tip("You cannot place state into the water. Please click on a land cell", false, "error"); return tip("You cannot place state into the water. Please click on a land cell", false, "error");
if (pack.cells.burg[cell]) if (pack.cells.burg[cell])
@ -603,11 +603,6 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) {
burgsOverviewAddLines(); burgsOverviewAddLines();
} }
function invertLock() {
pack.burgs = pack.burgs.map(burg => ({...burg, lock: !burg.lock}));
burgsOverviewAddLines();
}
function toggleLockAll() { function toggleLockAll() {
const activeBurgs = pack.burgs.filter(b => b.i && !b.removed); const activeBurgs = pack.burgs.filter(b => b.i && !b.removed);
const allLocked = activeBurgs.every(burg => burg.lock); const allLocked = activeBurgs.every(burg => burg.lock);

View file

@ -22,7 +22,7 @@ function clicked() {
if (grand.id === "emblems") editEmblem(); if (grand.id === "emblems") editEmblem();
else if (parent.id === "rivers") editRiver(el.id); else if (parent.id === "rivers") editRiver(el.id);
else if (grand.id === "routes") editRoute(); else if (grand.id === "routes") editRoute(el.id);
else if (el.tagName === "tspan" && grand.parentNode.parentNode.id === "labels") editLabel(); else if (el.tagName === "tspan" && grand.parentNode.parentNode.id === "labels") editLabel();
else if (grand.id === "burgLabels") editBurg(); else if (grand.id === "burgLabels") editBurg();
else if (grand.id === "burgIcons") editBurg(); else if (grand.id === "burgIcons") editBurg();
@ -132,27 +132,43 @@ function applySorting(headers) {
} }
function addBurg(point) { function addBurg(point) {
const cells = pack.cells; const {cells, states} = pack;
const x = rn(point[0], 2), const x = rn(point[0], 2);
y = rn(point[1], 2); const y = rn(point[1], 2);
const cell = findCell(x, point[1]);
const i = pack.burgs.length;
const culture = cells.culture[cell];
const name = Names.getCulture(culture);
const state = cells.state[cell];
const feature = cells.f[cell];
const temple = pack.states[state].form === "Theocracy"; const cellId = findCell(x, y);
const population = Math.max((cells.s[cell] + cells.road[cell]) / 3 + i / 1000 + (cell % 100) / 1000, 0.1); const i = pack.burgs.length;
const type = BurgsAndStates.getType(cell, false); const culture = cells.culture[cellId];
const name = Names.getCulture(culture);
const state = cells.state[cellId];
const feature = cells.f[cellId];
const population = Math.max(cells.s[cellId] / 3 + i / 1000 + (cellId % 100) / 1000, 0.1);
const type = BurgsAndStates.getType(cellId, false);
// generate emblem // generate emblem
const coa = COA.generate(pack.states[state].coa, 0.25, null, type); const coa = COA.generate(states[state].coa, 0.25, null, type);
coa.shield = COA.getShield(culture, state); coa.shield = COA.getShield(culture, state);
COArenderer.add("burg", i, coa, x, y); COArenderer.add("burg", i, coa, x, y);
pack.burgs.push({name, cell, x, y, state, i, culture, feature, capital: 0, port: 0, temple, population, coa, type}); const burg = {
cells.burg[cell] = i; name,
cell: cellId,
x,
y,
state,
i,
culture,
feature,
capital: 0,
port: 0,
temple: 0,
population,
coa,
type
};
pack.burgs.push(burg);
cells.burg[cellId] = i;
const townSize = burgIcons.select("#towns").attr("size") || 0.5; const townSize = burgIcons.select("#towns").attr("size") || 0.5;
burgIcons burgIcons
@ -173,7 +189,17 @@ function addBurg(point) {
.attr("dy", `${townSize * -1.5}px`) .attr("dy", `${townSize * -1.5}px`)
.text(name); .text(name);
BurgsAndStates.defineBurgFeatures(pack.burgs[i]); BurgsAndStates.defineBurgFeatures(burg);
const newRoute = Routes.connect(cellId);
if (newRoute && layerIsOn("toggleRoutes")) {
routes
.select("#" + newRoute.group)
.append("path")
.attr("d", Routes.getPath(newRoute))
.attr("id", "route" + newRoute.i);
}
return i; return i;
} }
@ -327,8 +353,7 @@ function createMfcgLink(burg) {
const citadel = +burg.citadel; const citadel = +burg.citadel;
const urban_castle = +(citadel && each(2)(i)); const urban_castle = +(citadel && each(2)(i));
const hub = +cells.road[cell] > 50; const hub = Routes.isCrossroad(cell);
const walls = +burg.walls; const walls = +burg.walls;
const plaza = +burg.plaza; const plaza = +burg.plaza;
const temple = +burg.temple; const temple = +burg.temple;
@ -372,10 +397,12 @@ function createVillageGeneratorLink(burg) {
else if (cells.r[cell]) tags.push("river"); else if (cells.r[cell]) tags.push("river");
else if (pop < 200 && each(4)(cell)) tags.push("pond"); else if (pop < 200 && each(4)(cell)) tags.push("pond");
const roadsAround = cells.c[cell].filter(c => cells.h[c] >= 20 && cells.road[c]).length; const connections = pack.cells.routes[cell] || {};
if (roadsAround > 1) tags.push("highway"); const roads = Object.values(connections).filter(routeId => {
else if (roadsAround === 1) tags.push("dead end"); const route = pack.routes[routeId];
else tags.push("isolated"); return route.group === "roads" || route.group === "trails";
}).length;
tags.push(roads > 1 ? "highway" : roads === 1 ? "dead end" : "isolated");
const biome = cells.biome[cell]; const biome = cells.biome[cell];
const arableBiomes = cells.r[cell] ? [1, 2, 3, 4, 5, 6, 7, 8] : [5, 6, 7, 8]; const arableBiomes = cells.r[cell] ? [1, 2, 3, 4, 5, 6, 7, 8] : [5, 6, 7, 8];
@ -1174,7 +1201,6 @@ function getAreaUnit(squareMark = "²") {
} }
function getArea(rawArea) { function getArea(rawArea) {
const distanceScale = byId("distanceScaleInput")?.value;
return rawArea * distanceScale ** 2; return rawArea * distanceScale ** 2;
} }
@ -1225,7 +1251,7 @@ function refreshAllEditors() {
// dynamically loaded editors // dynamically loaded editors
async function editStates() { async function editStates() {
if (customization) return; if (customization) return;
const Editor = await import("../dynamic/editors/states-editor.js?v=1.97.06"); const Editor = await import("../dynamic/editors/states-editor.js?v=1.99.00");
Editor.open(); Editor.open();
} }

View file

@ -1,43 +1,14 @@
"use strict"; "use strict";
function showEPForRoute(node) {
const points = [];
debug
.select("#controlPoints")
.selectAll("circle")
.each(function () {
const i = findCell(this.getAttribute("cx"), this.getAttribute("cy"));
points.push(i);
});
const routeLen = node.getTotalLength() * distanceScaleInput.value;
showElevationProfile(points, routeLen, false);
}
function showEPForRiver(node) {
const points = [];
debug
.select("#controlPoints")
.selectAll("circle")
.each(function () {
const i = findCell(this.getAttribute("cx"), this.getAttribute("cy"));
points.push(i);
});
const riverLen = (node.getTotalLength() / 2) * distanceScaleInput.value;
showElevationProfile(points, riverLen, true);
}
function showElevationProfile(data, routeLen, isRiver) {
// data is an array of cell indexes, routeLen is the distance (in actual metres/feet), isRiver should be true for rivers, false otherwise // data is an array of cell indexes, routeLen is the distance (in actual metres/feet), isRiver should be true for rivers, false otherwise
document.getElementById("epScaleRange").addEventListener("change", draw); function showElevationProfile(data, routeLen, isRiver) {
document.getElementById("epCurve").addEventListener("change", draw); byId("epScaleRange").on("change", draw);
document.getElementById("epSave").addEventListener("click", downloadCSV); byId("epCurve").on("change", draw);
byId("epSave").on("click", downloadCSV);
$("#elevationProfile").dialog({ $("#elevationProfile").dialog({
title: "Elevation profile", title: "Elevation profile",
resizable: false, resizable: false,
width: window.width,
close: closeElevationProfile, close: closeElevationProfile,
position: {my: "left top", at: "left+20 bottom-500", of: window, collision: "fit"} position: {my: "left top", at: "left+20 bottom-500", of: window, collision: "fit"}
}); });
@ -45,18 +16,20 @@ function showElevationProfile(data, routeLen, isRiver) {
// prevent river graphs from showing rivers as flowing uphill - remember the general slope // prevent river graphs from showing rivers as flowing uphill - remember the general slope
let slope = 0; let slope = 0;
if (isRiver) { if (isRiver) {
if (pack.cells.h[data[0]] < pack.cells.h[data[data.length - 1]]) { const firstCellHeight = pack.cells.h[data.at(0)];
const lastCellHeight = pack.cells.h[data.at(-1)];
if (firstCellHeight < lastCellHeight) {
slope = 1; // up-hill slope = 1; // up-hill
} else if (pack.cells.h[data[0]] > pack.cells.h[data[data.length - 1]]) { } else if (firstCellHeight > lastCellHeight) {
slope = -1; // down-hill slope = -1; // down-hill
} }
} }
const chartWidth = window.innerWidth - 180, const chartWidth = window.innerWidth - 200;
chartHeight = 300; // height of our land/sea profile, excluding the biomes data below const chartHeight = 300;
const xOffset = 80, const xOffset = 80;
yOffset = 80; // this is our drawing starting point from top-left (y = 0) of SVG const yOffset = 80;
const biomesHeight = 40; const biomesHeight = 10;
let lastBurgIndex = 0; let lastBurgIndex = 0;
let lastBurgCell = 0; let lastBurgCell = 0;
@ -174,7 +147,7 @@ function showElevationProfile(data, routeLen, isRiver) {
chartData.points.push([xscale(i) + xOffset, yscale(chartData.height[i]) + yOffset]); chartData.points.push([xscale(i) + xOffset, yscale(chartData.height[i]) + yOffset]);
} }
document.getElementById("elevationGraph").innerHTML = ""; byId("elevationGraph").innerHTML = "";
const chart = d3 const chart = d3
.select("#elevationGraph") .select("#elevationGraph")
@ -313,7 +286,7 @@ function showElevationProfile(data, routeLen, isRiver) {
.attr("x", x) .attr("x", x)
.attr("y", y) .attr("y", y)
.attr("width", xscale(1)) .attr("width", xscale(1))
.attr("height", 15) .attr("height", biomesHeight)
.attr("data-tip", dataTip); .attr("data-tip", dataTip);
} }
@ -388,7 +361,7 @@ function showElevationProfile(data, routeLen, isRiver) {
.attr("x", x1) .attr("x", x1)
.attr("y", y1) .attr("y", y1)
.attr("text-anchor", "middle"); .attr("text-anchor", "middle");
document.getElementById("ep" + b).innerHTML = pack.burgs[b].name; byId("ep" + b).innerHTML = pack.burgs[b].name;
// arrow from burg name to graph line // arrow from burg name to graph line
g.append("path") g.append("path")
@ -413,10 +386,10 @@ function showElevationProfile(data, routeLen, isRiver) {
} }
function closeElevationProfile() { function closeElevationProfile() {
document.getElementById("epScaleRange").removeEventListener("change", draw); byId("epScaleRange").removeEventListener("change", draw);
document.getElementById("epCurve").removeEventListener("change", draw); byId("epCurve").removeEventListener("change", draw);
document.getElementById("epSave").removeEventListener("click", downloadCSV); byId("epSave").removeEventListener("click", downloadCSV);
document.getElementById("elevationGraph").innerHTML = ""; byId("elevationGraph").innerHTML = "";
modules.elevation = false; modules.elevation = false;
} }
} }

View file

@ -67,9 +67,9 @@ function showDataTip(event) {
function showElementLockTip(event) { function showElementLockTip(event) {
const locked = event?.target?.classList?.contains("icon-lock"); const locked = event?.target?.classList?.contains("icon-lock");
if (locked) { if (locked) {
tip("Click to unlock the element and allow it to be changed by regeneration tools"); tip("Locked. Click to unlock the element and allow it to be changed by regeneration tools");
} else { } else {
tip("Click to lock the element and prevent changes to it by regeneration tools"); tip("Unlocked. Click to lock the element and prevent changes to it by regeneration tools");
} }
} }
@ -151,7 +151,12 @@ function showMapTooltip(point, e, i, g) {
return; return;
} }
if (group === "routes") return tip("Click to edit the Route"); if (group === "routes") {
const routeId = +e.target.id.slice(5);
const name = pack.routes[routeId]?.name;
if (name) return tip(`${name}. Click to edit the Route`);
return tip("Click to edit the Route");
}
if (group === "terrain") return tip("Click to edit the Relief Icon"); if (group === "terrain") return tip("Click to edit the Relief Icon");

View file

@ -246,6 +246,7 @@ function editHeightmap(options) {
Cultures.expand(); Cultures.expand();
BurgsAndStates.generate(); BurgsAndStates.generate();
Routes.generate();
Religions.generate(); Religions.generate();
BurgsAndStates.defineStateForms(); BurgsAndStates.defineStateForms();
BurgsAndStates.generateProvinces(); BurgsAndStates.generateProvinces();
@ -281,8 +282,7 @@ function editHeightmap(options) {
const l = grid.cells.i.length; const l = grid.cells.i.length;
const biome = new Uint8Array(l); const biome = new Uint8Array(l);
const pop = new Uint16Array(l); const pop = new Uint16Array(l);
const road = new Uint16Array(l); const routes = {};
const crossroad = new Uint16Array(l);
const s = new Uint16Array(l); const s = new Uint16Array(l);
const burg = new Uint16Array(l); const burg = new Uint16Array(l);
const state = new Uint16Array(l); const state = new Uint16Array(l);
@ -300,8 +300,7 @@ function editHeightmap(options) {
biome[g] = pack.cells.biome[i]; biome[g] = pack.cells.biome[i];
culture[g] = pack.cells.culture[i]; culture[g] = pack.cells.culture[i];
pop[g] = pack.cells.pop[i]; pop[g] = pack.cells.pop[i];
road[g] = pack.cells.road[i]; routes[g] = pack.cells.routes[i];
crossroad[g] = pack.cells.crossroad[i];
s[g] = pack.cells.s[i]; s[g] = pack.cells.s[i];
state[g] = pack.cells.state[i]; state[g] = pack.cells.state[i];
province[g] = pack.cells.province[i]; province[g] = pack.cells.province[i];
@ -353,8 +352,7 @@ function editHeightmap(options) {
// assign saved pack data from grid back to pack // assign saved pack data from grid back to pack
const n = pack.cells.i.length; const n = pack.cells.i.length;
pack.cells.pop = new Float32Array(n); pack.cells.pop = new Float32Array(n);
pack.cells.road = new Uint16Array(n); pack.cells.routes = {};
pack.cells.crossroad = new Uint16Array(n);
pack.cells.s = new Uint16Array(n); pack.cells.s = new Uint16Array(n);
pack.cells.burg = new Uint16Array(n); pack.cells.burg = new Uint16Array(n);
pack.cells.state = new Uint16Array(n); pack.cells.state = new Uint16Array(n);
@ -389,8 +387,7 @@ function editHeightmap(options) {
if (!isLand) continue; if (!isLand) continue;
pack.cells.culture[i] = culture[g]; pack.cells.culture[i] = culture[g];
pack.cells.pop[i] = pop[g]; pack.cells.pop[i] = pop[g];
pack.cells.road[i] = road[g]; pack.cells.routes[i] = routes[g];
pack.cells.crossroad[i] = crossroad[g];
pack.cells.s[i] = s[g]; pack.cells.s[i] = s[g];
pack.cells.state[i] = state[g]; pack.cells.state[i] = state[g];
pack.cells.province[i] = province[g]; pack.cells.province[i] = province[g];

View file

@ -50,6 +50,7 @@ function handleKeyup(event) {
else if (shift && code === "KeyO") editNotes(); else if (shift && code === "KeyO") editNotes();
else if (shift && code === "KeyA") overviewCharts(); else if (shift && code === "KeyA") overviewCharts();
else if (shift && code === "KeyT") overviewBurgs(); else if (shift && code === "KeyT") overviewBurgs();
else if (shift && code === "KeyU") overviewRoutes();
else if (shift && code === "KeyV") overviewRivers(); else if (shift && code === "KeyV") overviewRivers();
else if (shift && code === "KeyM") overviewMilitary(); else if (shift && code === "KeyM") overviewMilitary();
else if (shift && code === "KeyK") overviewMarkers(); else if (shift && code === "KeyK") overviewMarkers();
@ -57,7 +58,7 @@ function handleKeyup(event) {
else if (key === "!") toggleAddBurg(); else if (key === "!") toggleAddBurg();
else if (key === "@") toggleAddLabel(); else if (key === "@") toggleAddLabel();
else if (key === "#") toggleAddRiver(); else if (key === "#") toggleAddRiver();
else if (key === "$") toggleAddRoute(); else if (key === "$") createRoute();
else if (key === "%") toggleAddMarker(); else if (key === "%") toggleAddMarker();
else if (alt && code === "KeyB") console.table(pack.burgs); else if (alt && code === "KeyB") console.table(pack.burgs);
else if (alt && code === "KeyS") console.table(pack.states); else if (alt && code === "KeyS") console.table(pack.states);

View file

@ -48,8 +48,7 @@ function editLake() {
document.getElementById("lakeArea").value = si(getArea(l.area)) + " " + getAreaUnit(); document.getElementById("lakeArea").value = si(getArea(l.area)) + " " + getAreaUnit();
const length = d3.polygonLength(l.vertices.map(v => pack.vertices.p[v])); const length = d3.polygonLength(l.vertices.map(v => pack.vertices.p[v]));
document.getElementById("lakeShoreLength").value = document.getElementById("lakeShoreLength").value = si(length * distanceScale) + " " + distanceUnitInput.value;
si(length * distanceScaleInput.value) + " " + distanceUnitInput.value;
const lakeCells = Array.from(cells.i.filter(i => cells.f[i] === l.i)); const lakeCells = Array.from(cells.i.filter(i => cells.f[i] === l.i));
const heights = lakeCells.map(i => cells.h[i]); const heights = lakeCells.map(i => cells.h[i]);

View file

@ -169,6 +169,7 @@ function restoreLayers() {
if (layerIsOn("toggleGrid")) drawGrid(); if (layerIsOn("toggleGrid")) drawGrid();
if (layerIsOn("toggleCoordinates")) drawCoordinates(); if (layerIsOn("toggleCoordinates")) drawCoordinates();
if (layerIsOn("toggleCompass")) compass.style("display", "block"); if (layerIsOn("toggleCompass")) compass.style("display", "block");
if (layerIsOn("toggleRoutes")) drawRoutes();
if (layerIsOn("toggleTemp")) drawTemp(); if (layerIsOn("toggleTemp")) drawTemp();
if (layerIsOn("togglePrec")) drawPrec(); if (layerIsOn("togglePrec")) drawPrec();
if (layerIsOn("togglePopulation")) drawPopulation(); if (layerIsOn("togglePopulation")) drawPopulation();
@ -392,7 +393,6 @@ function drawTemp() {
const start = findStart(i, t); const start = findStart(i, t);
if (!start) continue; if (!start) continue;
used[i] = 1; used[i] = 1;
//debug.append("circle").attr("r", 3).attr("cx", vertices.p[start][0]).attr("cy", vertices.p[start][1]).attr("fill", "red").attr("stroke", "black").attr("stroke-width", .3);
const chain = connectVertices(start, t); // vertices chain to form a path const chain = connectVertices(start, t); // vertices chain to form a path
const relaxed = chain.filter((v, i) => i % 4 === 0 || vertices.c[v].some(c => c >= n)); const relaxed = chain.filter((v, i) => i % 4 === 0 || vertices.c[v].some(c => c >= n));
@ -1622,18 +1622,33 @@ function drawRivers() {
function toggleRoutes(event) { function toggleRoutes(event) {
if (!layerIsOn("toggleRoutes")) { if (!layerIsOn("toggleRoutes")) {
turnButtonOn("toggleRoutes"); turnButtonOn("toggleRoutes");
$("#routes").fadeIn(); drawRoutes();
if (event && isCtrlClick(event)) editStyle("routes"); if (event && isCtrlClick(event)) editStyle("routes");
} else { } else {
if (event && isCtrlClick(event)) { if (event && isCtrlClick(event)) return editStyle("routes");
editStyle("routes"); routes.selectAll("path").remove();
return;
}
$("#routes").fadeOut();
turnButtonOff("toggleRoutes"); turnButtonOff("toggleRoutes");
} }
} }
function drawRoutes() {
TIME && console.time("drawRoutes");
const routePaths = {};
for (const route of pack.routes) {
const {i, group} = route;
if (!routePaths[group]) routePaths[group] = [];
routePaths[group].push(`<path id="route${i}" d="${Routes.getPath(route)}"/>`);
}
routes.selectAll("path").remove();
for (const group in routePaths) {
routes.select("#" + group).html(routePaths[group].join(""));
}
TIME && console.timeEnd("drawRoutes");
}
function toggleMilitary() { function toggleMilitary() {
if (!layerIsOn("toggleMilitary")) { if (!layerIsOn("toggleMilitary")) {
turnButtonOn("toggleMilitary"); turnButtonOn("toggleMilitary");
@ -1758,7 +1773,6 @@ function toggleScaleBar(event) {
function drawScaleBar(scaleBar, scaleLevel) { function drawScaleBar(scaleBar, scaleLevel) {
if (!scaleBar.size() || scaleBar.style("display") === "none") return; if (!scaleBar.size() || scaleBar.style("display") === "none") return;
const distanceScale = +distanceScaleInput.value;
const unit = distanceUnitInput.value; const unit = distanceUnitInput.value;
const size = +scaleBar.attr("data-bar-size"); const size = +scaleBar.attr("data-bar-size");

View file

@ -66,7 +66,7 @@ class Measurer {
} }
getDash() { getDash() {
return rn(30 / distanceScaleInput.value, 2); return rn(30 / distanceScale, 2);
} }
drag() { drag() {
@ -205,7 +205,7 @@ class Ruler extends Measurer {
updateLabel() { updateLabel() {
const length = this.getLength(); const length = this.getLength();
const text = rn(length * distanceScaleInput.value) + " " + distanceUnitInput.value; const text = rn(length * distanceScale) + " " + distanceUnitInput.value;
const [x, y] = last(this.points); const [x, y] = last(this.points);
this.el.select("text").attr("x", x).attr("y", y).text(text); this.el.select("text").attr("x", x).attr("y", y).text(text);
} }
@ -337,7 +337,7 @@ class Opisometer extends Measurer {
updateLabel() { updateLabel() {
const length = this.el.select("path").node().getTotalLength(); const length = this.el.select("path").node().getTotalLength();
const text = rn(length * distanceScaleInput.value) + " " + distanceUnitInput.value; const text = rn(length * distanceScale) + " " + distanceUnitInput.value;
const [x, y] = last(this.points); const [x, y] = last(this.points);
this.el.select("text").attr("x", x).attr("y", y).text(text); this.el.select("text").attr("x", x).attr("y", y).text(text);
} }
@ -475,7 +475,7 @@ class RouteOpisometer extends Measurer {
updateLabel() { updateLabel() {
const length = this.el.select("path").node().getTotalLength(); const length = this.el.select("path").node().getTotalLength();
const text = rn(length * distanceScaleInput.value) + " " + distanceUnitInput.value; const text = rn(length * distanceScale) + " " + distanceUnitInput.value;
const [x, y] = last(this.points); const [x, y] = last(this.points);
this.el.select("text").attr("x", x).attr("y", y).text(text); this.el.select("text").attr("x", x).attr("y", y).text(text);
} }
@ -486,9 +486,7 @@ class RouteOpisometer extends Measurer {
const cells = pack.cells; const cells = pack.cells;
const c = findCell(mousePoint[0], mousePoint[1]); const c = findCell(mousePoint[0], mousePoint[1]);
if (!cells.road[c] && !d3.event.sourceEvent.shiftKey) { if (!Routes.isConnected(c) && !d3.event.sourceEvent.shiftKey) return;
return;
}
context.trackCell(c, rigth); context.trackCell(c, rigth);
}); });

View file

@ -545,6 +545,7 @@ function applyStoredOptions() {
lock(key); lock(key);
if (key === "points") changeCellsDensity(+value); if (key === "points") changeCellsDensity(+value);
if (key === "distanceScale") distanceScale = +value;
// add saved style presets to options // add saved style presets to options
if (key.slice(0, 5) === "style") applyOption(stylePreset, key, key.slice(5)); if (key.slice(0, 5) === "style") applyOption(stylePreset, key, key.slice(5));
@ -605,7 +606,8 @@ function randomizeOptions() {
// 'Units Editor' settings // 'Units Editor' settings
const US = navigator.language === "en-US"; const US = navigator.language === "en-US";
if (randomize || !locked("distanceScale")) distanceScaleOutput.value = distanceScaleInput.value = gauss(3, 1, 1, 5); if (randomize || !locked("distanceScale"))
distanceScale = distanceScaleOutput.value = distanceScaleInput.value = gauss(3, 1, 1, 5);
if (!stored("distanceUnit")) distanceUnitInput.value = US ? "mi" : "km"; if (!stored("distanceUnit")) distanceUnitInput.value = US ? "mi" : "km";
if (!stored("heightUnit")) heightUnit.value = US ? "ft" : "m"; if (!stored("heightUnit")) heightUnit.value = US ? "ft" : "m";
if (!stored("temperatureScale")) temperatureScale.value = US ? "°F" : "°C"; if (!stored("temperatureScale")) temperatureScale.value = US ? "°F" : "°C";

View file

@ -886,7 +886,7 @@ function editProvinces() {
const p = d3.mouse(this); const p = d3.mouse(this);
moveCircle(p[0], p[1], r); moveCircle(p[0], p[1], r);
const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)]; const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1])];
const selection = found.filter(isLand); const selection = found.filter(isLand);
if (selection) changeForSelection(selection); if (selection) changeForSelection(selection);
}); });

View file

@ -5,12 +5,15 @@ function editRiver(id) {
closeDialogs(".stable"); closeDialogs(".stable");
if (!layerIsOn("toggleRivers")) toggleRivers(); if (!layerIsOn("toggleRivers")) toggleRivers();
document.getElementById("toggleCells").dataset.forced = +!layerIsOn("toggleCells"); byId("toggleCells").dataset.forced = +!layerIsOn("toggleCells");
if (!layerIsOn("toggleCells")) toggleCells(); if (!layerIsOn("toggleCells")) toggleCells();
elSelected = d3.select("#" + id).on("click", addControlPoint); elSelected = d3.select("#" + id).on("click", addControlPoint);
tip("Drag control points to change the river course. Click on point to remove it. Click on river to add additional control point. For major changes please create a new river instead", true); tip(
"Drag control points to change the river course. Click on point to remove it. Click on river to add additional control point. For major changes please create a new river instead",
true
);
debug.append("g").attr("id", "controlCells"); debug.append("g").attr("id", "controlCells");
debug.append("g").attr("id", "controlPoints"); debug.append("g").attr("id", "controlPoints");
@ -33,18 +36,18 @@ function editRiver(id) {
modules.editRiver = true; modules.editRiver = true;
// add listeners // add listeners
document.getElementById("riverCreateSelectingCells").addEventListener("click", createRiver); byId("riverCreateSelectingCells").on("click", createRiver);
document.getElementById("riverEditStyle").addEventListener("click", () => editStyle("rivers")); byId("riverEditStyle").on("click", () => editStyle("rivers"));
document.getElementById("riverElevationProfile").addEventListener("click", showElevationProfile); byId("riverElevationProfile").on("click", showRiverElevationProfile);
document.getElementById("riverLegend").addEventListener("click", editRiverLegend); byId("riverLegend").on("click", editRiverLegend);
document.getElementById("riverRemove").addEventListener("click", removeRiver); byId("riverRemove").on("click", removeRiver);
document.getElementById("riverName").addEventListener("input", changeName); byId("riverName").on("input", changeName);
document.getElementById("riverType").addEventListener("input", changeType); byId("riverType").on("input", changeType);
document.getElementById("riverNameCulture").addEventListener("click", generateNameCulture); byId("riverNameCulture").on("click", generateNameCulture);
document.getElementById("riverNameRandom").addEventListener("click", generateNameRandom); byId("riverNameRandom").on("click", generateNameRandom);
document.getElementById("riverMainstem").addEventListener("change", changeParent); byId("riverMainstem").on("change", changeParent);
document.getElementById("riverSourceWidth").addEventListener("input", changeSourceWidth); byId("riverSourceWidth").on("input", changeSourceWidth);
document.getElementById("riverWidthFactor").addEventListener("input", changeWidthFactor); byId("riverWidthFactor").on("input", changeWidthFactor);
function getRiver() { function getRiver() {
const riverId = +elSelected.attr("id").slice(5); const riverId = +elSelected.attr("id").slice(5);
@ -55,10 +58,10 @@ function editRiver(id) {
function updateRiverData() { function updateRiverData() {
const r = getRiver(); const r = getRiver();
document.getElementById("riverName").value = r.name; byId("riverName").value = r.name;
document.getElementById("riverType").value = r.type; byId("riverType").value = r.type;
const parentSelect = document.getElementById("riverMainstem"); const parentSelect = byId("riverMainstem");
parentSelect.options.length = 0; parentSelect.options.length = 0;
const parent = r.parent || r.i; const parent = r.parent || r.i;
const sortedRivers = pack.rivers.slice().sort((a, b) => (a.name > b.name ? 1 : -1)); const sortedRivers = pack.rivers.slice().sort((a, b) => (a.name > b.name ? 1 : -1));
@ -66,11 +69,11 @@ function editRiver(id) {
const opt = new Option(river.name, river.i, false, river.i === parent); const opt = new Option(river.name, river.i, false, river.i === parent);
parentSelect.options.add(opt); parentSelect.options.add(opt);
}); });
document.getElementById("riverBasin").value = pack.rivers.find(river => river.i === r.basin).name; byId("riverBasin").value = pack.rivers.find(river => river.i === r.basin).name;
document.getElementById("riverDischarge").value = r.discharge + " m³/s"; byId("riverDischarge").value = r.discharge + " m³/s";
document.getElementById("riverSourceWidth").value = r.sourceWidth; byId("riverSourceWidth").value = r.sourceWidth;
document.getElementById("riverWidthFactor").value = r.widthFactor; byId("riverWidthFactor").value = r.widthFactor;
updateRiverLength(r); updateRiverLength(r);
updateRiverWidth(r); updateRiverWidth(r);
@ -78,8 +81,8 @@ function editRiver(id) {
function updateRiverLength(river) { function updateRiverLength(river) {
river.length = rn(elSelected.node().getTotalLength() / 2, 2); river.length = rn(elSelected.node().getTotalLength() / 2, 2);
const lengthUI = `${rn(river.length * distanceScaleInput.value)} ${distanceUnitInput.value}`; const lengthUI = `${rn(river.length * distanceScale)} ${distanceUnitInput.value}`;
document.getElementById("riverLength").value = lengthUI; byId("riverLength").value = lengthUI;
} }
function updateRiverWidth(river) { function updateRiverWidth(river) {
@ -88,8 +91,8 @@ function editRiver(id) {
const meanderedPoints = addMeandering(cells); const meanderedPoints = addMeandering(cells);
river.width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, sourceWidth)); river.width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, sourceWidth));
const width = `${rn(river.width * distanceScaleInput.value, 3)} ${distanceUnitInput.value}`; const width = `${rn(river.width * distanceScale, 3)} ${distanceUnitInput.value}`;
document.getElementById("riverWidth").value = width; byId("riverWidth").value = width;
} }
function drawControlPoints(points) { function drawControlPoints(points) {
@ -163,7 +166,7 @@ function editRiver(id) {
elSelected.attr("d", path); elSelected.attr("d", path);
updateRiverLength(river); updateRiverLength(river);
if (modules.elevation) showEPForRiver(elSelected.node()); if (byId("elevationProfile").offsetParent) showRiverElevationProfile();
} }
function addControlPoint() { function addControlPoint() {
@ -209,7 +212,7 @@ function editRiver(id) {
const r = getRiver(); const r = getRiver();
r.parent = +this.value; r.parent = +this.value;
r.basin = pack.rivers.find(river => river.i === r.parent).basin; r.basin = pack.rivers.find(river => river.i === r.parent).basin;
document.getElementById("riverBasin").value = pack.rivers.find(river => river.i === r.basin).name; byId("riverBasin").value = pack.rivers.find(river => river.i === r.basin).name;
} }
function changeSourceWidth() { function changeSourceWidth() {
@ -226,9 +229,14 @@ function editRiver(id) {
redrawRiver(); redrawRiver();
} }
function showElevationProfile() { function showRiverElevationProfile() {
modules.elevation = true; const points = debug
showEPForRiver(elSelected.node()); .selectAll("#controlPoints > *")
.data()
.map(([x, y]) => findCell(x, y));
const river = getRiver();
const riverLen = rn(river.length * distanceScale);
showElevationProfile(points, riverLen, true);
} }
function editRiverLegend() { function editRiverLegend() {
@ -266,8 +274,8 @@ function editRiver(id) {
unselect(); unselect();
clearMainTip(); clearMainTip();
const forced = +document.getElementById("toggleCells").dataset.forced; const forced = +byId("toggleCells").dataset.forced;
document.getElementById("toggleCells").dataset.forced = 0; byId("toggleCells").dataset.forced = 0;
if (forced && layerIsOn("toggleCells")) toggleCells(); if (forced && layerIsOn("toggleCells")) toggleCells();
} }
} }

View file

@ -1,4 +1,5 @@
"use strict"; "use strict";
function overviewRivers() { function overviewRivers() {
if (customization) return; if (customization) return;
closeDialogs("#riversOverview, .stable"); closeDialogs("#riversOverview, .stable");
@ -34,8 +35,8 @@ function overviewRivers() {
for (const r of pack.rivers) { for (const r of pack.rivers) {
const discharge = r.discharge + " m³/s"; const discharge = r.discharge + " m³/s";
const length = rn(r.length * distanceScaleInput.value) + " " + unit; const length = rn(r.length * distanceScale) + " " + unit;
const width = rn(r.width * distanceScaleInput.value, 3) + " " + unit; const width = rn(r.width * distanceScale, 3) + " " + unit;
const basin = pack.rivers.find(river => river.i === r.basin)?.name; const basin = pack.rivers.find(river => river.i === r.basin)?.name;
lines += /* html */ `<div lines += /* html */ `<div
@ -49,7 +50,7 @@ function overviewRivers() {
data-basin="${basin}" data-basin="${basin}"
> >
<span data-tip="Click to focus on river" class="icon-dot-circled pointer"></span> <span data-tip="Click to focus on river" class="icon-dot-circled pointer"></span>
<div data-tip="River name" class="riverName">${r.name}</div> <div data-tip="River name" style="margin-left: 0.4em;" class="riverName">${r.name}</div>
<div data-tip="River type name" class="riverType">${r.type}</div> <div data-tip="River type name" class="riverType">${r.type}</div>
<div data-tip="River discharge (flux power)" class="biomeArea">${discharge}</div> <div data-tip="River discharge (flux power)" class="biomeArea">${discharge}</div>
<div data-tip="River length from source to mouth" class="biomeArea">${length}</div> <div data-tip="River length from source to mouth" class="biomeArea">${length}</div>
@ -66,16 +67,18 @@ function overviewRivers() {
const averageDischarge = rn(d3.mean(pack.rivers.map(r => r.discharge))); const averageDischarge = rn(d3.mean(pack.rivers.map(r => r.discharge)));
riversFooterDischarge.innerHTML = averageDischarge + " m³/s"; riversFooterDischarge.innerHTML = averageDischarge + " m³/s";
const averageLength = rn(d3.mean(pack.rivers.map(r => r.length))); const averageLength = rn(d3.mean(pack.rivers.map(r => r.length)));
riversFooterLength.innerHTML = averageLength * distanceScaleInput.value + " " + unit; riversFooterLength.innerHTML = averageLength * distanceScale + " " + unit;
const averageWidth = rn(d3.mean(pack.rivers.map(r => r.width)), 3); const averageWidth = rn(d3.mean(pack.rivers.map(r => r.width)), 3);
riversFooterWidth.innerHTML = rn(averageWidth * distanceScaleInput.value, 3) + " " + unit; riversFooterWidth.innerHTML = rn(averageWidth * distanceScale, 3) + " " + unit;
// add listeners // add listeners
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => riverHighlightOn(ev))); body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => riverHighlightOn(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => riverHighlightOff(ev))); body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => riverHighlightOff(ev)));
body.querySelectorAll("div > span.icon-dot-circled").forEach(el => el.addEventListener("click", zoomToRiver)); body.querySelectorAll("div > span.icon-dot-circled").forEach(el => el.addEventListener("click", zoomToRiver));
body.querySelectorAll("div > span.icon-pencil").forEach(el => el.addEventListener("click", openRiverEditor)); body.querySelectorAll("div > span.icon-pencil").forEach(el => el.addEventListener("click", openRiverEditor));
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.addEventListener("click", triggerRiverRemove)); body
.querySelectorAll("div > span.icon-trash-empty")
.forEach(el => el.addEventListener("click", triggerRiverRemove));
applySorting(riversHeader); applySorting(riversHeader);
} }
@ -110,7 +113,18 @@ function overviewRivers() {
} else { } else {
rivers.attr("data-basin", "hightlighted"); rivers.attr("data-basin", "hightlighted");
const basins = [...new Set(pack.rivers.map(r => r.basin))]; const basins = [...new Set(pack.rivers.map(r => r.basin))];
const colors = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"]; const colors = [
"#1f77b4",
"#ff7f0e",
"#2ca02c",
"#d62728",
"#9467bd",
"#8c564b",
"#e377c2",
"#7f7f7f",
"#bcbd22",
"#17becf"
];
basins.forEach((b, i) => { basins.forEach((b, i) => {
const color = colors[i % colors.length]; const color = colors[i % colors.length];
@ -129,8 +143,8 @@ function overviewRivers() {
body.querySelectorAll(":scope > div").forEach(function (el) { body.querySelectorAll(":scope > div").forEach(function (el) {
const d = el.dataset; const d = el.dataset;
const discharge = d.discharge + " m³/s"; const discharge = d.discharge + " m³/s";
const length = rn(d.length * distanceScaleInput.value) + " " + distanceUnitInput.value; const length = rn(d.length * distanceScale) + " " + distanceUnitInput.value;
const width = rn(d.width * distanceScaleInput.value, 3) + " " + distanceUnitInput.value; const width = rn(d.width * distanceScale, 3) + " " + distanceUnitInput.value;
data += [d.id, d.name, d.type, discharge, length, width, d.basin].join(",") + "\n"; data += [d.id, d.name, d.type, discharge, length, width, d.basin].join(",") + "\n";
}); });

View file

@ -0,0 +1,85 @@
"use strict";
function editRouteGroups() {
if (customization) return;
if (!layerIsOn("toggleRoutes")) toggleRoutes();
addLines();
$("#routeGroupsEditor").dialog({
title: "Edit Route groups",
resizable: false,
position: {my: "left top", at: "left+10 top+140", of: "#map"}
});
if (modules.editRouteGroups) return;
modules.editRouteGroups = true;
// add listeners
byId("routeGroupsEditorAdd").addEventListener("click", addGroup);
byId("routeGroupsEditorBody").on("click", ev => {
const group = ev.target.parentNode.dataset.id;
if (ev.target.classList.contains("editStyle")) editStyle("routes", group);
else if (ev.target.classList.contains("removeGroup")) removeGroup(group);
});
function addLines() {
byId("routeGroupsEditorBody").innerHTML = "";
const lines = Array.from(routes.selectAll("g")._groups[0]).map(el => {
const count = el.children.length;
return /* html */ `<div data-id="${el.id}" class="states" style="display: flex; justify-content: space-between;">
<span>${el.id} (${count})</span>
<div style="width: auto; display: flex; gap: 0.4em;">
<span data-tip="Edit style" class="editStyle icon-brush pointer" style="font-size: smaller;"></span>
<span data-tip="Remove group" class="removeGroup icon-trash pointer"></span>
</div>
</div>`;
});
byId("routeGroupsEditorBody").innerHTML = lines.join("");
}
const DEFAULT_GROUPS = ["roads", "trails", "searoutes"];
function addGroup() {
prompt("Type group name", {default: "route-group-new"}, v => {
let group = v
.toLowerCase()
.replace(/ /g, "_")
.replace(/[^\w\s]/gi, "");
if (!group) return tip("Invalid group name", false, "error");
if (!group.startsWith("route-")) group = "route-" + group;
if (byId(group)) return tip("Element with this name already exists. Provide a unique name", false, "error");
if (Number.isFinite(+group.charAt(0))) return tip("Group name should start with a letter", false, "error");
routes
.append("g")
.attr("id", group)
.attr("stroke", "#000000")
.attr("stroke-width", 0.5)
.attr("stroke-dasharray", "1 0.5")
.attr("stroke-linecap", "butt");
byId("routeGroup")?.options.add(new Option(group, group));
addLines();
byId("routeCreatorGroupSelect").options.add(new Option(group, group));
});
}
function removeGroup(group) {
confirmationDialog({
title: "Remove route group",
message:
"Are you sure you want to remove the entire route group? All routes in this group will be removed. This action can't be reverted.",
confirm: "Remove",
onConfirm: () => {
const routes = pack.routes.filter(r => r.group === group);
routes.forEach(r => Routes.remove(r));
if (DEFAULT_GROUPS.includes(group)) routes.select(`#${group}`).remove();
addLines();
}
});
}
}

View file

@ -0,0 +1,140 @@
"use strict";
function createRoute(defaultGroup) {
if (customization) return;
closeDialogs();
if (!layerIsOn("toggleRoutes")) toggleRoutes();
byId("toggleCells").dataset.forced = +!layerIsOn("toggleCells");
if (!layerIsOn("toggleCells")) toggleCells();
tip("Click to add route point, click again to remove", true);
debug.append("g").attr("id", "controlCells");
debug.append("g").attr("id", "controlPoints");
viewbox.style("cursor", "crosshair").on("click", onClick);
createRoute.points = [];
const body = byId("routeCreatorBody");
// update route groups
byId("routeCreatorGroupSelect").innerHTML = Array.from(routes.selectAll("g")._groups[0]).map(el => {
const selected = defaultGroup || "roads";
return `<option value="${el.id}" ${el.id === selected ? "selected" : ""}>${el.id}</option>`;
});
$("#routeCreator").dialog({
title: "Create Route",
resizable: false,
position: {my: "left top", at: "left+10 top+10", of: "#map"},
close: closeRouteCreator
});
if (modules.createRoute) return;
modules.createRoute = true;
// add listeners
byId("routeCreatorGroupSelect").on("change", () => drawRoute(createRoute.points));
byId("routeCreatorGroupEdit").on("click", editRouteGroups);
byId("routeCreatorComplete").on("click", completeCreation);
byId("routeCreatorCancel").on("click", () => $("#routeCreator").dialog("close"));
body.on("click", ev => {
if (ev.target.classList.contains("icon-trash-empty")) removePoint(ev.target.parentNode.dataset.point);
});
function onClick() {
const [x, y] = d3.mouse(this);
const cellId = findCell(x, y);
const point = [rn(x, 2), rn(y, 2), cellId];
createRoute.points.push(point);
drawRoute(createRoute.points);
body.innerHTML += `<div class="editorLine" style="display: grid; grid-template-columns: 1fr 1fr 1fr auto; gap: 1em;" data-point="${point.join(
"-"
)}">
<span><b>Cell</b>: ${cellId}</span>
<span><b>X</b>: ${point[0]}</span>
<span><b>Y</b>: ${point[1]}</span>
<span data-tip="Remove the point" class="icon-trash-empty pointer"></span>
</div>`;
}
function removePoint(pointString) {
createRoute.points = createRoute.points.filter(p => p.join("-") !== pointString);
drawRoute(createRoute.points);
body.querySelector(`[data-point='${pointString}']`)?.remove();
}
function drawRoute(points) {
debug
.select("#controlCells")
.selectAll("polygon")
.data(points)
.join("polygon")
.attr("points", p => getPackPolygon(p[2]))
.attr("class", "current");
debug
.select("#controlPoints")
.selectAll("circle")
.data(points)
.join("circle")
.attr("cx", d => d[0])
.attr("cy", d => d[1])
.attr("r", 0.6);
const group = byId("routeCreatorGroupSelect").value;
routes.select("#routeTemp").remove();
routes
.select("#" + group)
.append("path")
.attr("d", Routes.getPath({group, points}))
.attr("id", "routeTemp");
}
function completeCreation() {
const points = createRoute.points;
if (points.length < 2) return tip("Add at least 2 points", false, "error");
const routeId = Math.max(...pack.routes.map(route => route.i)) + 1;
const group = byId("routeCreatorGroupSelect").value;
const feature = pack.cells.f[points[0][2]];
const route = {points, group, feature, i: routeId};
pack.routes.push(route);
const links = pack.cells.routes;
for (let i = 0; i < points.length; i++) {
const point = points[i];
const nextPoint = points[i + 1];
if (nextPoint) {
const cellId = point[2];
const nextId = nextPoint[2];
if (!links[cellId]) links[cellId] = {};
links[cellId][nextId] = routeId;
if (!links[nextId]) links[nextId] = {};
links[nextId][cellId] = routeId;
}
}
routes.select("#routeTemp").attr("id", "route" + routeId);
editRoute("route" + routeId);
}
function closeRouteCreator() {
body.innerHTML = "";
debug.select("#controlCells").remove();
debug.select("#controlPoints").remove();
routes.select("#routeTemp").remove();
restoreDefaultEvents();
clearMainTip();
const forced = +byId("toggleCells").dataset.forced;
byId("toggleCells").dataset.forced = 0;
if (forced && layerIsOn("toggleCells")) toggleCells();
}
}

View file

@ -1,308 +1,403 @@
"use strict"; "use strict";
const CONTROL_POINST_DISTANCE = 10; function editRoute(id) {
function editRoute(onClick) {
if (customization) return; if (customization) return;
if (!onClick && elSelected && d3.event.target.id === elSelected.attr("id")) return; if (elSelected && id === elSelected.attr("id")) return;
closeDialogs(".stable"); closeDialogs(".stable");
if (!layerIsOn("toggleRoutes")) toggleRoutes(); if (!layerIsOn("toggleRoutes")) toggleRoutes();
byId("toggleCells").dataset.forced = +!layerIsOn("toggleCells");
if (!layerIsOn("toggleCells")) toggleCells();
elSelected = d3.select("#" + id).on("click", addControlPoint);
tip(
"Drag control points to change the route. Click on point to remove it. Click on the route to add additional control point. For major changes please create a new route instead",
true
);
debug.append("g").attr("id", "controlCells");
debug.append("g").attr("id", "controlPoints");
{
const route = getRoute();
updateRouteData(route);
drawControlPoints(route.points);
drawCells(route.points);
updateLockIcon();
}
$("#routeEditor").dialog({ $("#routeEditor").dialog({
title: "Edit Route", title: "Edit Route",
resizable: false, resizable: false,
position: {my: "center top+60", at: "top", of: d3.event, collision: "fit"}, position: {my: "left top", at: "left+10 top+10", of: "#map"},
close: closeRoutesEditor close: closeRouteEditor
}); });
debug.append("g").attr("id", "controlPoints");
const node = onClick ? elSelected.node() : d3.event.target;
elSelected = d3.select(node).on("click", addInterimControlPoint);
drawControlPoints(node);
selectRouteGroup(node);
viewbox.on("touchmove mousemove", showEditorTips);
if (onClick) toggleRouteCreationMode();
if (modules.editRoute) return; if (modules.editRoute) return;
modules.editRoute = true; modules.editRoute = true;
// add listeners // add listeners
document.getElementById("routeGroupsShow").addEventListener("click", showGroupSection); byId("routeCreateSelectingCells").on("click", showCreationDialog);
document.getElementById("routeGroup").addEventListener("change", changeRouteGroup); byId("routeSplit").on("click", togglePressed);
document.getElementById("routeGroupAdd").addEventListener("click", toggleNewGroupInput); byId("routeJoin").on("click", openJoinRoutesDialog);
document.getElementById("routeGroupName").addEventListener("change", createNewGroup); byId("routeElevationProfile").on("click", showRouteElevationProfile);
document.getElementById("routeGroupRemove").addEventListener("click", removeRouteGroup); byId("routeLegend").on("click", editRouteLegend);
document.getElementById("routeGroupsHide").addEventListener("click", hideGroupSection); byId("routeLock").on("click", toggleLockButton);
document.getElementById("routeElevationProfile").addEventListener("click", showElevationProfile); byId("routeRemove").on("click", removeRoute);
byId("routeName").on("input", changeName);
byId("routeGroup").on("input", changeGroup);
byId("routeGroupEdit").on("click", editRouteGroups);
byId("routeEditStyle").on("click", editRouteGroupStyle);
byId("routeGenerateName").on("click", generateName);
document.getElementById("routeEditStyle").addEventListener("click", editGroupStyle); function getRoute() {
document.getElementById("routeSplit").addEventListener("click", toggleRouteSplitMode); const routeId = +elSelected.attr("id").slice(5);
document.getElementById("routeLegend").addEventListener("click", editRouteLegend); return pack.routes.find(route => route.i === routeId);
document.getElementById("routeNew").addEventListener("click", toggleRouteCreationMode);
document.getElementById("routeRemove").addEventListener("click", removeRoute);
function showEditorTips() {
showMainTip();
if (routeNew.classList.contains("pressed")) return;
if (d3.event.target.id === elSelected.attr("id")) tip("Click to add a control point");
else if (d3.event.target.parentNode.id === "controlPoints") tip("Drag to move, click to delete the control point");
} }
function drawControlPoints(node) { function updateRouteData(route) {
const totalLength = node.getTotalLength(); route.name = route.name || Routes.generateName(route);
const increment = totalLength / Math.ceil(totalLength / CONTROL_POINST_DISTANCE); byId("routeName").value = route.name;
for (let i = 0; i <= totalLength; i += increment) {
const point = node.getPointAtLength(i); const routeGroup = byId("routeGroup");
addControlPoint([point.x, point.y]); routeGroup.options.length = 0;
} routes.selectAll("g").each(function () {
routeLength.innerHTML = rn(totalLength * distanceScaleInput.value) + " " + distanceUnitInput.value; routeGroup.options.add(new Option(this.id, this.id, false, this.id === route.group));
});
updateRouteLength(route);
const isWater = route.points.some(([x, y, cellId]) => pack.cells.h[cellId] < 20);
byId("routeElevationProfile").style.display = isWater ? "none" : "inline-block";
} }
function addControlPoint(point, before = null) { function updateRouteLength(route) {
route.length = Routes.getLength(route.i);
byId("routeLength").value = rn(route.length * distanceScale) + " " + distanceUnitInput.value;
}
function drawControlPoints(points) {
debug debug
.select("#controlPoints") .select("#controlPoints")
.insert("circle", before) .selectAll("circle")
.attr("cx", point[0]) .data(points)
.attr("cy", point[1]) .join("circle")
.attr("cx", d => d[0])
.attr("cy", d => d[1])
.attr("r", 0.6) .attr("r", 0.6)
.call(d3.drag().on("drag", dragControlPoint)) .call(d3.drag().on("start", dragControlPoint))
.on("click", clickControlPoint); .on("click", handleControlPointClick);
} }
function addInterimControlPoint() { function drawCells(points) {
const point = d3.mouse(this); debug
const controls = document.getElementById("controlPoints").querySelectorAll("circle"); .select("#controlCells")
const points = Array.from(controls).map(circle => [+circle.getAttribute("cx"), +circle.getAttribute("cy")]); .selectAll("polygon")
const index = getSegmentId(points, point, 2); .data(points)
addControlPoint(point, ":nth-child(" + (index + 1) + ")"); .join("polygon")
.attr("points", p => getPackPolygon(p[2]));
redrawRoute();
} }
function dragControlPoint() { function dragControlPoint() {
const route = getRoute();
const initCell = d3.event.subject[2];
const pointIndex = route.points.indexOf(d3.event.subject);
d3.event.on("drag", function () {
this.setAttribute("cx", d3.event.x); this.setAttribute("cx", d3.event.x);
this.setAttribute("cy", d3.event.y); this.setAttribute("cy", d3.event.y);
redrawRoute();
}
function redrawRoute() { const x = rn(d3.event.x, 2);
lineGen.curve(d3.curveCatmullRom.alpha(0.1)); const y = rn(d3.event.y, 2);
const points = []; const cellId = findCell(x, y);
debug
.select("#controlPoints") this.__data__ = route.points[pointIndex] = [x, y, cellId];
.selectAll("circle") redrawRoute(route);
.each(function () { drawCells(route.points);
points.push([this.getAttribute("cx"), this.getAttribute("cy")]);
}); });
elSelected.attr("d", round(lineGen(points))); d3.event.on("end", () => {
const l = elSelected.node().getTotalLength(); const movedToCell = findCell(d3.event.x, d3.event.y);
routeLength.innerHTML = rn(l * distanceScaleInput.value) + " " + distanceUnitInput.value;
if (modules.elevation) showEPForRoute(elSelected.node()); if (movedToCell !== initCell) {
const prev = route.points[pointIndex - 1];
if (prev) {
removeConnection(initCell, prev[2]);
addConnection(movedToCell, prev[2], route.i);
} }
function showElevationProfile() { const next = route.points[pointIndex + 1];
modules.elevation = true; if (next) {
showEPForRoute(elSelected.node()); removeConnection(initCell, next[2]);
addConnection(movedToCell, next[2], route.i);
} }
function showGroupSection() {
document.querySelectorAll("#routeEditor > button").forEach(el => (el.style.display = "none"));
document.getElementById("routeGroupsSelection").style.display = "inline-block";
} }
function hideGroupSection() {
document.querySelectorAll("#routeEditor > button").forEach(el => (el.style.display = "inline-block"));
document.getElementById("routeGroupsSelection").style.display = "none";
document.getElementById("routeGroupName").style.display = "none";
document.getElementById("routeGroupName").value = "";
document.getElementById("routeGroup").style.display = "inline-block";
}
function selectRouteGroup(node) {
const group = node.parentNode.id;
const select = document.getElementById("routeGroup");
select.options.length = 0; // remove all options
routes.selectAll("g").each(function () {
select.options.add(new Option(this.id, this.id, false, this.id === group));
}); });
} }
function changeRouteGroup() { function redrawRoute(route) {
document.getElementById(this.value).appendChild(elSelected.node()); elSelected.attr("d", Routes.getPath(route));
updateRouteLength(route);
if (byId("elevationProfile").offsetParent) showRouteElevationProfile();
} }
function toggleNewGroupInput() { function addControlPoint() {
if (routeGroupName.style.display === "none") { const route = getRoute();
routeGroupName.style.display = "inline-block"; const [x, y] = d3.mouse(this);
routeGroupName.focus(); const cellId = findCell(x, y);
routeGroup.style.display = "none";
} else { const point = [rn(x, 2), rn(y, 2), cellId];
routeGroupName.style.display = "none"; const isNewCell = !route.points.some(p => p[2] === cellId);
routeGroup.style.display = "inline-block";
} const index = getSegmentId(route.points, point, 2);
route.points.splice(index, 0, point);
// check if added point is in new cell
if (isNewCell) {
const prev = route.points[index - 1];
const next = route.points[index + 1];
if (!prev) ERROR && console.error("Can't add control point to the start of the route");
if (!next) ERROR && console.error("Can't add control point to the end of the route");
if (!prev || !next) return;
removeConnection(prev[2], next[2]);
addConnection(prev[2], cellId, route.i);
addConnection(cellId, next[2], route.i);
drawCells(route.points);
} }
function createNewGroup() { drawControlPoints(route.points);
if (!this.value) { redrawRoute(route);
tip("Please provide a valid group name");
return;
}
const group = this.value
.toLowerCase()
.replace(/ /g, "_")
.replace(/[^\w\s]/gi, "");
if (document.getElementById(group)) {
tip("Element with this id already exists. Please provide a unique name", false, "error");
return;
} }
if (Number.isFinite(+group.charAt(0))) { function handleControlPointClick() {
tip("Group name should start with a letter", false, "error"); const controlPoint = d3.select(this);
return;
} const point = controlPoint.datum();
// just rename if only 1 element left const route = getRoute();
const oldGroup = elSelected.node().parentNode; const index = route.points.indexOf(point);
const basic = ["roads", "trails", "searoutes"].includes(oldGroup.id);
if (!basic && oldGroup.childElementCount === 1) { const isSplitMode = byId("routeSplit").classList.contains("pressed");
document.getElementById("routeGroup").selectedOptions[0].remove(); return isSplitMode ? splitRoute() : removeControlPoint(controlPoint);
document.getElementById("routeGroup").options.add(new Option(group, group, false, true));
oldGroup.id = group; function splitRoute() {
toggleNewGroupInput(); const oldRoutePoints = route.points.slice(0, index + 1);
document.getElementById("routeGroupName").value = ""; const newRoutePoints = route.points.slice(index);
return;
// update old route
route.points = oldRoutePoints;
drawControlPoints(route.points);
drawCells(route.points);
redrawRoute(route);
// create new route
const newRoute = {
i: Math.max(...pack.routes.map(route => route.i)) + 1,
group: route.group,
feature: route.feature,
name: route.name,
points: newRoutePoints
};
pack.routes.push(newRoute);
for (let i = 0; i < newRoute.points.length; i++) {
const cellId = newRoute.points[i][2];
const nextPoint = newRoute.points[i + 1];
if (nextPoint) addConnection(cellId, nextPoint[2], newRoute.i);
} }
const newGroup = elSelected.node().parentNode.cloneNode(false);
document.getElementById("routes").appendChild(newGroup);
newGroup.id = group;
document.getElementById("routeGroup").options.add(new Option(group, group, false, true));
document.getElementById(group).appendChild(elSelected.node());
toggleNewGroupInput();
document.getElementById("routeGroupName").value = "";
}
function removeRouteGroup() {
const group = elSelected.node().parentNode.id;
const basic = ["roads", "trails", "searoutes"].includes(group);
const count = elSelected.node().parentNode.childElementCount;
alertMessage.innerHTML = /* html */ `Are you sure you want to remove ${
basic ? "all elements in the group" : "the entire route group"
}? <br /><br />Routes to be
removed: ${count}`;
$("#alert").dialog({
resizable: false,
title: "Remove route group",
buttons: {
Remove: function () {
$(this).dialog("close");
$("#routeEditor").dialog("close");
hideGroupSection();
if (basic)
routes routes
.select("#" + group) .select("#" + newRoute.group)
.selectAll("path") .append("path")
.remove(); .attr("d", Routes.getPath(newRoute))
else routes.select("#" + group).remove(); .attr("id", "route" + newRoute.i);
byId("routeSplit").classList.remove("pressed");
}
function removeControlPoint(controlPoint) {
const isOnlyPointInCell = route.points.filter(p => p[2] === point[2]).length === 1;
if (isOnlyPointInCell) {
const prev = route.points[index - 1];
const next = route.points[index + 1];
if (prev) removeConnection(prev[2], point[2]);
if (next) removeConnection(point[2], next[2]);
if (prev && next) addConnection(prev[2], next[2], route.i);
}
controlPoint.remove();
route.points = route.points.filter(p => p !== point);
drawCells(route.points);
redrawRoute(route);
}
}
function openJoinRoutesDialog() {
const route = getRoute();
const firstCell = route.points.at(0)[2];
const lastCell = route.points.at(-1)[2];
const candidateRoutes = pack.routes.filter(r => {
if (r.i === route.i) return false;
if (r.group !== route.group) return false;
if (r.points.at(0)[2] === lastCell) return true;
if (r.points.at(-1)[2] === firstCell) return true;
if (r.points.at(0)[2] === firstCell) return true;
if (r.points.at(-1)[2] === lastCell) return true;
return false;
});
if (candidateRoutes.length) {
const options = candidateRoutes.map(r => {
r.name = r.name || Routes.generateName(r);
r.length = r.length || Routes.getLength(r.i);
const length = rn(r.length * distanceScale) + " " + distanceUnitInput.value;
return `<option value="${r.i}">${r.name} (${length})</option>`;
});
alertMessage.innerHTML = /* html */ `<div>Route to join with:
<select>${options.join("")}</select>
</div>`;
$("#alert").dialog({
title: "Join routes",
width: fitContent(),
position: {my: "left top", at: "left+10 top+150", of: "#map"},
buttons: {
Cancel: () => {
$("#alert").dialog("close");
}, },
Cancel: function () { Join: () => {
$(this).dialog("close"); const selectedRouteId = +alertMessage.querySelector("select").value;
const selectedRoute = pack.routes.find(r => r.i === selectedRouteId);
joinRoutes(route, selectedRoute);
tip("Routes joined", false, "success", 5000);
$("#alert").dialog("close");
} }
} }
}); });
} else {
tip("No routes to join with. Route must start or end at current route's start or end cell", false, "error", 4000);
}
} }
function editGroupStyle() { function joinRoutes(route, joinedRoute) {
const g = elSelected.node().parentNode.id; if (route.points.at(-1)[2] === joinedRoute.points.at(0)[2]) {
editStyle("routes", g); // joinedRoute starts at the end of current route
route.points = [...route.points, ...joinedRoute.points.slice(1)];
} else if (route.points.at(0)[2] === joinedRoute.points.at(-1)[2]) {
// joinedRoute ends at the start of current route
route.points = [...joinedRoute.points, ...route.points.slice(1)];
} else if (route.points.at(0)[2] === joinedRoute.points.at(0)[2]) {
// joinedRoute and current route both start at the same cell
route.points = [...route.points.reverse(), ...joinedRoute.points.slice(1)];
} else if (route.points.at(-1)[2] === joinedRoute.points.at(-1)[2]) {
// joinedRoute and current route both end at the same cell
route.points = [...route.points, ...joinedRoute.points.reverse().slice(1)];
} }
function toggleRouteSplitMode() { for (let i = 0; i < route.points.length; i++) {
document.getElementById("routeNew").classList.remove("pressed"); const point = route.points[i];
const nextPoint = route.points[i + 1];
if (nextPoint) addConnection(point[2], nextPoint[2], route.i);
}
Routes.remove(joinedRoute);
drawControlPoints(route.points);
redrawRoute(route);
drawCells(route.points);
}
function showCreationDialog() {
const route = getRoute();
createRoute(route.group);
}
function togglePressed() {
this.classList.toggle("pressed"); this.classList.toggle("pressed");
} }
function clickControlPoint() { function removeConnection(from, to) {
if (routeSplit.classList.contains("pressed")) splitRoute(this); const routes = pack.cells.routes;
else { if (routes[from]) delete routes[from][to];
this.remove(); if (routes[to]) delete routes[to][from];
redrawRoute();
}
} }
function splitRoute(clicked) { function addConnection(from, to, routeId) {
lineGen.curve(d3.curveCatmullRom.alpha(0.1)); const routes = pack.cells.routes;
const group = d3.select(elSelected.node().parentNode);
routeSplit.classList.remove("pressed");
const points1 = [], if (!routes[from]) routes[from] = {};
points2 = []; routes[from][to] = routeId;
let points = points1;
debug
.select("#controlPoints")
.selectAll("circle")
.each(function () {
points.push([this.getAttribute("cx"), this.getAttribute("cy")]);
if (this === clicked) {
points = points2;
points.push([this.getAttribute("cx"), this.getAttribute("cy")]);
}
this.remove();
});
elSelected.attr("d", round(lineGen(points1))); if (!routes[to]) routes[to] = {};
const id = getNextId("route"); routes[to][from] = routeId;
group.append("path").attr("id", id).attr("d", lineGen(points2));
debug.select("#controlPoints").selectAll("circle").remove();
drawControlPoints(elSelected.node());
} }
function toggleRouteCreationMode() { function changeName() {
document.getElementById("routeSplit").classList.remove("pressed"); getRoute().name = this.value;
document.getElementById("routeNew").classList.toggle("pressed");
if (document.getElementById("routeNew").classList.contains("pressed")) {
tip("Click on map to add control points", true);
viewbox.on("click", addPointOnClick).style("cursor", "crosshair");
elSelected.on("click", null);
} else {
clearMainTip();
viewbox.on("click", clicked).style("cursor", "default");
elSelected.on("click", addInterimControlPoint).attr("data-new", null);
}
} }
function addPointOnClick() { function changeGroup() {
// create new route const group = this.value;
if (!elSelected.attr("data-new")) { byId(group).appendChild(elSelected.node());
debug.select("#controlPoints").selectAll("circle").remove(); getRoute().group = group;
const parent = elSelected.node().parentNode;
const id = getNextId("route");
elSelected = d3.select(parent).append("path").attr("id", id).attr("data-new", 1);
} }
addControlPoint(d3.mouse(this)); function generateName() {
redrawRoute(); const route = getRoute();
route.name = routeName.value = Routes.generateName(route);
}
function showRouteElevationProfile() {
const route = getRoute();
const length = rn(route.length * distanceScale);
showElevationProfile(
route.points.map(p => p[2]),
length,
false
);
} }
function editRouteLegend() { function editRouteLegend() {
const id = elSelected.attr("id"); const id = elSelected.attr("id");
editNotes(id, id); const route = getRoute();
editNotes(id, route.name);
}
function editRouteGroupStyle() {
const {group} = getRoute();
editStyle("routes", group);
}
function toggleLockButton() {
const route = getRoute();
route.lock = !route.lock;
updateLockIcon();
}
function updateLockIcon() {
const route = getRoute();
if (route.lock) {
byId("routeLock").classList.remove("icon-lock-open");
byId("routeLock").classList.add("icon-lock");
} else {
byId("routeLock").classList.remove("icon-lock");
byId("routeLock").classList.add("icon-lock-open");
}
} }
function removeRoute() { function removeRoute() {
alertMessage.innerHTML = "Are you sure you want to remove the route?"; alertMessage.innerHTML = "Are you sure you want to remove the route";
$("#alert").dialog({ $("#alert").dialog({
resizable: false, resizable: false,
width: "22em",
title: "Remove route", title: "Remove route",
buttons: { buttons: {
Remove: function () { Remove: function () {
Routes.remove(getRoute());
$(this).dialog("close"); $(this).dialog("close");
elSelected.remove();
$("#routeEditor").dialog("close"); $("#routeEditor").dialog("close");
}, },
Cancel: function () { Cancel: function () {
@ -312,12 +407,16 @@ function editRoute(onClick) {
}); });
} }
function closeRoutesEditor() { function closeRouteEditor() {
elSelected.attr("data-new", null).on("click", null);
clearMainTip();
routeSplit.classList.remove("pressed");
routeNew.classList.remove("pressed");
debug.select("#controlPoints").remove(); debug.select("#controlPoints").remove();
debug.select("#controlCells").remove();
elSelected.on("click", null);
unselect(); unselect();
clearMainTip();
const forced = +byId("toggleCells").dataset.forced;
byId("toggleCells").dataset.forced = 0;
if (forced && layerIsOn("toggleCells")) toggleCells();
} }
} }

View file

@ -0,0 +1,187 @@
"use strict";
function overviewRoutes() {
if (customization) return;
closeDialogs("#routesOverview, .stable");
if (!layerIsOn("toggleRoutes")) toggleRoutes();
const body = byId("routesBody");
routesOverviewAddLines();
$("#routesOverview").dialog();
if (modules.overviewRoutes) return;
modules.overviewRoutes = true;
$("#routesOverview").dialog({
title: "Routes Overview",
resizable: false,
width: fitContent(),
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
});
// add listeners
byId("routesOverviewRefresh").on("click", routesOverviewAddLines);
byId("routesCreateNew").on("click", createRoute);
byId("routesExport").on("click", downloadRoutesData);
byId("routesLockAll").on("click", toggleLockAll);
byId("routesRemoveAll").on("click", triggerAllRoutesRemove);
// add line for each route
function routesOverviewAddLines() {
body.innerHTML = "";
let lines = "";
for (const route of pack.routes) {
route.name = route.name || Routes.generateName(route);
route.length = route.length || Routes.getLength(route.i);
const length = rn(route.length * distanceScale) + " " + distanceUnitInput.value;
lines += /* html */ `<div
class="states"
data-id="${route.i}"
data-name="${route.name}"
data-group="${route.group}"
data-length="${route.length}"
>
<span data-tip="Click to focus on route" class="icon-dot-circled pointer"></span>
<div data-tip="Route name" style="width: 15em; margin-left: 0.4em;">${route.name}</div>
<div data-tip="Route group" style="width: 8em;">${route.group}</div>
<div data-tip="Route length" style="width: 6em;">${length}</div>
<span data-tip="Edit route" class="icon-pencil"></span>
<span class="locks pointer ${
route.lock ? "icon-lock" : "icon-lock-open inactive"
}" onmouseover="showElementLockTip(event)"></span>
<span data-tip="Remove route" class="icon-trash-empty"></span>
</div>`;
}
body.insertAdjacentHTML("beforeend", lines);
// update footer
routesFooterNumber.innerHTML = pack.routes.length;
const averageLength = rn(d3.mean(pack.routes.map(r => r.length)));
routesFooterLength.innerHTML = averageLength * distanceScale + " " + distanceUnitInput.value;
// add listeners
body.querySelectorAll("div.states").forEach(el => el.on("mouseenter", routeHighlightOn));
body.querySelectorAll("div.states").forEach(el => el.on("mouseleave", routeHighlightOff));
body.querySelectorAll("div > span.icon-dot-circled").forEach(el => el.on("click", zoomToRoute));
body.querySelectorAll("div > span.icon-pencil").forEach(el => el.on("click", openRouteEditor));
body.querySelectorAll("div > span.locks").forEach(el => el.addEventListener("click", toggleLockStatus));
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.on("click", triggerRouteRemove));
applySorting(routesHeader);
}
function routeHighlightOn(event) {
if (!layerIsOn("toggleRoutes")) toggleRoutes();
const routeId = +event.target.dataset.id;
routes
.select("#route" + routeId)
.attr("stroke", "red")
.attr("stroke-width", 2)
.attr("stroke-dasharray", "none");
}
function routeHighlightOff(e) {
const routeId = +e.target.dataset.id;
routes
.select("#route" + routeId)
.attr("stroke", null)
.attr("stroke-width", null)
.attr("stroke-dasharray", null);
}
function zoomToRoute() {
const r = +this.parentNode.dataset.id;
const route = routes.select("#route" + r).node();
highlightElement(route, 3);
}
function downloadRoutesData() {
let data = "Id,Route,Group,Length\n"; // headers
body.querySelectorAll(":scope > div").forEach(function (el) {
const d = el.dataset;
const length = rn(d.length * distanceScale) + " " + distanceUnitInput.value;
data += [d.id, d.name, d.group, length].join(",") + "\n";
});
const name = getFileName("Routes") + ".csv";
downloadFile(data, name);
}
function openRouteEditor() {
const id = "route" + this.parentNode.dataset.id;
editRoute(id);
}
function toggleLockStatus() {
const routeId = +this.parentNode.dataset.id;
const route = pack.routes[routeId];
route.lock = !route.lock;
if (this.classList.contains("icon-lock")) {
this.classList.remove("icon-lock");
this.classList.add("icon-lock-open");
this.classList.add("inactive");
} else {
this.classList.remove("icon-lock-open");
this.classList.add("icon-lock");
this.classList.remove("inactive");
}
}
function toggleLockAll() {
const allLocked = pack.routes.every(route => route.lock);
pack.routes.forEach(route => {
route.lock = !allLocked;
});
routesOverviewAddLines();
byId("routesLockAll").className = allLocked ? "icon-lock" : "icon-lock-open";
}
function triggerRouteRemove() {
const routeId = +this.parentNode.dataset.id;
alertMessage.innerHTML = `Are you sure you want to remove the route?`;
$("#alert").dialog({
resizable: false,
width: "22em",
title: "Remove route",
buttons: {
Remove: function () {
const route = pack.routes.find(r => r.i === routeId);
Routes.remove(route);
routesOverviewAddLines();
$(this).dialog("close");
},
Cancel: function () {
$(this).dialog("close");
}
}
});
}
function triggerAllRoutesRemove() {
alertMessage.innerHTML = /* html */ `Are you sure you want to remove all routes? This action can't be undone`;
$("#alert").dialog({
resizable: false,
title: "Remove all routes",
buttons: {
Remove: function () {
pack.cells.routes = {};
pack.routes = [];
routes.selectAll("path").remove();
$(this).dialog("close");
$("#routesOverview").dialog("close");
},
Cancel: function () {
$(this).dialog("close");
}
}
});
}
}

View file

@ -503,7 +503,7 @@ styleGridScale.addEventListener("input", function () {
function calculateFriendlyGridSize() { function calculateFriendlyGridSize() {
const size = styleGridScale.value * 25; const size = styleGridScale.value * 25;
const friendly = `${rn(size * distanceScaleInput.value, 2)} ${distanceUnitInput.value}`; const friendly = `${rn(size * distanceScale, 2)} ${distanceUnitInput.value}`;
styleGridSizeFriendly.value = friendly; styleGridSizeFriendly.value = friendly;
} }

View file

@ -258,11 +258,16 @@ window.UISubmap = (function () {
byId("latitudeInput").value = latitudeOutput.value; byId("latitudeInput").value = latitudeOutput.value;
// fix scale // fix scale
distanceScaleInput.value = distanceScaleOutput.value = rn((distanceScale = distanceScaleOutput.value / scale), 2); distanceScale =
distanceScaleInput.value =
distanceScaleOutput.value =
rn((distanceScale = distanceScaleOutput.value / scale), 2);
populationRateInput.value = populationRateOutput.value = rn( populationRateInput.value = populationRateOutput.value = rn(
(populationRate = populationRateOutput.value / scale), (populationRate = populationRateOutput.value / scale),
2 2
); );
customization = 0; customization = 0;
startResample(options); startResample(options);
}, 1000); }, 1000);

View file

@ -22,6 +22,7 @@ toolsContent.addEventListener("click", function (event) {
else if (button === "editZonesButton") editZones(); else if (button === "editZonesButton") editZones();
else if (button === "overviewChartsButton") overviewCharts(); else if (button === "overviewChartsButton") overviewCharts();
else if (button === "overviewBurgsButton") overviewBurgs(); else if (button === "overviewBurgsButton") overviewBurgs();
else if (button === "overviewRoutesButton") overviewRoutes();
else if (button === "overviewRiversButton") overviewRivers(); else if (button === "overviewRiversButton") overviewRivers();
else if (button === "overviewMilitaryButton") overviewMilitary(); else if (button === "overviewMilitaryButton") overviewMilitary();
else if (button === "overviewMarkersButton") overviewMarkers(); else if (button === "overviewMarkersButton") overviewMarkers();
@ -66,7 +67,7 @@ toolsContent.addEventListener("click", function (event) {
if (button === "addLabel") toggleAddLabel(); if (button === "addLabel") toggleAddLabel();
else if (button === "addBurgTool") toggleAddBurg(); else if (button === "addBurgTool") toggleAddBurg();
else if (button === "addRiver") toggleAddRiver(); else if (button === "addRiver") toggleAddRiver();
else if (button === "addRoute") toggleAddRoute(); 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 === "openSubmapMenu") UISubmap.openSubmapMenu();
@ -79,7 +80,7 @@ function processFeatureRegeneration(event, button) {
ReliefIcons(); ReliefIcons();
if (!layerIsOn("toggleRelief")) toggleRelief(); if (!layerIsOn("toggleRelief")) toggleRelief();
} else if (button === "regenerateRoutes") { } else if (button === "regenerateRoutes") {
Routes.regenerate(); regenerateRoutes();
if (!layerIsOn("toggleRoutes")) toggleRoutes(); if (!layerIsOn("toggleRoutes")) toggleRoutes();
} else if (button === "regenerateRivers") regenerateRivers(); } else if (button === "regenerateRivers") regenerateRivers();
else if (button === "regeneratePopulation") recalculatePopulation(); else if (button === "regeneratePopulation") recalculatePopulation();
@ -115,6 +116,14 @@ async function openEmblemEditor() {
editEmblem(type, id, el); editEmblem(type, id, el);
} }
function regenerateRoutes() {
const locked = pack.routes.filter(route => route.lock).map((route, index) => ({...route, i: index}));
Routes.generate(locked);
routes.selectAll("path").remove();
if (layerIsOn("toggleRoutes")) drawRoutes();
}
function regenerateRivers() { function regenerateRivers() {
Rivers.generate(); Rivers.generate();
Lakes.defineGroup(); Lakes.defineGroup();
@ -129,7 +138,7 @@ function recalculatePopulation() {
if (!b.i || b.removed || b.lock) return; if (!b.i || b.removed || b.lock) return;
const i = b.cell; const i = b.cell;
b.population = rn(Math.max((pack.cells.s[i] + pack.cells.road[i] / 2) / 8 + b.i / 1000 + (i % 100) / 1000, 0.1), 3); b.population = rn(Math.max(pack.cells.s[i] / 8 + b.i / 1000 + (i % 100) / 1000, 0.1), 3);
if (b.capital) b.population = b.population * 1.3; // increase capital population if (b.capital) b.population = b.population * 1.3; // increase capital population
if (b.port) b.population = b.population * 1.3; // increase port population if (b.port) b.population = b.population * 1.3; // increase port population
b.population = rn(b.population * gauss(2, 3, 0.6, 20, 3), 3); b.population = rn(b.population * gauss(2, 3, 0.6, 20, 3), 3);
@ -429,7 +438,7 @@ function regenerateBurgs() {
BurgsAndStates.specifyBurgs(); BurgsAndStates.specifyBurgs();
BurgsAndStates.defineBurgFeatures(); BurgsAndStates.defineBurgFeatures();
BurgsAndStates.drawBurgs(); BurgsAndStates.drawBurgs();
Routes.regenerate(); regenerateRoutes();
// remove emblems // remove emblems
document.querySelectorAll("[id^=burgCOA]").forEach(el => el.remove()); document.querySelectorAll("[id^=burgCOA]").forEach(el => el.remove());
@ -792,34 +801,6 @@ function addRiverOnClick() {
} }
} }
function toggleAddRoute() {
const pressed = document.getElementById("addRoute").classList.contains("pressed");
if (pressed) {
unpressClickToAddButton();
return;
}
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
addRoute.classList.add("pressed");
closeDialogs(".stable");
viewbox.style("cursor", "crosshair").on("click", addRouteOnClick);
tip("Click on map to add a first control point", true);
if (!layerIsOn("toggleRoutes")) toggleRoutes();
}
function addRouteOnClick() {
unpressClickToAddButton();
const point = d3.mouse(this);
const id = getNextId("route");
elSelected = routes
.select("g")
.append("path")
.attr("id", id)
.attr("data-new", 1)
.attr("d", `M${point[0]},${point[1]}`);
editRoute(true);
}
function toggleAddMarker() { function toggleAddMarker() {
const pressed = document.getElementById("addMarker")?.classList.contains("pressed"); const pressed = document.getElementById("addMarker")?.classList.contains("pressed");
if (pressed) { if (pressed) {
@ -960,6 +941,6 @@ function viewCellDetails() {
} }
async function overviewCharts() { async function overviewCharts() {
const Overview = await import("../dynamic/overview/charts-overview.js?v=1.89.24"); const Overview = await import("../dynamic/overview/charts-overview.js?v=1.99.00");
Overview.open(); Overview.open();
} }

View file

@ -55,6 +55,7 @@ function editUnits() {
} }
function changeDistanceScale() { function changeDistanceScale() {
distanceScale = +this.value;
renderScaleBar(); renderScaleBar();
calculateFriendlyGridSize(); calculateFriendlyGridSize();
} }
@ -90,10 +91,9 @@ function editUnits() {
} }
function restoreDefaultUnits() { function restoreDefaultUnits() {
// distanceScale
distanceScale = 3; distanceScale = 3;
byId("distanceScaleOutput").value = 3; byId("distanceScaleOutput").value = distanceScale;
byId("distanceScaleInput").value = 3; byId("distanceScaleInput").value = distanceScale;
unlock("distanceScale"); unlock("distanceScale");
// units // units
@ -179,13 +179,15 @@ function editUnits() {
tip("Draw a curve along routes to measure length. Hold Shift to measure away from roads.", true); tip("Draw a curve along routes to measure length. Hold Shift to measure away from roads.", true);
unitsBottom.querySelectorAll(".pressed").forEach(button => button.classList.remove("pressed")); unitsBottom.querySelectorAll(".pressed").forEach(button => button.classList.remove("pressed"));
this.classList.add("pressed"); this.classList.add("pressed");
viewbox.style("cursor", "crosshair").call( viewbox.style("cursor", "crosshair").call(
d3.drag().on("start", function () { d3.drag().on("start", function () {
const cells = pack.cells; const cells = pack.cells;
const burgs = pack.burgs; const burgs = pack.burgs;
const point = d3.mouse(this); const point = d3.mouse(this);
const c = findCell(point[0], point[1]); const c = findCell(point[0], point[1]);
if (cells.road[c] || d3.event.sourceEvent.shiftKey) {
if (Routes.isConnected(c) || d3.event.sourceEvent.shiftKey) {
const b = cells.burg[c]; const b = cells.burg[c];
const x = b ? burgs[b].x : cells.p[c][0]; const x = b ? burgs[b].x : cells.p[c][0];
const y = b ? burgs[b].y : cells.p[c][1]; const y = b ? burgs[b].y : cells.p[c][1];
@ -194,7 +196,7 @@ function editUnits() {
d3.event.on("drag", function () { d3.event.on("drag", function () {
const point = d3.mouse(this); const point = d3.mouse(this);
const c = findCell(point[0], point[1]); const c = findCell(point[0], point[1]);
if (cells.road[c] || d3.event.sourceEvent.shiftKey) { if (Routes.isConnected(c) || d3.event.sourceEvent.shiftKey) {
routeOpisometer.trackCell(c, true); routeOpisometer.trackCell(c, true);
} }
}); });

View file

@ -105,13 +105,12 @@ function editWorld() {
calculateMapCoordinates(); calculateMapCoordinates();
const mc = mapCoordinates; const mc = mapCoordinates;
const scale = +distanceScaleInput.value;
const unit = distanceUnitInput.value; const unit = distanceUnitInput.value;
const meridian = toKilometer(eqD * 2 * scale); const meridian = toKilometer(eqD * 2 * distanceScale);
byId("mapSize").innerHTML = `${graphWidth}x${graphHeight}`; byId("mapSize").innerHTML = `${graphWidth}x${graphHeight}`;
byId("mapSizeFriendly").innerHTML = `${rn(graphWidth * scale)}x${rn(graphHeight * scale)} ${unit}`; byId("mapSizeFriendly").innerHTML = `${rn(graphWidth * distanceScale)}x${rn(graphHeight * distanceScale)} ${unit}`;
byId("meridianLength").innerHTML = rn(eqD * 2); byId("meridianLength").innerHTML = rn(eqD * 2);
byId("meridianLengthFriendly").innerHTML = `${rn(eqD * 2 * scale)} ${unit}`; byId("meridianLengthFriendly").innerHTML = `${rn(eqD * 2 * distanceScale)} ${unit}`;
byId("meridianLengthEarth").innerHTML = meridian ? " = " + rn(meridian / 200) + "%🌏" : ""; byId("meridianLengthEarth").innerHTML = meridian ? " = " + rn(meridian / 200) + "%🌏" : "";
byId("mapCoordinates").innerHTML = `${lat(mc.latN)} ${Math.abs(rn(mc.lonW))}°W; ${lat(mc.latS)} ${rn(mc.lonE)}°E`; byId("mapCoordinates").innerHTML = `${lat(mc.latN)} ${Math.abs(rn(mc.lonW))}°W; ${lat(mc.latS)} ${rn(mc.lonE)}°E`;

View file

@ -14,11 +14,27 @@ function toHEX(rgb) {
: ""; : "";
} }
const C_12 = [
"#dababf",
"#fb8072",
"#80b1d3",
"#fdb462",
"#b3de69",
"#fccde5",
"#c6b9c1",
"#bc80bd",
"#ccebc5",
"#ffed6f",
"#8dd3c7",
"#eb8de7"
];
const scaleRainbow = d3.scaleSequential(d3.interpolateRainbow);
// return array of standard shuffled colors // return array of standard shuffled colors
function getColors(number) { function getColors(number) {
const c12 = ["#dababf", "#fb8072", "#80b1d3", "#fdb462", "#b3de69", "#fccde5", "#c6b9c1", "#bc80bd", "#ccebc5", "#ffed6f", "#8dd3c7", "#eb8de7"]; const colors = d3.shuffle(
const cRB = d3.scaleSequential(d3.interpolateRainbow); d3.range(number).map(i => (i < 12 ? C_12[i] : d3.color(scaleRainbow((i - 12) / (number - 12))).hex()))
const colors = d3.shuffle(d3.range(number).map(i => (i < 12 ? c12[i] : d3.color(cRB((i - 12) / (number - 12))).hex()))); );
return colors; return colors;
} }

View file

@ -9,7 +9,6 @@ function clipPoly(points, secure = 0) {
// get segment of any point on polyline // get segment of any point on polyline
function getSegmentId(points, point, step = 10) { function getSegmentId(points, point, step = 10) {
if (points.length === 2) return 1; if (points.length === 2) return 1;
const d2 = (p1, p2) => (p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2;
let minSegment = 1; let minSegment = 1;
let minDist = Infinity; let minDist = Infinity;
@ -18,7 +17,7 @@ function getSegmentId(points, point, step = 10) {
const p1 = points[i]; const p1 = points[i];
const p2 = points[i + 1]; const p2 = points[i + 1];
const length = Math.sqrt(d2(p1, p2)); const length = Math.sqrt(dist2(p1, p2));
const segments = Math.ceil(length / step); const segments = Math.ceil(length / step);
const dx = (p2[0] - p1[0]) / segments; const dx = (p2[0] - p1[0]) / segments;
const dy = (p2[1] - p1[1]) / segments; const dy = (p2[1] - p1[1]) / segments;
@ -26,10 +25,10 @@ function getSegmentId(points, point, step = 10) {
for (let s = 0; s < segments; s++) { for (let s = 0; s < segments; s++) {
const x = p1[0] + s * dx; const x = p1[0] + s * dx;
const y = p1[1] + s * dy; const y = p1[1] + s * dy;
const dist2 = d2(point, [x, y]); const dist = dist2(point, [x, y]);
if (dist2 >= minDist) continue; if (dist >= minDist) continue;
minDist = dist2; minDist = dist;
minSegment = i + 1; minSegment = i + 1;
} }
} }
@ -37,20 +36,6 @@ function getSegmentId(points, point, step = 10) {
return minSegment; return minSegment;
} }
// return center point of common edge of 2 pack cells
function getMiddlePoint(cell1, cell2) {
const {cells, vertices} = pack;
const commonVertices = cells.v[cell1].filter(vertex => vertices.c[vertex].some(cell => cell === cell2));
const [x1, y1] = vertices.p[commonVertices[0]];
const [x2, y2] = vertices.p[commonVertices[1]];
const x = (x1 + x2) / 2;
const y = (y1 + y2) / 2;
return [x, y];
}
function debounce(func, ms) { function debounce(func, ms) {
let isCooldown = false; let isCooldown = false;

58
utils/debugUtils.js Normal file
View file

@ -0,0 +1,58 @@
"use strict";
// FMG utils used for debugging
function drawCellsValue(data) {
debug.selectAll("text").remove();
debug
.selectAll("text")
.data(data)
.enter()
.append("text")
.attr("x", (d, i) => pack.cells.p[i][0])
.attr("y", (d, i) => pack.cells.p[i][1])
.text(d => d);
}
function drawPolygons(data) {
const max = d3.max(data);
const min = d3.min(data);
const scheme = getColorScheme(terrs.select("#landHeights").attr("scheme"));
data = data.map(d => 1 - normalize(d, min, max));
debug.selectAll("polygon").remove();
debug
.selectAll("polygon")
.data(data)
.enter()
.append("polygon")
.attr("points", (d, i) => getGridPolygon(i))
.attr("fill", d => scheme(d))
.attr("stroke", d => scheme(d));
}
function drawRouteConnections() {
debug.select("#connections").remove();
const routes = debug.append("g").attr("id", "connections").attr("stroke-width", 0.4);
const points = pack.cells.p;
const links = pack.cells.routes;
for (const from in links) {
for (const to in links[from]) {
const [x1, y1] = points[from];
const [x3, y3] = points[to];
const [x2, y2] = [(x1 + x3) / 2, (y1 + y3) / 2];
const routeId = links[from][to];
routes
.append("line")
.attr("x1", x1)
.attr("y1", y1)
.attr("x2", x2)
.attr("y2", y2)
.attr("data-id", routeId)
.attr("stroke", C_12[routeId % 12]);
}
}
}

View file

@ -1,7 +1,9 @@
"use strict";
// FMG helper functions
// extracted d3 code to bypass version conflicts // extracted d3 code to bypass version conflicts
// https://github.com/d3/d3-array/blob/main/src/group.js // https://github.com/d3/d3-array/blob/main/src/group.js
function rollups(values, reduce, ...keys) {
export function rollups(values, reduce, ...keys) {
return nest(values, Array.from, reduce, keys); return nest(values, Array.from, reduce, keys);
} }
@ -23,3 +25,7 @@ function nest(values, map, reduce, keys) {
return map(groups); return map(groups);
})(values, 0); })(values, 0);
} }
function dist2([x1, y1], [x2, y2]) {
return (x1 - x2) ** 2 + (y1 - y2) ** 2;
}

View file

@ -312,38 +312,6 @@ void (function addFindAll() {
}; };
})(); })();
// helper function non-used for the generation
function drawCellsValue(data) {
debug.selectAll("text").remove();
debug
.selectAll("text")
.data(data)
.enter()
.append("text")
.attr("x", (d, i) => pack.cells.p[i][0])
.attr("y", (d, i) => pack.cells.p[i][1])
.text(d => d);
}
// helper function non-used for the main generation
function drawPolygons(data) {
const max = d3.max(data);
const min = d3.min(data);
const scheme = getColorScheme(terrs.select("#landHeights").attr("scheme"));
data = data.map(d => 1 - normalize(d, min, max));
debug.selectAll("polygon").remove();
debug
.selectAll("polygon")
.data(data)
.enter()
.append("polygon")
.attr("points", (d, i) => getGridPolygon(i))
.attr("fill", d => scheme(d))
.attr("stroke", d => scheme(d));
}
// draw raster heightmap preview (not used in main generation) // draw raster heightmap preview (not used in main generation)
function drawHeights({heights, width, height, scheme, renderOcean}) { function drawHeights({heights, width, height, scheme, renderOcean}) {
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");

View file

@ -1,7 +1,7 @@
"use strict"; "use strict";
// version and caching control // version and caching control
const version = "1.98.07"; // generator version, update each time const version = "1.99.00"; // generator version, update each time
{ {
document.title += " v" + version; document.title += " v" + version;
@ -28,6 +28,8 @@ const version = "1.98.07"; // generator version, update each time
<ul> <ul>
<strong>Latest changes:</strong> <strong>Latest changes:</strong>
<li>New routes generatation algorithm</li>
<li>Routes overview tool</li>
<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>
@ -41,7 +43,6 @@ const version = "1.98.07"; // generator version, update each time
<li>North and South Poles temperature can be set independently</li> <li>North and South Poles temperature can be set independently</li>
<li>More than 70 new heraldic charges</li> <li>More than 70 new heraldic charges</li>
<li>Multi-color heraldic charges support</li> <li>Multi-color heraldic charges support</li>
<li>New 3D scene options and improvements</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>