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

This commit is contained in:
Azgaar 2021-08-05 00:09:16 +03:00
commit 1180a3c67b
41 changed files with 5185 additions and 3469 deletions

21
.docker/default.conf Normal file
View file

@ -0,0 +1,21 @@
server {
listen 80;
listen [::]:80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; font-src data: 'self'";
add_header X-XSS-Protection "1; mode=block";
add_header X-Frame-Options "DENY";
add_header X-Content-Type-Options nosniff;
add_header Referrer-Policy "strict-origin";
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

2
.gitignore vendored
View file

@ -1,2 +1,2 @@
run_php_server.bat
.bat
.vscode

7
Dockerfile Normal file
View file

@ -0,0 +1,7 @@
FROM nginx:stable-alpine
# Copy the contents of the repo to the container
COPY . /usr/share/nginx/html
# Move the customized nginx config file to the nginx folder
RUN mv /usr/share/nginx/html/.docker/default.conf /etc/nginx/conf.d/default.conf

View file

@ -1,6 +1,6 @@
MIT License
Copyright 2018-2020 Max Ganiev (Azgaar), azgaar.fmg@yandex.by
Copyright 2017-2021 Max Haniyeu (Azgaar), azgaar.fmg@yandex.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -169,23 +169,28 @@ a {
font-size: 0.8em;
}
#statesBody,
#provincesBody {
stroke-width: 2;
fill-rule: evenodd;
mask: url(#land);
}
#relig,
#biomes,
#cults {
fill-rule: evenodd;
mask: url(#land);
#statesBody {
stroke-width: 3;
}
#statesHalo {
fill: none;
filter: url(#blur5);
stroke-linecap: round;
stroke-linejoin: round;
}
#provincesBody {
stroke-width: 0.2;
}
#statesBody,
#provincesBody,
#relig,
#biomes,
#cults {
stroke-linejoin: round;
fill-rule: evenodd;
mask: url(#land);
}
#borders {
@ -197,6 +202,7 @@ a {
stroke: none;
mask: url(#land);
cursor: pointer;
fill-rule: nonzero;
}
#anchors {
@ -979,6 +985,12 @@ body button.noicon {
cursor: pointer;
}
#controlCells > .current {
fill: #82c8ff40;
stroke: #82c8ff;
stroke-width: 0.4;
}
#vertices > circle {
fill: #ff0000;
stroke: #841f1f;
@ -1116,6 +1128,12 @@ div#regimentSelectorBody > div > div {
fill: none;
}
#debug > text {
font-size: 2px;
text-anchor: middle;
dominant-baseline: central;
}
.selectedCell {
stroke-width: 1;
stroke: #da3126;
@ -1697,6 +1715,12 @@ rect.fillRect {
text-align: center;
}
div.editorLine {
margin: 0.2em 0;
padding: 0 0.2em;
font-size: 0.9em;
}
#emblemDownloadControl > input {
width: 4.1em;
}
@ -2058,10 +2082,6 @@ svg.button {
width: 16em;
}
#reliefEditor input[type='number'] {
width: 3em;
}
#reliefIconsDiv {
margin-top: 2px;
padding: 2px;

View file

@ -45,36 +45,36 @@
xmlns:svg="http://www.w3.org/2000/svg">
<defs>
<g id="filters">
<filter id="blurFilter" x="-1" y="-1" width="100" height="100">
<filter id="blurFilter" name="Blur 0.2" x="-1" y="-1" width="100" height="100">
<feGaussianBlur in="SourceGraphic" stdDeviation="0.2"/>
</filter>
<filter id="blur1" x="-1" y="-1" width="100" height="100">
<filter id="blur1" name="Blur 1" x="-1" y="-1" width="100" height="100">
<feGaussianBlur in="SourceGraphic" stdDeviation="1"/>
</filter>
<filter id="blur3" x="-1" y="-1" width="100" height="100">
<filter id="blur3" name="Blur 3" x="-1" y="-1" width="100" height="100">
<feGaussianBlur in="SourceGraphic" stdDeviation="3"/>
</filter>
<filter id="blur5" x="-1" y="-1" width="100" height="100">
<filter id="blur5" name="Blur 5" x="-1" y="-1" width="100" height="100">
<feGaussianBlur in="SourceGraphic" stdDeviation="5"/>
</filter>
<filter id="blur7" x="-1" y="-1" width="100" height="100">
<filter id="blur7" name="Blur 7" x="-1" y="-1" width="100" height="100">
<feGaussianBlur in="SourceGraphic" stdDeviation="7"/>
</filter>
<filter id="blur10" x="-1" y="-1" width="100" height="100">
<filter id="blur10" name="Blur 10" x="-1" y="-1" width="100" height="100">
<feGaussianBlur in="SourceGraphic" stdDeviation="10"/>
</filter>
<filter id="splotch">
<filter id="splotch" name="Splotch">
<feTurbulence type="fractalNoise" baseFrequency=".01" numOctaves="4"/>
<feColorMatrix values="0 0 0 0 0, 0 0 0 0 0, 0 0 0 0 0, 0 0 0 -0.9 1.2" result="texture"/>
<feComposite in="SourceGraphic" in2="texture" operator="in"/>
</filter>
<filter id="bluredSplotch">
<filter id="bluredSplotch" name="Blurred Splotch">
<feTurbulence type="fractalNoise" baseFrequency=".01" numOctaves="4"/>
<feColorMatrix values="0 0 0 0 0, 0 0 0 0 0, 0 0 0 0 0, 0 0 0 -0.9 1.2" result="texture"/>
<feComposite in="SourceGraphic" in2="texture" operator="in"/>
<feGaussianBlur stdDeviation="4"/>
</filter>
<filter id="dropShadow">
<filter id="dropShadow" name="Shadow 2">
<feGaussianBlur in="SourceAlpha" stdDeviation="2"/>
<feOffset dx="1" dy="2"/>
<feMerge>
@ -82,7 +82,7 @@
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<filter id="dropShadow01">
<filter id="dropShadow01" name="Shadow 0.1">
<feGaussianBlur in="SourceAlpha" stdDeviation=".1"/>
<feOffset dx=".2" dy=".3"/>
<feMerge>
@ -90,7 +90,7 @@
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<filter id="dropShadow05">
<filter id="dropShadow05" name="Shadow 0.5">
<feGaussianBlur in="SourceAlpha" stdDeviation=".5"/>
<feOffset dx=".5" dy=".7"/>
<feMerge>
@ -98,23 +98,23 @@
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<filter id="outline">
<filter id="outline" name="Outline">
<feGaussianBlur in="SourceAlpha" stdDeviation="1"/>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<filter id="pencil">
<filter id="pencil" name="Pencil">
<feTurbulence baseFrequency="0.03" numOctaves="6" type="fractalNoise"/>
<feDisplacementMap scale="3" in="SourceGraphic" xChannelSelector="R" yChannelSelector="G"/>
</filter>
<filter id="turbulence">
<filter id="turbulence" name="Turbulence">
<feTurbulence baseFrequency="0.1" numOctaves="3" type="fractalNoise"/>
<feDisplacementMap scale="10" in="SourceGraphic" xChannelSelector="R" yChannelSelector="G"/>
</filter>
<filter id="paper" x="-20%" y="-20%" width="140%" height="140%" filterUnits="objectBoundingBox" primitiveUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<filter id="paper" name="Paper" x="-20%" y="-20%" width="140%" height="140%" filterUnits="objectBoundingBox" primitiveUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feGaussianBlur stdDeviation="1 1" x="0%" y="0%" width="100%" height="100%" in="SourceGraphic" edgeMode="none" result="blur"/>
<feTurbulence type="fractalNoise" baseFrequency="0.05 0.05" numOctaves="4" seed="1" stitchTiles="stitch" result="turbulence"/>
<feDiffuseLighting surfaceScale="2" diffuseConstant="1" lighting-color="#707070" in="turbulence" result="diffuseLighting">
@ -124,7 +124,7 @@
<feComposite in="composite" in2="SourceGraphic" operator="in" x="0%" y="0%" width="100%" height="100%" result="composite1"/>
</filter>
<filter id="crumpled" x="-20%" y="-20%" width="140%" height="140%" filterUnits="objectBoundingBox" primitiveUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<filter id="crumpled" name="Crumpled" x="-20%" y="-20%" width="140%" height="140%" filterUnits="objectBoundingBox" primitiveUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feGaussianBlur stdDeviation="2 2" x="0%" y="0%" width="100%" height="100%" in="SourceGraphic" edgeMode="none" result="blur"/>
<feTurbulence type="turbulence" baseFrequency="0.05 0.05" numOctaves="4" seed="1" stitchTiles="stitch" result="turbulence"/>
<feDiffuseLighting surfaceScale="2" diffuseConstant="1" lighting-color="#828282" in="turbulence" result="diffuseLighting">
@ -134,16 +134,16 @@
<feComposite in="composite" in2="SourceGraphic" operator="in" x="0%" y="0%" width="100%" height="100%" result="composite1"/>
</filter>
<filter id="filter-grayscale">
<filter id="filter-grayscale" name="Grayscale">
<feColorMatrix values="0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0 0 0 1 0"/>
</filter>
<filter id="filter-sepia">
<filter id="filter-sepia" name="Sepia">
<feColorMatrix values="0.393 0.769 0.189 0 0 0.349 0.686 0.168 0 0 0.272 0.534 0.131 0 0 0 0 0 1 0"/>
</filter>
<filter id="filter-dingy">
<filter id="filter-dingy" name="Dingy">
<feColorMatrix values="1 0 0 0 0 0 1 0 0 0 0 0.3 0.3 0 0 0 0 0 1 0"></feColorMatrix>
</filter>
<filter id="filter-tint">
<filter id="filter-tint" name="Tint">
<feColorMatrix values="1.1 0 0 0 0 0 1.1 0 0 0 0 0 0.9 0 0 0 0 0 1 0"></feColorMatrix>
</filter>
</g>
@ -235,7 +235,7 @@
<div id="loading">
<div id="titleName"><t data-t="titleName">Azgaar's</t></div>
<div id="title"><t data-t="title">Fantasy Map Generator</t></div>
<div id="version"><t data-t="version">v. </t>1.63</div>
<div id="version"><t data-t="version">v. </t>1.65</div>
<p id="loading-text"><t data-t="loading">LOADING</t><span>.</span><span>.</span><span>.</span></p>
</div>
@ -319,13 +319,15 @@
</div>
<div id="styleContent" class="tabcontent">
<p data-tip="Select a style preset" style="display: inline-block">Style preset:</p>
<p data-tip="Select a style preset. State labels may required regeneration if font is changed" style="display: inline-block">Style preset:</p>
<select data-tip="Select a style preset" id="stylePreset" onchange="changeStylePreset(this.value)" style="width:45%">
<option value="styleDefault" data-system=1 selected>Default</option>
<option value="styleAncient" data-system=1>Ancient</option>
<option value="styleGloom" data-system=1>Gloom</option>
<option value="styleClean" data-system=1>Clean</option>
<option value="styleMonochrome" data-system=1>Monochrome (for heightmap)</option>
<option value="styleLight" data-system=1>Light</option>
<option value="styleWatercolor" data-system=1>Watercolor</option>
<option value="styleMonochrome" data-system=1>Monochrome</option>
</select>
<button id="addStyleButton" data-tip="Click to save current style as a new preset" class="icon-plus styleButton" style="display: inline-block" onclick="addStylePreset()"></button>
<button id="removeStyleButton" data-tip="Click to remove current custom style preset" class="icon-minus styleButton" style="display: none" onclick="removeStylePreset()"></button>
@ -367,16 +369,15 @@
<option value="compass">Wind Rose</option>
<option value="zones">Zones</option>
</select>
<!-- <button id="restoreStyle" data-tip="Click to restore default style for all elements" class="icon-ccw styleButton" onclick="askToRestoreDefaultStyle()"></button> -->
<table id="styleElements">
<caption id="styleIsOff" data-tip="The selected layer is not visible. See the buttons above to toggle it on">Please ensure the element is toggled on!</caption>
<caption id="styleIsOff" data-tip="The selected layer is not visible. Toogle it on to see style changes effect">Ensure the element visibility is toggled on!</caption>
<tbody id="styleGroup">
<tr data-tip="Select element group">
<td><b>Group</b></td>
<td>
<select id="styleGroupSelect"><option value="regions">regions</option></select>
<select id="styleGroupSelect"></select>
</td>
</tr>
</tbody>
@ -391,24 +392,6 @@
</tr>
</tbody>
<tbody id="styleStates" style="display: block">
<tr data-tip="Set states halo effect width">
<td>Halo width</td>
<td>
<input id="styleStatesHaloWidth" type="range" min=0 max=30 step=.1 value=10>
<output id="styleStatesHaloWidthOutput">10</output>
</td>
</tr>
<tr data-tip="Set states halo effect opacity. 0: invisible, 1: solid">
<td>Halo opacity</td>
<td>
<input id="styleStatesHaloOpacity" type="range" min=0 max=1 step=0.01 value=1>
<output id="styleStatesHaloOpacityOutput">1</output>
</td>
</tr>
</tbody>
<tbody id="styleLegend">
<tr data-tip="Set maximum number of items in one column">
<td>Column items</td>
@ -600,7 +583,7 @@
</tbody>
<tbody id="styleRelief">
<tr data-tip="Select set of relief icons. The change will trigger icons regeneration">
<tr data-tip="Select set of relief icons. All relief icons will be regenerated">
<td>Style</td>
<td>
<select id="styleReliefSet">
@ -611,15 +594,15 @@
</td>
</tr>
<tr data-tip="Define the size of relief icons">
<tr data-tip="Define the size of relief icons. All relief icons will be regenerated">
<td>Size</td>
<td>
<input id="styleReliefSizeInput" data-stored="reliefSize" type="range" min=.2 max=3 step=.01 value=1>
<output id="styleReliefSizeOutput">1</output>
<input id="styleReliefSizeInput" data-stored="reliefSize" type="range" min=.2 max=4 step=.01>
<output id="styleReliefSizeOutput"></output>
</td>
</tr>
<tr data-tip="Define the density of relief icons. Highly affects performance!">
<tr data-tip="Define the density of relief icons. All relief icons will be regenerated. Highly affects performance!">
<td>Density</td>
<td>
<input id="styleReliefDensityInput" data-stored="reliefDensity" type="range" min=.3 max=.8 step=.01 value=.4>
@ -697,8 +680,8 @@
<tr data-tip="Set font size">
<td>Font size</td>
<td>
<button id="styleFontPlus" data-tip="Multiply font size by 1.1" class="whiteButton">+</button>
<button id="styleFontMinus" data-tip="Multiply font size by 0.9" class="whiteButton" >-</button>
<button id="styleFontPlus" data-tip="Increase font" class="whiteButton">+</button>
<button id="styleFontMinus" data-tip="Descrease font" class="whiteButton" >-</button>
<input id="styleFontSize" type="number" min=.5 max=100 step=.1 value=14>
</td>
</tr>
@ -761,6 +744,45 @@
</tr>
</tbody>
<tbody id="styleStates" style="display: block">
<tr data-tip="Set states fill opacity. 0: invisible, 1: solid">
<td>Body opacity</td>
<td>
<input id="styleStatesBodyOpacity" type="range" min=0 max=1 step=0.01>
<output id="styleStatesBodyOpacityOutput"></output>
</td>
</tr>
<tr data-tip="Select filter for states fill. Please note filters may cause performance issues!">
<td>Body filter</td>
<td><select id="styleStatesBodyFilter"/></td>
</tr>
<tr data-tip="Set states halo effect width">
<td>Halo width</td>
<td>
<input id="styleStatesHaloWidth" type="range" min=0 max=30 step=.1 value=10>
<output id="styleStatesHaloWidthOutput">10</output>
</td>
</tr>
<tr data-tip="Set states halo effect opacity. 0: invisible, 1: solid">
<td>Halo opacity</td>
<td>
<input id="styleStatesHaloOpacity" type="range" min=0 max=1 step=0.01 value=1>
<output id="styleStatesHaloOpacityOutput">1</output>
</td>
</tr>
<tr data-tip="Select halo effect power (blur). Set to 0 to make it solid line">
<td>Halo blur</td>
<td>
<input id="styleStatesHaloBlur" type="range" min=0 max=10 step=0.01 value=4>
<output id="styleStatesHaloBlurOutput">4</output>
</td>
</tr>
</tbody>
<tbody id="styleHeightmap">
<tr data-tip="Select color scheme for the element">
<td>Color scheme</td>
@ -830,21 +852,21 @@
<tr data-tip="Set state emblems size multiplier">
<td>State Size</td>
<td>
<input id="styleEmblemsStateSizeInput" data-stored="styleEmblemsStateSize" type="number" min=0 max=5 step=.02 value=1 />
<input id="emblemsStateSizeInput" data-stored="emblemsStateSize" type="number" min=0 max=5 step=.02 value=1 />
</td>
</tr>
<tr data-tip="Set province emblems size multiplier">
<td>Province Size</td>
<td>
<input id="styleEmblemsProvinceSizeInput" data-stored="styleEmblemsProvinceSize" type="number" min=0 max=5 step=.02 value=1 />
<input id="emblemsProvinceSizeInput" data-stored="emblemsProvinceSize" type="number" min=0 max=5 step=.02 value=1 />
</td>
</tr>
<tr data-tip="Set burg emblems size multiplier">
<td>Burg Size</td>
<td>
<input id="styleEmblemsBurgSizeInput" data-stored="styleEmblemsBurgSize" type="number" min=0 max=5 step=.02 value=1 />
<input id="emblemsBurgSizeInput" data-stored="emblemsBurgSize" type="number" min=0 max=5 step=.02 value=1 />
</td>
</tr>
@ -859,29 +881,7 @@
<tbody id="styleFilter" style="display: block">
<tr data-tip="Select filter for element. Please note filters may cause performance issues!">
<td>Filter</td>
<td>
<select id="styleFilterInput">
<option value="" selected>None</option>
<option value="url(#blurFilter)">Blur 0.2</option>
<option value="url(#blur1)">Blur 1</option>
<option value="url(#blur3)">Blur 3</option>
<option value="url(#blur5)">Blur 5</option>
<option value="url(#blur7)">Blur 7</option>
<option value="url(#blur10)">Blur 10</option>
<option value="url(#splotch)">Splotch</option>
<option value="url(#bluredSplotch)">Blurred Splotch</option>
<option value="url(#dropShadow01)">Shadow 0.1</option>
<option value="url(#dropShadow05)">Shadow 0.5</option>
<option value="url(#dropShadow)">Shadow 2</option>
<option value="url(#outline)">Outline</option>
<option value="url(#pencil)">Pencil</option>
<option value="url(#turbulence)">Turbulence</option>
<option value="url(#paper)">Paper</option>
<option value="url(#crumpled)">Crumpled</option>
<option value="url(#filter-grayscale)">Grayscale</option>
<option value="url(#filter-sepia)">Sepia</option>
</select>
</td>
<td><select id="styleFilterInput"/></td>
</tr>
</tbody>
@ -923,6 +923,13 @@
<label for="hideLabels" class="checkbox-label">Toggle visibility automatically</label>
</td>
</tr>
<tr data-tip="Allow system to rescale labels on zoom">
<td colspan=2>
<input id="rescaleLabels" class="checkbox" type="checkbox" onchange="invokeActiveZooming()" checked>
<label for="rescaleLabels" class="checkbox-label">Rescale on zoom</label>
</td>
</tr>
</tbody>
</table>
@ -1013,17 +1020,18 @@
<td>Map template</td>
<td>
<select id="templateInput" data-stored="template">
<option value="Volcano">Volcano</option>
<option value="High Island">High Island</option>
<option value="Low Island">Low Island</option>
<option value="Continents">Two Continents</option>
<option value="Archipelago">Archipelago</option>
<option value="Atoll">Atoll</option>
<option value="Mediterranean">Mediterranean</option>
<option value="Peninsula">Peninsula</option>
<option value="Pangea">Pangea</option>
<option value="Isthmus">Isthmus</option>
<option value="Shattered">Shattered</option>
<option value="volcano">Volcano</option>
<option value="highIsland">High Island</option>
<option value="lowIsland">Low Island</option>
<option value="continents">Two Continents</option>
<option value="archipelago">Archipelago</option>
<option value="atoll">Atoll</option>
<option value="mediterranean">Mediterranean</option>
<option value="peninsula">Peninsula</option>
<option value="pangea">Pangea</option>
<option value="isthmus">Isthmus</option>
<option value="shattered">Shattered</option>
<option value="taklamakan">Taklamakan</option>
</select>
</td>
<td></td>
@ -1291,6 +1299,18 @@
</td>
</tr>
<tr data-tip="Select shape rendering model">
<td></td>
<td>Shape rendering</td>
<td>
<select id="shapeRendering" data-stored="shapeRendering">
<option value="geometricPrecision" selected>Best quality</option>
<option value="optimizeSpeed">Best performace</option>
</select>
</td>
<td></td>
</tr>
<!-- <tr data-tip="Select language (not all languages are fully supported). Reload the page to apply">
<td></td>
<td>Language</td>
@ -1357,7 +1377,7 @@
<div id="addFeature">
<p>Click to add:</p>
<button id="addBurgTool" data-tip="Click on map to place a burg. Hold Shift to add multiple. Shortcut: Shift + 1">Burg</button>
<button id="addBurgTool" data-tip="Click on map to place a burg. Hold <kbd>Shift</kbd> to add multiple. Shortcut: Shift + 1">Burg</button>
<button id="addLabel" data-tip="Click on map to place label. Hold Shift to add multiple. Shortcut: Shift + 2">Label</button>
<button id="addRiver" data-tip="Click on map to place a river. Hold Shift to add multiple. Shortcut: Shift + 3">River</button>
<button id="addRoute" data-tip="Click on map to place a route. Shortcut: Shift + 4">Route</button>
@ -1647,19 +1667,19 @@
<input id="riverWidth" disabled/>
</div>
<div data-tip="River source width in pixels">
<div data-tip="River additional width. Default value is 0">
<div class="label">Source width:</div>
<input id="riverSourceWidth" type="number" min=0 max=3 step=.1 />
</div>
<div data-tip="River width multiplier">
<div data-tip="River width multiplier. Default value is 1">
<div class="label">Width modifier:</div>
<input id="riverWidthFactor" type="number" min=.1 max=4 step=.1 />
</div>
</div>
<div id="riverBottom">
<button id="riverNew" data-tip="Create new river clicking on map" class="icon-map-pin"></button>
<button id="riverCreateSelectingCells" data-tip="Create new river selecting river cells" class="icon-map-pin"></button>
<button id="riverEditStyle" data-tip="Edit style for all rivers in Style Editor" class="icon-brush"></button>
<button id="riverElevationProfile" data-tip="Show the elevation profile for the river" class="icon-chart-area"></button>
<button id="riverLegend" data-tip="Edit free text notes (legend) for the river" class="icon-edit"></button>
@ -1667,6 +1687,14 @@
</div>
</div>
<div id="riverCreator" class="dialog" style="display: none">
<div id="riverCreatorBody" class="table"></div>
<div id="riverCreatorBottom">
<button id="riverCreatorComplete" data-tip="Complete river creation" class="icon-check"></button>
<button id="riverCreatorCancel" data-tip="Cancel the creation" class="icon-cancel"></button>
</div>
</div>
<div id="lakeEditor" class="dialog" style="display: none">
<div id="lakeBody" style="padding-bottom: .3em">
<div>
@ -1927,7 +1955,7 @@
<button id="reliefCopy" data-tip="Copy selected relief icon" class="icon-clone"></button>
<button id="reliefMoveFront" data-tip="Move selected relief icon to front" class="icon-level-up"></button>
<button id="reliefMoveBack" data-tip="Move selected relief icon back" class="icon-level-down"></button>
<button id="reliefRemove" data-tip="Remove selected relief icon. Shortcut: Delete" class="icon-trash fastDelete"></button>
<button id="reliefRemove" data-tip="Remove selected relief icon or icon type. Shortcut: Delete" class="icon-trash fastDelete"></button>
</div>
</div>
@ -2327,18 +2355,19 @@
<div id="templateEditor" class="dialog stable" style="display: none">
<div id="templateTop">
<i>Select template: </i><select id="templateSelect" style="width:16em" data-prev="templateCustom" data-tip="Select base template">
<option value="templateCustom" selected>Custom</option>
<option value="templateVolcano">Volcano</option>
<option value="templateHighIsland">High Island</option>
<option value="templateLowIsland">Low Island</option>
<option value="templateContinents">Two Continents</option>
<option value="templateArchipelago">Archipelago</option>
<option value="templateAtoll">Atoll</option>
<option value="templateMediterranean">Mediterranean</option>
<option value="templatePeninsula">Peninsula</option>
<option value="templatePangea">Pangea</option>
<option value="templateIsthmus">Isthmus</option>
<option value="templateShattered">Shattered</option>
<option value="custom" selected>Custom</option>
<option value="volcano">Volcano</option>
<option value="highIsland">High Island</option>
<option value="lowIsland">Low Island</option>
<option value="continents">Two Continents</option>
<option value="archipelago">Archipelago</option>
<option value="atoll">Atoll</option>
<option value="mediterranean">Mediterranean</option>
<option value="peninsula">Peninsula</option>
<option value="pangea">Pangea</option>
<option value="isthmus">Isthmus</option>
<option value="shattered">Shattered</option>
<option value="taklamakan">Taklamakan</option>
</select>
</div>
<div id="templateTools">
@ -2553,6 +2582,7 @@
<option value="Diarchy">Diarchy</option>
<option value="Federation">Federation</option>
<option value="Free City">Free City</option>
<option value="Most Serene Republic">Most Serene Republic</option>
<option value="Oligarchy">Oligarchy</option>
<option value="Protectorate">Protectorate</option>
<option value="Republic">Republic</option>
@ -2575,10 +2605,17 @@
<option value="Tribes">United Tribes</option>
</optgroup>
<optgroup label="Theocracy">
<option value="Bishopric">Bishopric</option>
<option value="Brotherhood">Brotherhood</option>
<option value="Caliphate">Caliphate</option>
<option value="Diocese">Diocese</option>
<option value="Divine Duchy">Divine Duchy</option>
<option value="Divine Grand Duchy">Divine Grand Duchy</option>
<option value="Divine Principality">Divine Principality</option>
<option value="Divine Kingdom">Divine Kingdom</option>
<option value="Divine Empire">Divine Empire</option>
<option value="Eparchy">Eparchy</option>
<option value="Holy State">Holy State</option>
<option value="Imamah">Imamah</option>
<option value="Theocracy">Theocracy</option>
</optgroup>
@ -3275,7 +3312,8 @@
<div id="riversBottom">
<button id="riversOverviewRefresh" data-tip="Refresh the Editor" class="icon-cw"></button>
<button id="addNewRiver" data-tip="Add a new river. Hold Shift to add multiple" class="icon-plus"></button>
<button id="addNewRiver" data-tip="Automatically add river starting from clicked cell. Hold Shift to add multiple" class="icon-plus"></button>
<button id="riverCreateNew" data-tip="Create 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="riversExport" data-tip="Save rivers-related data as a text file (.csv)" class="icon-download"></button>
<button id="riversRemoveAll" data-tip="Remove all rivers" class="icon-trash"></button>
@ -3432,6 +3470,11 @@
<input id="options3dSunZ" type="number" min=-1500 max=1500 step=100 style="width:4.7em">
</div>
<div data-tip="Toggle 3d labels" style="margin: .6em 0 .3em -.2em">
<input id="options3dMeshLabels3d" class="checkbox" type="checkbox">
<label for="options3dMeshLabels3d" class="checkbox-label"><i>Show 3D labels</i></label>
</div>
<div data-tip="Toggle sky mode" style="margin: .6em 0 .3em -.2em">
<input id="options3dMeshSkyMode" class="checkbox" type="checkbox">
<label for="options3dMeshSkyMode" class="checkbox-label"><i>Show sky and extend water</i></label>
@ -3601,12 +3644,12 @@
<path d="m20,58 h70 m-62,3 h50" stroke="#5c5c70" stroke-dasharray="7, 11" stroke-width="1"></path>
</symbol>
<symbol id="relief-deciduous-1" viewBox="0 0 100 100">
<path d="m50,52 v7 h1 v-7 h-0.5 q13,-7 0,-16 q-13,9 0,16" fill="#fff" stroke="#5c5c70"></path>
<path d="m50,52 q-12,-7 0,-16 q-3.5,10 0,15.5" fill="#999999"></path>
<path d="m49.5,52 v7 h1 v-7 h-0.5 q13,-7 0,-16 q-13,9 0,16" fill="#fff" stroke="#5c5c70"></path>
<path d="M 50,51.5 C 44,49 40,43 50,36.5" fill="#999999"></path>
</symbol>
<symbol id="relief-conifer-1" viewBox="0 0 100 100">
<path d="m50,55 v4 h1 v-4 l4.5,0 -4,-8 l3.5,0 -4.5,-9 -4,9 3,0 -3.5,8 7,0" fill="#fff" stroke="#5c5c70"></path>
<path d="m46,55 l4,-8 -4,0 5,-9 -2.5,9 l1.5,0 -2,8" fill="#999999"></path>
<path d="m49.5,55 v4 h1 v-4 l4.5,0 -4,-8 l3.5,0 -4.5,-9 -4,9 3,0 -3.5,8 7,0" fill="#fff" stroke="#5c5c70"></path>
<path d="m 46,54.5 3.5,-8 H 46.6 L 50,39 v 15.5 z" fill="#999999"></path>
</symbol>
<symbol id="relief-acacia-1" viewBox="0 0 100 100">
<path d="m34.5 44.5 c 1.8, -3 8.1, -5.7 12.6, -5.4 6, -2.2 9.3, -0.9 11.9, 1.3 1.7, 0.2 3.2,-0.3 5.2, 2.2 2.7, 1.2 3.7, 2.4 2.7, 3.7 -1.6, 0.3 -2.2, 0 -4.7, -1.6 -5.2, 0.1 -7, 0.7 -8.7, -0.9 -2.8, 1 -3.6, 0 -9.7, 0.2 -4.6, 0 -8, 1.6 -9.3, 0.4 z" fill="#fff"></path>
@ -3623,7 +3666,7 @@
<path d="m 49.5,53.1 c 0,-3.4 -2.4,-4.8 -3,-5.4 1,1.8 2.4,3.7 1.8,5.4 z M 51,53.2 C 51.4,49.6 49.6,47.9 48,46.8 c 1.1,1.8 2.8,4.6 1.8,6.5 z M 51.4,51.4 c 0.6,-1.9 1.8,-3.4 3,-4.3 -0.8,0.3 -2.9,1.5 -3.4,2.8 0.2,0.4 0.3,0.8 0.4,1.5 z M 52.9,53.2 c -0.7,-1.9 0.5,-3.3 1.5,-4.4 -1.7,1 -3,2.2 -2.7,4.4 z" fill="#5c5c70" stroke="none"></path>
</symbol>
<symbol id="relief-swamp-1" viewBox="0 0 100 100">
<path d="m50,46 v6 l3,-4 m-3,4 l-3,-4 m-7,4.5 h4 m4,0 h4 m4,0 h4" fill="none" stroke="#5c5c70"></path>
<path d="m 50,46 v 6 m 0,0 3,-4 m -3,4 -3,-4 m -6,4.5 h 3 m 4,0 h 4 m 4,0 3,0" fill="none" stroke="#5c5c70" stroke-linecap="round"></path>
</symbol>
<symbol id="relief-dune-1" viewBox="0 0 100 100">
<path d="m 28.7,52.8 c 5,-3.9 10,-8.2 15.8,-8.3 4.5,0 10.8,3.8 15.2,6.5 3.5,2.2 6.8,2 6.8,2" fill="none" stroke="#5c5c70" stroke-width="1.8"></path>
@ -4432,6 +4475,7 @@
<script src="libs/delaunator.min.js"></script>
<script src="modules/utils.js"></script>
<script src="modules/voronoi.js"></script>
<script src="modules/heightmap-templates.js"></script>
<script src="modules/heightmap-generator.js"></script>
<script src="modules/ocean-layers.js"></script>
<script src="modules/river-generator.js"></script>
@ -4448,6 +4492,7 @@
<script src="libs/lineclip.min.js"></script>
<script src="libs/jquery-ui.min.js"></script>
<script src="libs/alea.min.js"></script>
<script src="modules/fonts.js"></script>
<script src="modules/ui/layers.js"></script>
<script src="modules/ui/measurers.js"></script>
@ -4473,6 +4518,7 @@
<script defer src="modules/ui/coastline-editor.js"></script>
<script defer src="modules/ui/labels-editor.js"></script>
<script defer src="modules/ui/rivers-editor.js"></script>
<script defer src="modules/ui/rivers-creator.js"></script>
<script defer src="modules/ui/relief-editor.js"></script>
<script defer src="modules/ui/religions-editor.js"></script>
<script defer src="modules/ui/markers-editor.js"></script>

File diff suppressed because one or more lines are too long

View file

@ -1,163 +1,157 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.Pell = factory());
}(this, (function () { 'use strict';
"use strict";
const defaultParagraphSeparatorString = 'defaultParagraphSeparator'
const formatBlock = 'formatBlock'
const addEventListener = (parent, type, listener) => parent.addEventListener(type, listener)
const appendChild = (parent, child) => parent.appendChild(child)
const createElement = tag => document.createElement(tag)
const queryCommandState = command => document.queryCommandState(command)
const queryCommandValue = command => document.queryCommandValue(command)
const exec = (command, value = null) => document.execCommand(command, false, value)
window.Pell = (function () {
const defaultParagraphSeparatorString = "defaultParagraphSeparator";
const formatBlock = "formatBlock";
const addEventListener = (parent, type, listener) => parent.addEventListener(type, listener);
const appendChild = (parent, child) => parent.appendChild(child);
const createElement = tag => document.createElement(tag);
const queryCommandState = command => document.queryCommandState(command);
const queryCommandValue = command => document.queryCommandValue(command);
const exec = (command, value = null) => document.execCommand(command, false, value);
const defaultActions = {
bold: {
icon: '<b>B</b>',
title: 'Bold',
state: () => queryCommandState('bold'),
result: () => exec('bold')
icon: "<b>B</b>",
title: "Bold",
state: () => queryCommandState("bold"),
result: () => exec("bold")
},
italic: {
icon: '<i>I</i>',
title: 'Italic',
state: () => queryCommandState('italic'),
result: () => exec('italic')
icon: "<i>I</i>",
title: "Italic",
state: () => queryCommandState("italic"),
result: () => exec("italic")
},
underline: {
icon: '<u>U</u>',
title: 'Underline',
state: () => queryCommandState('underline'),
result: () => exec('underline')
icon: "<u>U</u>",
title: "Underline",
state: () => queryCommandState("underline"),
result: () => exec("underline")
},
strikethrough: {
icon: '<strike>S</strike>',
title: 'Strike-through',
state: () => queryCommandState('strikeThrough'),
result: () => exec('strikeThrough')
icon: "<strike>S</strike>",
title: "Strike-through",
state: () => queryCommandState("strikeThrough"),
result: () => exec("strikeThrough")
},
heading1: {
icon: '<b>H<sub>1</sub></b>',
title: 'Heading 1',
result: () => exec(formatBlock, '<h1>')
icon: "<b>H<sub>1</sub></b>",
title: "Heading 1",
result: () => exec(formatBlock, "<h1>")
},
heading2: {
icon: '<b>H<sub>2</sub></b>',
title: 'Heading 2',
result: () => exec(formatBlock, '<h2>')
icon: "<b>H<sub>2</sub></b>",
title: "Heading 2",
result: () => exec(formatBlock, "<h2>")
},
paragraph: {
icon: '&#182;',
title: 'Paragraph',
result: () => exec(formatBlock, '<p>')
icon: "&#182;",
title: "Paragraph",
result: () => exec(formatBlock, "<p>")
},
quote: {
icon: '&#8220; &#8221;',
title: 'Quote',
result: () => exec(formatBlock, '<blockquote>')
icon: "&#8220; &#8221;",
title: "Quote",
result: () => exec(formatBlock, "<blockquote>")
},
olist: {
icon: '&#35;',
title: 'Ordered List',
result: () => exec('insertOrderedList')
icon: "&#35;",
title: "Ordered List",
result: () => exec("insertOrderedList")
},
ulist: {
icon: '&#8226;',
title: 'Unordered List',
result: () => exec('insertUnorderedList')
icon: "&#8226;",
title: "Unordered List",
result: () => exec("insertUnorderedList")
},
code: {
icon: '&lt;/&gt;',
title: 'Code',
result: () => exec(formatBlock, '<pre>')
icon: "&lt;/&gt;",
title: "Code",
result: () => exec(formatBlock, "<pre>")
},
line: {
icon: '&#8213;',
title: 'Horizontal Line',
result: () => exec('insertHorizontalRule')
icon: "&#8213;",
title: "Horizontal Line",
result: () => exec("insertHorizontalRule")
},
link: {
icon: '&#128279;',
title: 'Link',
result: () => navigator.clipboard.readText().then(url => exec('createLink', url))
icon: "&#128279;",
title: "Link",
result: () => navigator.clipboard.readText().then(url => exec("createLink", url))
},
image: {
icon: '&#128247;',
title: 'Image',
icon: "&#128247;",
title: "Image",
result: () => {
navigator.clipboard.readText().then(url => exec('insertImage', url))
exec('enableObjectResizing')
navigator.clipboard.readText().then(url => exec("insertImage", url));
exec("enableObjectResizing");
}
}
}
};
const defaultClasses = {
actionbar: 'pell-actionbar',
button: 'pell-button',
content: 'pell-content',
selected: 'pell-button-selected'
}
actionbar: "pell-actionbar",
button: "pell-button",
content: "pell-content",
selected: "pell-button-selected"
};
const init = settings => {
const actions = settings.actions
? (
settings.actions.map(action => {
if (typeof action === 'string') return defaultActions[action]
else if (defaultActions[action.name]) return { ...defaultActions[action.name], ...action }
return action
? settings.actions.map(action => {
if (typeof action === "string") return defaultActions[action];
else if (defaultActions[action.name]) return {...defaultActions[action.name], ...action};
return action;
})
)
: Object.keys(defaultActions).map(action => defaultActions[action])
const classes = { ...defaultClasses, ...settings.classes }
const defaultParagraphSeparator = settings[defaultParagraphSeparatorString] || 'div'
const actionbar = createElement('div')
actionbar.className = classes.actionbar
appendChild(settings.element, actionbar)
const content = settings.element.content = createElement('div')
content.contentEditable = true
content.className = classes.content
content.oninput = ({ target: { firstChild } }) => {
if (firstChild && firstChild.nodeType === 3) exec(formatBlock, `<${defaultParagraphSeparator}>`)
else if (content.innerHTML === '<br>') content.innerHTML = ''
settings.onChange(content.innerHTML)
}
content.onkeydown = event => {
if (event.key === 'Enter' && queryCommandValue(formatBlock) === 'blockquote') {
setTimeout(() => exec(formatBlock, `<${defaultParagraphSeparator}>`), 0)
}
}
appendChild(settings.element, content)
actions.forEach(action => {
const button = createElement('button')
button.className = classes.button
button.innerHTML = action.icon
button.title = action.title
button.setAttribute('type', 'button')
button.onclick = () => action.result() && content.focus()
if (action.state) {
const handler = () => button.classList[action.state() ? 'add' : 'remove'](classes.selected)
addEventListener(content, 'keyup', handler)
addEventListener(content, 'mouseup', handler)
addEventListener(button, 'click', handler)
}
appendChild(actionbar, button)
})
if (settings.styleWithCSS) exec('styleWithCSS')
exec(defaultParagraphSeparatorString, defaultParagraphSeparator)
return settings.element
}
return {exec, init}
: Object.keys(defaultActions).map(action => defaultActions[action]);
})));
const classes = {...defaultClasses, ...settings.classes};
const defaultParagraphSeparator = settings[defaultParagraphSeparatorString] || "div";
const actionbar = createElement("div");
actionbar.className = classes.actionbar;
appendChild(settings.element, actionbar);
const content = (settings.element.content = createElement("div"));
content.contentEditable = true;
content.className = classes.content;
content.oninput = ({target: {firstChild}}) => {
if (firstChild && firstChild.nodeType === 3) exec(formatBlock, `<${defaultParagraphSeparator}>`);
else if (content.innerHTML === "<br>") content.innerHTML = "";
settings.onChange(content.innerHTML);
};
content.onkeydown = event => {
if (event.key === "Enter" && queryCommandValue(formatBlock) === "blockquote") {
setTimeout(() => exec(formatBlock, `<${defaultParagraphSeparator}>`), 0);
}
};
appendChild(settings.element, content);
actions.forEach(action => {
const button = createElement("button");
button.className = classes.button;
button.innerHTML = action.icon;
button.title = action.title;
button.setAttribute("type", "button");
button.onclick = () => action.result() && content.focus();
if (action.state) {
const handler = () => button.classList[action.state() ? "add" : "remove"](classes.selected);
addEventListener(content, "keyup", handler);
addEventListener(content, "mouseup", handler);
addEventListener(button, "click", handler);
}
appendChild(actionbar, button);
});
if (settings.styleWithCSS) exec("styleWithCSS");
exec(defaultParagraphSeparatorString, defaultParagraphSeparator);
return settings.element;
};
return {exec, init};
})();

127
main.js
View file

@ -1,9 +1,9 @@
// Azgaar (azgaar.fmg@yandex.com). Minsk, 2017-2021. MIT License
// https://github.com/Azgaar/Fantasy-Map-Generator
'use strict';
const version = '1.63'; // generator version
document.title += ' v' + version;
"use strict";
const version = "1.652"; // generator version
document.title += " v" + version;
// Logging constants
const PRODUCTION = window.location.host;
@ -123,16 +123,30 @@ let customization = 0; // 0 - no; 1 = heightmap draw; 2 - states draw; 3 - add s
let biomesData = applyDefaultBiomesSystem();
let nameBases = Names.getNameBases(); // cultures-related data
const fonts = ['Almendra+SC', 'Georgia', 'Arial', 'Times+New+Roman', 'Comic+Sans+MS', 'Lucida+Sans+Unicode', 'Courier+New']; // default web-safe fonts
const fonts = ["Almendra+SC", "Georgia", "Arial", "Times+New+Roman", "Comic+Sans+MS", "Lucida+Sans+Unicode", "Courier+New", "Verdana", "Arial", "Impact"]; // default fonts
let color = d3.scaleSequential(d3.interpolateSpectral); // default color scheme
const lineGen = d3.line().curve(d3.curveBasis); // d3 line generator with default curve interpolation
// d3 zoom behavior
let scale = 1,
viewX = 0,
viewY = 0;
const zoom = d3.zoom().scaleExtent([1, 20]).on('zoom', zoomed);
let scale = 1;
let viewX = 0;
let viewY = 0;
const zoomThrottled = throttle(doWorkOnZoom, 100);
function zoomed() {
const {k, x, y} = d3.event.transform;
const isScaleChanged = Boolean(scale - k);
const isPositionChanged = Boolean(viewX - x || viewY - y);
scale = k;
viewX = x;
viewY = y;
zoomThrottled(isScaleChanged, isPositionChanged);
}
const zoom = d3.zoom().scaleExtent([1, 20]).on("zoom", zoomed);
// default options
let options = {pinNotes: false}; // options object
@ -399,26 +413,17 @@ function applyDefaultBiomesSystem() {
}
function showWelcomeMessage() {
const post = link('https://www.patreon.com/posts/48228540', 'Main changes:');
const changelog = link('https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog', 'previous version');
const reddit = link('https://www.reddit.com/r/FantasyMapGenerator', 'Reddit community');
const discord = link('https://discordapp.com/invite/X7E84HU', 'Discord server');
const patreon = link('https://www.patreon.com/azgaar', 'Patreon');
const changelog = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "previous version");
const reddit = link("https://www.reddit.com/r/FantasyMapGenerator", "Reddit community");
const discord = link("https://discordapp.com/invite/X7E84HU", "Discord server");
const patreon = link("https://www.patreon.com/azgaar", "Patreon");
alertMessage.innerHTML = `The Fantasy Map Generator is updated up to version <b>${version}</b>.
This version is compatible with ${changelog}, loaded <i>.map</i> files will be auto-updated.
<ul>${post}
<li>River overview and River editor rework</li>
<li>River generation code refactored and optimized</li>
<li>Rivers discharge (flux) and mouth width calculated</li>
<li>Lake editor rework</li>
<li>Lake type based on evaporation and river system</li>
<li>Lake flux, inlets and outlet tracked properly</li>
<li>Lake outlet width depends on flux</li>
<li>Lakes now have names</li>
<li>Rulers rework (v1.61)</li>
<li>New ocean pattern by Kiwiroo (v1.61)</li>
<li>Water erosion rework (v1.62)</li>
<ul>Main changes:
<li>Ability to add river selecting its cells</li>
<li>Keep river course on edit</li>
<li>Refactor river rendering code</li>
</ul>
<p>Join our ${discord} and ${reddit} to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.</p>
@ -438,31 +443,25 @@ function showWelcomeMessage() {
});
}
function zoomed() {
const transform = d3.event.transform;
const scaleDiff = scale - transform.k;
const positionDiff = (viewX - transform.x) | (viewY - transform.y);
if (!positionDiff && !scaleDiff) return;
function doWorkOnZoom(isScaleChanged, isPositionChanged) {
viewbox.attr("transform", `translate(${viewX} ${viewY}) scale(${scale})`);
scale = transform.k;
viewX = transform.x;
viewY = transform.y;
viewbox.attr('transform', transform);
if (isPositionChanged) drawCoordinates();
// update grid only if view position
if (positionDiff) drawCoordinates();
// rescale only if zoom is changed
if (scaleDiff) {
if (isScaleChanged) {
invokeActiveZooming();
drawScaleBar();
}
// zoom image converter overlay
const canvas = document.getElementById('canvas');
if (canvas && +canvas.style.opacity) {
const img = document.getElementById('image');
const ctx = canvas.getContext('2d');
if (customization === 1) {
const canvas = document.getElementById("canvas");
if (!canvas || canvas.style.opacity === "0") return;
const img = document.getElementById("imageToConvert");
if (!img) return;
const ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.setTransform(scale, 0, 0, scale, viewX, viewY);
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
@ -499,15 +498,16 @@ function invokeActiveZooming() {
}
// rescale lables on zoom
if (labels.style('display') !== 'none') {
labels.selectAll('g').each(function (d) {
if (this.id === 'burgLabels') return;
if (labels.style("display") !== "none") {
labels.selectAll("g").each(function () {
if (this.id === "burgLabels") return;
const desired = +this.dataset.size;
const relative = Math.max(rn((desired + desired / scale) / 2, 2), 1);
this.getAttribute('font-size', relative);
const hidden = hideLabels.checked && (relative * scale < 6 || relative * scale > 50);
if (hidden) this.classList.add('hidden');
else this.classList.remove('hidden');
if (rescaleLabels.checked) this.setAttribute("font-size", relative);
const hidden = hideLabels.checked && (relative * scale < 6 || relative * scale > 60);
if (hidden) this.classList.add("hidden");
else this.classList.remove("hidden");
});
}
@ -530,13 +530,14 @@ function invokeActiveZooming() {
// change states halo width
if (!customization) {
const haloSize = rn(statesHalo.attr('data-width') / scale, 1);
statesHalo.attr('stroke-width', haloSize).style('display', haloSize > 3 ? 'block' : 'none');
const desired = +statesHalo.attr("data-width");
const haloSize = rn(desired / scale ** 0.8, 2);
statesHalo.attr("stroke-width", haloSize).style("display", haloSize > 0.1 ? "block" : "none");
}
// rescale map markers
if (+markers.attr('rescale') && markers.style('display') !== 'none') {
markers.selectAll('use').each(function (d) {
if (+markers.attr("rescale") && markers.style("display") !== "none") {
markers.selectAll("use").each(function () {
const x = +this.dataset.x,
y = +this.dataset.y,
desired = +this.dataset.size;
@ -639,6 +640,7 @@ function generate() {
drawCoastline();
Rivers.generate();
drawRivers();
Lakes.defineGroup();
defineBiomes();
@ -1316,6 +1318,15 @@ function reMarkFeatures() {
cells.haven = cells.i.length < 65535 ? new Uint16Array(cells.i.length) : new Uint32Array(cells.i.length); // cell haven (opposite water cell);
cells.harbor = new Uint8Array(cells.i.length); // cell harbor (number of adjacent water cells);
const defineHaven = i => {
const water = cells.c[i].filter(c => cells.h[c] < 20);
const dist2 = water.map(c => (cells.p[i][0] - cells.p[c][0]) ** 2 + (cells.p[i][1] - cells.p[c][1]) ** 2);
const closest = water[dist2.indexOf(Math.min.apply(Math, dist2))];
cells.haven[i] = closest;
cells.harbor[i] = water.length;
};
for (let i = 1, queue = [0]; queue[0] !== -1; i++) {
const start = queue[0]; // first cell
cells.f[start] = i; // assign feature number
@ -1331,8 +1342,7 @@ function reMarkFeatures() {
if (land && !eLand) {
cells.t[q] = 1;
cells.t[e] = -1;
cells.harbor[q]++;
if (!cells.haven[q]) cells.haven[q] = e;
if (!cells.haven[q]) defineHaven(q);
} else if (land && eLand) {
if (!cells.t[e] && cells.t[q] === 1) cells.t[e] = 2;
else if (!cells.t[q] && cells.t[e] === 1) cells.t[q] = 2;
@ -2146,8 +2156,13 @@ const regenerateMap = debounce(function () {
generate();
restoreLayers();
if (ThreeD.options.isOn) ThreeD.redraw();
<<<<<<< HEAD
if ($('#worldConfigurator').is(':visible')) editWorld();
}, 500);
=======
if ($("#worldConfigurator").is(":visible")) editWorld();
}, 1000);
>>>>>>> 597f9ae038fbcc149315df9b1618e64744fb929d
// clear the map
function undraw() {

View file

@ -1,12 +1,9 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? (module.exports = factory()) : typeof define === 'function' && define.amd ? define(factory) : (global.BurgsAndStates = factory());
})(this, function () {
'use strict';
'use strict';
window.BurgsAndStates = (function () {
const generate = function () {
const cells = pack.cells,
cultures = pack.cultures,
n = cells.i.length;
const {cells, cultures} = pack;
const n = cells.i.length;
cells.burg = new Uint16Array(n); // cell burg
cells.road = new Uint16Array(n); // cell road power
@ -80,6 +77,7 @@
TIME && console.time('createStates');
const states = [{i: 0, name: 'Neutrals'}];
const colors = getColors(burgs.length - 1);
const each5th = each(5);
burgs.forEach(function (b, i) {
if (!i) return; // skip first element
@ -93,7 +91,7 @@
// states data
const expansionism = rn(Math.random() * powerInput.value + 1, 1);
const basename = b.name.length < 9 && b.cell % 5 === 0 ? b.name : Names.getCultureShort(b.culture);
const basename = b.name.length < 9 && each5th(b.cell) ? b.name : Names.getCultureShort(b.culture);
const name = Names.getState(basename, b.culture);
const type = cultures[b.culture].type;
@ -110,10 +108,10 @@
// place secondary settlements based on geo and economical evaluation
function placeTowns() {
TIME && console.time('placeTowns');
const score = new Int16Array(cells.s.map((s) => s * Math.random())); // a bit randomized cell score for towns placement
const sorted = cells.i.filter((i) => score[i] > 0 && cells.culture[i] && !cells.burg[i]).sort((a, b) => score[b] - score[a]); // filtered and sorted array of indexes
const score = new Int16Array(cells.s.map((s) => s * gauss(1, 3, 0, 20, 3))); // a bit randomized cell score for towns placement
const sorted = cells.i.filter((i) => !cells.burg[i] && score[i] > 0 && cells.culture[i]).sort((a, b) => score[b] - score[a]); // filtered and sorted array of indexes
const desiredNumber = manorsInput.value == 1000 ? rn(sorted.length / 6 / (grid.points.length / 10000) ** 0.8) : +manorsInput.value;
const desiredNumber = manorsInput.value == 1000 ? rn(sorted.length / 5 / (grid.points.length / 10000) ** 0.8) : manorsInput.valueAsNumber;
const burgsNumber = Math.min(desiredNumber, sorted.length); // towns to generate
let burgsAdded = 0;
@ -175,9 +173,9 @@
if (b.port) {
b.population = b.population * 1.3; // increase port population
const e = cells.v[i].filter((v) => vertices.c[v].some((c) => c === cells.haven[i])); // vertices of common edge
b.x = rn((vertices.p[e[0]][0] + vertices.p[e[1]][0]) / 2, 2);
b.y = rn((vertices.p[e[0]][1] + vertices.p[e[1]][1]) / 2, 2);
const [x, y] = getMiddlePoint(i, haven);
b.x = x;
b.y = y;
}
// add random factor
@ -477,9 +475,7 @@
// calculate and draw curved state labels for a list of states
const drawStateLabels = function (list) {
TIME && console.time('drawStateLabels');
const cells = pack.cells,
features = pack.features,
states = pack.states;
const {cells, features, states} = pack;
const paths = []; // text paths
lineGen.curve(d3.curveBundle.beta(1));
@ -572,8 +568,8 @@
}
void (function drawLabels() {
const g = labels.select('#states'),
t = defs.select('#textPaths');
const g = labels.select('#states');
const t = defs.select('#textPaths');
const displayed = layerIsOn('toggleLabels');
if (!displayed) toggleLabels();
@ -602,8 +598,8 @@
.attr('id', 'textPath_stateLabel' + id);
const pathLength = p[1].length > 1 ? textPath.node().getTotalLength() / letterLength : 0; // path length in letters
let lines = [],
ratio = 100;
let lines = [];
let ratio = 100;
if (pathLength < s.name.length) {
// only short name will fit
@ -622,10 +618,9 @@
// prolongate path if it's too short
if (pathLength && pathLength < lines[0].length) {
const points = p[1];
const f = points[0],
l = points[points.length - 1];
const dx = l[0] - f[0],
dy = l[1] - f[1];
const f = points[0];
const l = points[points.length - 1];
const [dx, dy] = [l[0] - f[0], l[1] - f[1]];
const mod = Math.abs((letterLength * lines[0].length) / dx) / 2;
points[0] = [rn(f[0] - dx * mod), rn(f[1] - dy * mod)];
points[points.length - 1] = [rn(l[0] + dx * mod), rn(l[1] + dy * mod)];
@ -653,8 +648,8 @@
if (lines.length < 2) return;
// check whether multilined label is generally inside the state. If no, replace with short name label
const cs = pack.cells.state,
b = el.parentNode.getBBox();
const cs = pack.cells.state;
const b = el.parentNode.getBBox();
const c1 = () => +cs[findCell(b.x, b.y)] === id;
const c2 = () => +cs[findCell(b.x + b.width / 2, b.y)] === id;
const c3 = () => +cs[findCell(b.x + b.width, b.y)] === id;
@ -950,30 +945,31 @@
});
const monarchy = ['Duchy', 'Grand Duchy', 'Principality', 'Kingdom', 'Empire']; // per expansionism tier
const republic = {Republic: 75, Federation: 4, Oligarchy: 2, Tetrarchy: 1, Triumvirate: 1, Diarchy: 1, 'Trade Company': 4, Junta: 1}; // weighted random
const republic = {Republic: 75, Federation: 4, Oligarchy: 2, 'Most Serene Republic': 2, Tetrarchy: 1, Triumvirate: 1, Diarchy: 1, 'Trade Company': 4, Junta: 1}; // weighted random
const union = {Union: 3, League: 4, Confederation: 1, 'United Kingdom': 1, 'United Republic': 1, 'United Provinces': 2, Commonwealth: 1, Heptarchy: 1}; // weighted random
const theocracy = {Theocracy: 20, Brotherhood: 1, Thearchy: 2, See: 1};
const theocracy = {Theocracy: 20, Brotherhood: 1, Thearchy: 2, See: 1, 'Holy State': 1};
const anarchy = {'Free Territory': 2, Council: 3, Commune: 1, Community: 1};
for (const s of states) {
if (list && !list.includes(s.i)) continue;
const tier = expTiers[s.i];
const religion = pack.cells.religion[s.center];
const isTheocracy = (religion && pack.religions[religion].expansion === 'state') || (P(0.1) && ['Organized', 'Cult'].includes(pack.religions[religion].type));
const isAnarchy = P(0.01 - expTiers[s.i] / 500);
const isAnarchy = P(0.01 - tier / 500);
if (isTheocracy) s.form = 'Theocracy';
else if (isAnarchy) s.form = 'Anarchy';
else s.form = s.type === 'Naval' ? rw(naval) : rw(generic);
s.formName = selectForm(s);
s.formName = selectForm(s, tier);
s.fullName = getFullName(s);
}
function selectForm(s) {
function selectForm(s, tier) {
const base = pack.cultures[s.culture].base;
if (s.form === 'Monarchy') {
const form = monarchy[expTiers[s.i]];
const form = monarchy[tier];
// Default name depends on exponent tier, some culture bases have special names for tiers
if (s.diplomacy) {
if (form === 'Duchy' && s.neighbors.length > 1 && rand(6) < s.neighbors.length && s.diplomacy.includes('Vassal')) return 'Marches'; // some vassal dutchies on borderland
@ -995,7 +991,7 @@
if (s.form === 'Republic') {
// Default name is from weighted array, special case for small states with only 1 burg
if (expTiers[s.i] < 2 && s.burgs === 1) {
if (tier < 2 && s.burgs === 1) {
if (trimVowels(s.name) === trimVowels(pack.burgs[s.capital].name)) {
s.name = pack.burgs[s.capital].name;
return 'Free City';
@ -1009,10 +1005,15 @@
if (s.form === 'Anarchy') return rw(anarchy);
if (s.form === 'Theocracy') {
if (P(0.5) && [0, 1, 2, 3, 4, 6, 8, 9, 13, 15, 20].includes(base)) return 'Diocese'; // Euporean
if (P(0.9) && [7, 5].includes(base)) return 'Eparchy'; // Greek, Ruthenian
// European
if ([0, 1, 2, 3, 4, 6, 8, 9, 13, 15, 20].includes(base)) {
if (P(0.1)) return 'Divine ' + monarchy[tier];
if (tier < 2 && P(0.5)) return 'Diocese';
if (tier < 2 && P(0.5)) return 'Bishopric';
}
if (tier < 2 && P(0.9) && [7, 5].includes(base)) return 'Eparchy'; // Greek, Ruthenian
if (P(0.9) && [21, 16].includes(base)) return 'Imamah'; // Nigerian, Turkish
if (P(0.8) && [18, 17, 28].includes(base)) return 'Caliphate'; // Arabic, Berber, Swahili
if (tier > 2 && P(0.8) && [18, 17, 28].includes(base)) return 'Caliphate'; // Arabic, Berber, Swahili
return rw(theocracy);
}
}
@ -1255,4 +1256,4 @@
generateProvinces,
updateCultures
};
});
})();

View file

@ -1,170 +1,364 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.COA = factory());
}(this, (function () {'use strict';
"use strict";
window.COA = (function () {
const tinctures = {
field: { metals: 3, colours: 4, stains: +P(.03), patterns: 1 },
division: { metals: 5, colours: 8, stains: +P(.03), patterns: 1 },
charge: { metals: 2, colours: 3, stains: +P(.05), patterns: 0 },
metals: { argent: 3, or: 2 },
colours: { gules: 5, azure: 4, sable: 3, purpure: 3, vert: 2 },
stains: { murrey: 1, sanguine: 1, tenné: 1 },
field: {metals: 3, colours: 4, stains: +P(0.03), patterns: 1},
division: {metals: 5, colours: 8, stains: +P(0.03), patterns: 1},
charge: {metals: 2, colours: 3, stains: +P(0.05), patterns: 0},
metals: {argent: 3, or: 2},
colours: {gules: 5, azure: 4, sable: 3, purpure: 3, vert: 2},
stains: {murrey: 1, sanguine: 1, tenné: 1},
patterns: {
semy: 8, ermine: 6,
vair: 4, counterVair: 1, vairInPale: 1, vairEnPointe: 2, vairAncien: 2,
potent: 2, counterPotent: 1, potentInPale: 1, potentEnPointe: 1,
chequy: 8, lozengy: 5, fusily: 2, pally: 8, barry: 10, gemelles: 1,
bendy: 8, bendySinister: 4, palyBendy: 2, barryBendy: 1,
pappellony: 2, pappellony2: 3, scaly: 1, plumetty: 1,
masoned: 6, fretty: 3, grillage: 1, chainy: 1, maily: 2, honeycombed: 1 }
}
semy: 8,
ermine: 6,
vair: 4,
counterVair: 1,
vairInPale: 1,
vairEnPointe: 2,
vairAncien: 2,
potent: 2,
counterPotent: 1,
potentInPale: 1,
potentEnPointe: 1,
chequy: 8,
lozengy: 5,
fusily: 2,
pally: 8,
barry: 10,
gemelles: 1,
bendy: 8,
bendySinister: 4,
palyBendy: 2,
barryBendy: 1,
pappellony: 2,
pappellony2: 3,
scaly: 1,
plumetty: 1,
masoned: 6,
fretty: 3,
grillage: 1,
chainy: 1,
maily: 2,
honeycombed: 1
}
};
const charges = {
// categories selection
types: { conventional: 30, crosses: 10, animals: 2, animalHeads: 1, birds: 2, fantastic: 3, plants: 1, agriculture: 1, arms: 3, bodyparts: 1, people: 1, architecture: 1, miscellaneous: 3, inescutcheon: 3 },
single: { conventional: 12, crosses: 8, plants: 2, animals: 10, animalHeads: 2, birds: 4, fantastic: 7, agriculture: 1, arms: 6, bodyparts: 1, people: 2, architecture: 1, miscellaneous: 10, inescutcheon: 5 },
semy: { conventional: 12, crosses: 3, plants: 1 },
types: {conventional: 30, crosses: 10, animals: 2, animalHeads: 1, birds: 2, fantastic: 3, plants: 1, agriculture: 1, arms: 3, bodyparts: 1, people: 1, architecture: 1, miscellaneous: 3, inescutcheon: 3},
single: {conventional: 12, crosses: 8, plants: 2, animals: 10, animalHeads: 2, birds: 4, fantastic: 7, agriculture: 1, arms: 6, bodyparts: 1, people: 2, architecture: 1, miscellaneous: 10, inescutcheon: 5},
semy: {conventional: 12, crosses: 3, plants: 1},
// generic categories
conventional: {
lozenge: 2, fusil: 4, mascle: 4, rustre: 2, lozengeFaceted: 3, lozengePloye: 1, roundel: 4, roundel2: 3, annulet: 4,
mullet: 5, mulletPierced: 1, mulletFaceted: 1, mullet4: 3, mullet6: 4, mullet6Pierced: 1, mullet6Faceted: 1, mullet7: 1, mullet8: 1, mullet10: 1,
estoile: 1, compassRose: 1, billet: 5, delf: 0, triangle: 3, trianglePierced: 1, goutte: 4, heart: 4, pique: 2, carreau: 1, trefle: 2,
fleurDeLis: 6, sun: 3, sunInSplendour: 1, crescent: 5, fountain: 1
lozenge: 2,
fusil: 4,
mascle: 4,
rustre: 2,
lozengeFaceted: 3,
lozengePloye: 1,
roundel: 4,
roundel2: 3,
annulet: 4,
mullet: 5,
mulletPierced: 1,
mulletFaceted: 1,
mullet4: 3,
mullet6: 4,
mullet6Pierced: 1,
mullet6Faceted: 1,
mullet7: 1,
mullet8: 1,
mullet10: 1,
estoile: 1,
compassRose: 1,
billet: 5,
delf: 0,
triangle: 3,
trianglePierced: 1,
goutte: 4,
heart: 4,
pique: 2,
carreau: 1,
trefle: 2,
fleurDeLis: 6,
sun: 3,
sunInSplendour: 1,
crescent: 5,
fountain: 1
},
crosses: {
crossHummetty: 15, crossVoided: 1, crossPattee: 2, crossPatteeAlisee: 1, crossFormee: 1, crossFormee2: 2, crossPotent: 2, crossJerusalem:1,
crosslet: 1, crossClechy: 3, crossBottony: 1, crossFleury: 3, crossPatonce: 1, crossPommy: 1, crossGamma: 1, crossArrowed: 1, crossFitchy: 1,
crossCercelee: 1, crossMoline: 2, crossFourchy: 1, crossAvellane: 1, crossErminee: 1, crossBiparted: 1, crossMaltese: 3, crossTemplar: 2,
crossCeltic: 1, crossCeltic2: 1, crossTriquetra: 1, crossCarolingian: 1, crossOccitan: 1, crossSaltire: 3, crossBurgundy: 1,
crossLatin: 3, crossPatriarchal: 1, crossOrthodox: 1, crossCalvary: 1, crossDouble: 1, crossTau: 1, crossSantiago: 1, crossAnkh: 1
crossHummetty: 15,
crossVoided: 1,
crossPattee: 2,
crossPatteeAlisee: 1,
crossFormee: 1,
crossFormee2: 2,
crossPotent: 2,
crossJerusalem: 1,
crosslet: 1,
crossClechy: 3,
crossBottony: 1,
crossFleury: 3,
crossPatonce: 1,
crossPommy: 1,
crossGamma: 1,
crossArrowed: 1,
crossFitchy: 1,
crossCercelee: 1,
crossMoline: 2,
crossFourchy: 1,
crossAvellane: 1,
crossErminee: 1,
crossBiparted: 1,
crossMaltese: 3,
crossTemplar: 2,
crossCeltic: 1,
crossCeltic2: 1,
crossTriquetra: 1,
crossCarolingian: 1,
crossOccitan: 1,
crossSaltire: 3,
crossBurgundy: 1,
crossLatin: 3,
crossPatriarchal: 1,
crossOrthodox: 1,
crossCalvary: 1,
crossDouble: 1,
crossTau: 1,
crossSantiago: 1,
crossAnkh: 1
},
animals: {
lionRampant: 5, lionPassant: 2, lionPassantGuardant: 1, wolfRampant: 1, wolfPassant: 1, wolfStatant: 1, greyhoundCourant: 1, boarRampant: 1,
horseRampant: 2, horseSalient: 1, bearRampant: 2, bearPassant: 1, bullPassant: 1, goat: 1, lamb: 1, elephant: 1, camel: 1
lionRampant: 5,
lionPassant: 2,
lionPassantGuardant: 1,
wolfRampant: 1,
wolfPassant: 1,
wolfStatant: 1,
greyhoundCourant: 1,
boarRampant: 1,
horseRampant: 2,
horseSalient: 1,
bearRampant: 2,
bearPassant: 1,
bullPassant: 1,
goat: 1,
lamb: 1,
elephant: 1,
camel: 1
},
animalHeads: { wolfHeadErased: 1, bullHeadCaboshed: 1, deerHeadCaboshed: 1, lionHeadCaboshed: 2 },
fantastic: { dragonPassant: 2, dragonRampant: 2, wyvern: 1, wyvernWithWingsDisplayed: 1, griffinPassant: 1, griffinRampant: 1, eagleTwoHeards: 2, unicornRampant: 1, pegasus: 1, serpent: 1 },
birds: { eagle: 9, raven: 1, cock: 3, parrot: 1, swan: 2, swanErased: 1, heron: 1, owl: 1 },
plants: { tree: 1, oak: 1, cinquefoil: 1, rose: 1 },
agriculture: { garb: 1, rake: 1 },
arms: { sword: 5, sabre: 1, sabresCrossed: 1, hatchet: 2, axe: 2, lochaberAxe: 1, mallet: 1, bowWithArrow: 2, bow: 1, arrow: 1, arrowsSheaf: 1, helmet: 2 },
bodyparts: { hand: 4, head: 1, headWreathed: 1 },
people: { cavalier: 3, monk: 1, angel: 2 },
architecture: { tower: 1, castle: 1 },
animalHeads: {wolfHeadErased: 1, bullHeadCaboshed: 1, deerHeadCaboshed: 1, lionHeadCaboshed: 2},
fantastic: {dragonPassant: 2, dragonRampant: 2, wyvern: 1, wyvernWithWingsDisplayed: 1, griffinPassant: 1, griffinRampant: 1, eagleTwoHeards: 2, unicornRampant: 1, pegasus: 1, serpent: 1},
birds: {eagle: 9, raven: 1, cock: 3, parrot: 1, swan: 2, swanErased: 1, heron: 1, owl: 1},
plants: {tree: 1, oak: 1, cinquefoil: 1, rose: 1},
agriculture: {garb: 1, rake: 1},
arms: {sword: 5, sabre: 1, sabresCrossed: 1, hatchet: 2, axe: 2, lochaberAxe: 1, mallet: 1, bowWithArrow: 2, bow: 1, arrow: 1, arrowsSheaf: 1, helmet: 2},
bodyparts: {hand: 4, head: 1, headWreathed: 1},
people: {cavalier: 3, monk: 1, angel: 2},
architecture: {tower: 1, castle: 1},
miscellaneous: {
crown: 3, orb: 1, chalice: 1, key: 1, buckle: 1, bugleHorn: 1, bugleHorn2: 1, bell: 2, pot: 1, bucket: 1, horseshoe: 3,
attire: 1, stagsAttires: 1, ramsHorn: 1, cowHorns: 2, wing: 1, wingSword: 1, lute: 1, harp: 1, wheel: 2, crosier: 1, fasces: 1, log: 1
crown: 3,
orb: 1,
chalice: 1,
key: 1,
buckle: 1,
bugleHorn: 1,
bugleHorn2: 1,
bell: 2,
pot: 1,
bucket: 1,
horseshoe: 3,
attire: 1,
stagsAttires: 1,
ramsHorn: 1,
cowHorns: 2,
wing: 1,
wingSword: 1,
lute: 1,
harp: 1,
wheel: 2,
crosier: 1,
fasces: 1,
log: 1
},
// selection based on culture type:
Naval: { anchor: 3, boat: 1, lymphad: 2, armillarySphere: 1, escallop: 1, dolphin: 1 },
Highland: { tower: 1, raven: 1, wolfHeadErased: 1, wolfPassant: 1, goat: 1, axe: 1 },
River: { tower: 1, garb: 1, rake: 1, boat: 1, pike: 2, bullHeadCaboshed: 1 },
Lake: { cancer: 2, escallop: 1, pike: 2, heron: 1, boat: 1, boat2: 2 },
Nomadic: { pot: 1, buckle: 1, wheel: 2, sabre: 2, sabresCrossed: 1, bow: 2, arrow: 1, horseRampant: 1, horseSalient: 1, crescent: 1, camel: 3 },
Hunting: { bugleHorn: 2, bugleHorn2: 1, stagsAttires: 2, attire: 2, hatchet: 1, bowWithArrow: 1, arrowsSheaf: 1, deerHeadCaboshed: 1, wolfStatant: 1, oak: 1 },
Naval: {anchor: 3, boat: 1, lymphad: 2, armillarySphere: 1, escallop: 1, dolphin: 1},
Highland: {tower: 1, raven: 1, wolfHeadErased: 1, wolfPassant: 1, goat: 1, axe: 1},
River: {tower: 1, garb: 1, rake: 1, boat: 1, pike: 2, bullHeadCaboshed: 1},
Lake: {cancer: 2, escallop: 1, pike: 2, heron: 1, boat: 1, boat2: 2},
Nomadic: {pot: 1, buckle: 1, wheel: 2, sabre: 2, sabresCrossed: 1, bow: 2, arrow: 1, horseRampant: 1, horseSalient: 1, crescent: 1, camel: 3},
Hunting: {bugleHorn: 2, bugleHorn2: 1, stagsAttires: 2, attire: 2, hatchet: 1, bowWithArrow: 1, arrowsSheaf: 1, deerHeadCaboshed: 1, wolfStatant: 1, oak: 1},
// selection based on type
City: { key: 3, bell: 2, lute: 1, tower: 1, castle: 1, mallet: 1 },
Capital: { crown: 4, orb: 1, lute: 1, castle: 3, tower: 1 },
Сathedra: { chalice: 1, orb: 1, crosier: 2, lamb: 1, monk: 2, angel: 3, crossLatin: 2, crossPatriarchal: 1, crossOrthodox: 1, crossCalvary: 1 },
City: {key: 3, bell: 2, lute: 1, tower: 1, castle: 1, mallet: 1},
Capital: {crown: 4, orb: 1, lute: 1, castle: 3, tower: 1},
Сathedra: {chalice: 1, orb: 1, crosier: 2, lamb: 1, monk: 2, angel: 3, crossLatin: 2, crossPatriarchal: 1, crossOrthodox: 1, crossCalvary: 1},
// specific cases
natural: { fountain: "azure", garb: "or", raven: "sable" }, // charges to mainly use predefined colours
sinister: [ // charges that can be sinister
"crossGamma", "lionRampant", "lionPassant", "wolfRampant", "wolfPassant", "wolfStatant", "wolfHeadErased", "greyhoundСourant", "boarRampant",
"horseRampant", "horseSalient", "bullPassant", "bearRampant", "bearPassant", "goat", "lamb", "elephant", "eagle", "raven", "cock", "parrot",
"swan", "swanErased", "heron", "pike", "dragonPassant", "dragonRampant", "wyvern", "wyvernWithWingsDisplayed", "griffinPassant", "griffinRampant",
"unicornRampant", "pegasus", "serpent", "hatchet", "lochaberAxe", "hand", "wing", "wingSword", "lute", "harp", "bow", "head", "headWreathed",
"knight", "lymphad", "log", "crosier", "dolphin", "sabre", "monk", "owl", "axe", "camel", "fasces", "lionPassantGuardant", "helmet"],
reversed: [ // charges that can be reversed
"goutte", "mullet", "mullet7", "crescent", "crossTau", "cancer", "sword", "sabresCrossed", "hand",
"horseshoe", "bowWithArrow", "arrow", "arrowsSheaf", "rake", "crossTriquetra", "crossLatin", "crossTau"
natural: {fountain: "azure", garb: "or", raven: "sable"}, // charges to mainly use predefined colours
sinister: [
// charges that can be sinister
"crossGamma",
"lionRampant",
"lionPassant",
"wolfRampant",
"wolfPassant",
"wolfStatant",
"wolfHeadErased",
"greyhoundСourant",
"boarRampant",
"horseRampant",
"horseSalient",
"bullPassant",
"bearRampant",
"bearPassant",
"goat",
"lamb",
"elephant",
"eagle",
"raven",
"cock",
"parrot",
"swan",
"swanErased",
"heron",
"pike",
"dragonPassant",
"dragonRampant",
"wyvern",
"wyvernWithWingsDisplayed",
"griffinPassant",
"griffinRampant",
"unicornRampant",
"pegasus",
"serpent",
"hatchet",
"lochaberAxe",
"hand",
"wing",
"wingSword",
"lute",
"harp",
"bow",
"head",
"headWreathed",
"knight",
"lymphad",
"log",
"crosier",
"dolphin",
"sabre",
"monk",
"owl",
"axe",
"camel",
"fasces",
"lionPassantGuardant",
"helmet"
],
reversed: [
// charges that can be reversed
"goutte",
"mullet",
"mullet7",
"crescent",
"crossTau",
"cancer",
"sword",
"sabresCrossed",
"hand",
"horseshoe",
"bowWithArrow",
"arrow",
"arrowsSheaf",
"rake",
"crossTriquetra",
"crossLatin",
"crossTau"
]
}
};
const positions = {
conventional: { e: 20, abcdefgzi: 3, beh: 3, behdf: 2, acegi: 1, kn: 3, bhdf: 1, jeo: 1, abc: 3, jln: 6, jlh: 3, kmo: 2, jleh: 1, def: 3, abcpqh: 4, ABCDEFGHIJKL: 1 },
complex: { e: 40, beh: 1, kn: 1, jeo: 1, abc: 2, jln: 7, jlh: 2, def: 1, abcpqh: 1 },
conventional: {e: 20, abcdefgzi: 3, beh: 3, behdf: 2, acegi: 1, kn: 3, bhdf: 1, jeo: 1, abc: 3, jln: 6, jlh: 3, kmo: 2, jleh: 1, def: 3, abcpqh: 4, ABCDEFGHIJKL: 1},
complex: {e: 40, beh: 1, kn: 1, jeo: 1, abc: 2, jln: 7, jlh: 2, def: 1, abcpqh: 1},
divisions: {
perPale: { e: 15, pq: 5, jo: 2, jl: 2, ABCDEFGHIJKL: 1 },
perFess: { e: 12, kn: 4, jkl: 2, gizgiz: 1, jlh: 3, kmo: 1, ABCDEFGHIJKL: 1 },
perBend: { e: 5, lm: 5, bcfdgh: 1 },
perBendSinister: { e: 1, jo: 1 },
perCross: { e: 4, jlmo: 1, j: 1, jo: 2, jl: 1 },
perChevron: { e: 1, jlh: 1, dfk: 1, dfbh: 2, bdefh: 1 },
perChevronReversed: { e: 1, mok: 2, dfh: 2, dfbh: 1, bdefh: 1 },
perSaltire: { bhdf: 8, e: 3, abcdefgzi: 1, bh: 1, df: 1, ABCDEFGHIJKL: 1 },
perPile: { ee: 3, be: 2, abceh: 1, abcabc: 1, jleh: 1 }
perPale: {e: 15, pq: 5, jo: 2, jl: 2, ABCDEFGHIJKL: 1},
perFess: {e: 12, kn: 4, jkl: 2, gizgiz: 1, jlh: 3, kmo: 1, ABCDEFGHIJKL: 1},
perBend: {e: 5, lm: 5, bcfdgh: 1},
perBendSinister: {e: 1, jo: 1},
perCross: {e: 4, jlmo: 1, j: 1, jo: 2, jl: 1},
perChevron: {e: 1, jlh: 1, dfk: 1, dfbh: 2, bdefh: 1},
perChevronReversed: {e: 1, mok: 2, dfh: 2, dfbh: 1, bdefh: 1},
perSaltire: {bhdf: 8, e: 3, abcdefgzi: 1, bh: 1, df: 1, ABCDEFGHIJKL: 1},
perPile: {ee: 3, be: 2, abceh: 1, abcabc: 1, jleh: 1}
},
ordinariesOn: {
pale: { ee: 12, beh: 10, kn: 3, bb: 1 },
fess: { ee: 1, def: 3 },
bar: { defdefdef: 1 },
fessCotissed: { ee: 1, def: 3 },
fessDoubleCotissed: { ee: 1, defdef: 3 },
bend: { ee: 2, jo: 1, joe: 1 },
bendSinister: { ee: 1, lm: 1, lem: 4 },
bendlet: { joejoejoe: 1 },
bendletSinister: { lemlemlem: 1 },
bordure: { ABCDEFGHIJKL: 1 },
chief: { abc: 5, bbb: 1 },
quarter: { jjj: 1 },
canton: { yyyy: 1 },
cross: { eeee: 1, behdfbehdf: 3, behbehbeh: 2 },
crossParted: { e: 5, ee: 1 },
saltire: { ee: 5, jlemo: 1 },
saltireParted: { e: 5, ee: 1 },
pall: { ee: 1, jleh: 5, jlhh: 3 },
pallReversed: { ee: 1, bemo: 5 },
pile: { bbb: 1 },
pileInBend: { eeee: 1, eeoo: 1 },
pileInBendSinister: { eeee: 1, eemm: 1 }
pale: {ee: 12, beh: 10, kn: 3, bb: 1},
fess: {ee: 1, def: 3},
bar: {defdefdef: 1},
fessCotissed: {ee: 1, def: 3},
fessDoubleCotissed: {ee: 1, defdef: 3},
bend: {ee: 2, jo: 1, joe: 1},
bendSinister: {ee: 1, lm: 1, lem: 4},
bendlet: {joejoejoe: 1},
bendletSinister: {lemlemlem: 1},
bordure: {ABCDEFGHIJKL: 1},
chief: {abc: 5, bbb: 1},
quarter: {jjj: 1},
canton: {yyyy: 1},
cross: {eeee: 1, behdfbehdf: 3, behbehbeh: 2},
crossParted: {e: 5, ee: 1},
saltire: {ee: 5, jlemo: 1},
saltireParted: {e: 5, ee: 1},
pall: {ee: 1, jleh: 5, jlhh: 3},
pallReversed: {ee: 1, bemo: 5},
pile: {bbb: 1},
pileInBend: {eeee: 1, eeoo: 1},
pileInBendSinister: {eeee: 1, eemm: 1}
},
ordinariesOff: {
pale: { yyy: 1 },
fess: { abc: 3, abcz: 1 },
bar: { abc: 2, abcgzi: 1, jlh: 5, bgi: 2, ach: 1 },
gemelle: { abc: 1 },
bend: { ccg: 2, ccc: 1 },
bendSinister: { aai: 2, aaa: 1 },
bendlet: { ccg: 2, ccc: 1 },
bendletSinister: { aai: 2, aaa: 1 },
bordure: { e: 4, jleh:2, kenken: 1, peqpeq: 1 },
orle: { e: 4, jleh: 1, kenken: 1, peqpeq: 1 },
chief: { emo: 2, emoz: 1, ez: 2 },
terrace: { e: 5, def: 1, bdf: 3 },
mount: { e: 5, def: 1, bdf: 3 },
point: { e: 2, def: 1, bdf: 3, acbdef: 1 },
flaunches: { e: 3, kn: 1, beh: 3 },
gyron: { bh: 1 },
quarter: { e: 1 },
canton: { e: 5, beh: 1, def: 1, bdefh: 1, kn: 1 },
cross: { acgi: 1 },
pall: { BCKFEILGJbdmfo: 1 },
pallReversed: { aczac: 1 },
chevron: { ach: 3, hhh: 1 },
chevronReversed: { bbb: 1 },
pile: { acdfgi: 1, acac: 1 },
pileInBend: { cg: 1 },
pileInBendSinister: { ai: 1 },
label: { defgzi: 2, eh: 3, defdefhmo: 1, egiegi: 1, pqn: 5 }
pale: {yyy: 1},
fess: {abc: 3, abcz: 1},
bar: {abc: 2, abcgzi: 1, jlh: 5, bgi: 2, ach: 1},
gemelle: {abc: 1},
bend: {ccg: 2, ccc: 1},
bendSinister: {aai: 2, aaa: 1},
bendlet: {ccg: 2, ccc: 1},
bendletSinister: {aai: 2, aaa: 1},
bordure: {e: 4, jleh: 2, kenken: 1, peqpeq: 1},
orle: {e: 4, jleh: 1, kenken: 1, peqpeq: 1},
chief: {emo: 2, emoz: 1, ez: 2},
terrace: {e: 5, def: 1, bdf: 3},
mount: {e: 5, def: 1, bdf: 3},
point: {e: 2, def: 1, bdf: 3, acbdef: 1},
flaunches: {e: 3, kn: 1, beh: 3},
gyron: {bh: 1},
quarter: {e: 1},
canton: {e: 5, beh: 1, def: 1, bdefh: 1, kn: 1},
cross: {acgi: 1},
pall: {BCKFEILGJbdmfo: 1},
pallReversed: {aczac: 1},
chevron: {ach: 3, hhh: 1},
chevronReversed: {bbb: 1},
pile: {acdfgi: 1, acac: 1},
pileInBend: {cg: 1},
pileInBendSinister: {ai: 1},
label: {defgzi: 2, eh: 3, defdefhmo: 1, egiegi: 1, pqn: 5}
},
// charges
inescutcheon: { e: 4, jln: 1 },
mascle: { e: 15, abcdefgzi: 3, beh: 3, bdefh: 4, acegi: 1, kn: 3, joe: 2, abc: 3, jlh: 8, jleh: 1, df: 3, abcpqh: 4, pqe: 3, eknpq: 3 },
lionRampant: { e: 10, def: 2, abc: 2, bdefh: 1, kn: 1, jlh: 2, abcpqh: 1 },
lionPassant: { e: 10, def: 1, abc: 1, bdefh: 1, jlh: 1, abcpqh: 1 },
wolfPassant: { e: 10, def: 1, abc: 1, bdefh: 1, jlh: 1, abcpqh: 1 },
greyhoundСourant: { e: 10, def: 1, abc: 1, bdefh: 1, jlh: 1, abcpqh: 1 },
griffinRampant: { e: 10, def: 2, abc: 2, bdefh: 1, kn: 1, jlh: 2, abcpqh: 1 },
griffinPassant: { e: 10, def: 1, abc: 1, bdefh: 1, jlh: 1, abcpqh: 1 },
boarRampant: { e: 12, beh: 1, kn: 1, jln: 2 },
eagle: { e: 15, beh: 1, kn: 1, abc: 1, jlh: 2, def: 2, pq: 1 },
raven: { e: 15, beh: 1, kn: 1, jeo: 1, abc: 3, jln: 3, def: 1 },
wyvern: { e: 10, jln: 1 },
garb: { e: 1, def: 3, abc: 2, beh: 1, kn: 1, jln: 3, jleh: 1, abcpqh: 1, joe: 1, lme: 1 },
crown: { e: 10, abcdefgzi: 1, beh: 3, behdf: 2, acegi: 1, kn: 1, pq: 2, abc: 1, jln: 4, jleh: 1, def: 2, abcpqh: 3 },
hand: { e: 10, jln: 2, kn: 1, jeo: 1, abc: 2, pqe: 1 },
inescutcheon: {e: 4, jln: 1},
mascle: {e: 15, abcdefgzi: 3, beh: 3, bdefh: 4, acegi: 1, kn: 3, joe: 2, abc: 3, jlh: 8, jleh: 1, df: 3, abcpqh: 4, pqe: 3, eknpq: 3},
lionRampant: {e: 10, def: 2, abc: 2, bdefh: 1, kn: 1, jlh: 2, abcpqh: 1},
lionPassant: {e: 10, def: 1, abc: 1, bdefh: 1, jlh: 1, abcpqh: 1},
wolfPassant: {e: 10, def: 1, abc: 1, bdefh: 1, jlh: 1, abcpqh: 1},
greyhoundСourant: {e: 10, def: 1, abc: 1, bdefh: 1, jlh: 1, abcpqh: 1},
griffinRampant: {e: 10, def: 2, abc: 2, bdefh: 1, kn: 1, jlh: 2, abcpqh: 1},
griffinPassant: {e: 10, def: 1, abc: 1, bdefh: 1, jlh: 1, abcpqh: 1},
boarRampant: {e: 12, beh: 1, kn: 1, jln: 2},
eagle: {e: 15, beh: 1, kn: 1, abc: 1, jlh: 2, def: 2, pq: 1},
raven: {e: 15, beh: 1, kn: 1, jeo: 1, abc: 3, jln: 3, def: 1},
wyvern: {e: 10, jln: 1},
garb: {e: 1, def: 3, abc: 2, beh: 1, kn: 1, jln: 3, jleh: 1, abcpqh: 1, joe: 1, lme: 1},
crown: {e: 10, abcdefgzi: 1, beh: 3, behdf: 2, acegi: 1, kn: 1, pq: 2, abc: 1, jln: 4, jleh: 1, def: 2, abcpqh: 3},
hand: {e: 10, jln: 2, kn: 1, jeo: 1, abc: 2, pqe: 1},
armillarySphere: {e: 1},
tree: {e: 1},
lymphad: {e: 1},
@ -175,33 +369,92 @@
};
const lines = {
straight: 50, wavy: 8, engrailed: 4, invecked: 3, rayonne: 3, embattled: 1, raguly: 1, urdy: 1, dancetty: 1, indented: 2,
dentilly: 1, bevilled: 1, angled: 1, flechy: 1, barby: 1, enclavy: 1, escartely: 1, arched: 2, archedReversed: 1, nowy: 1, nowyReversed: 1,
embattledGhibellin: 1, embattledNotched: 1, embattledGrady: 1, dovetailedIndented: 1, dovetailed: 1,
potenty: 1, potentyDexter: 1, potentySinister: 1, nebuly: 2, seaWaves: 1, dragonTeeth: 1, firTrees: 1
straight: 50,
wavy: 8,
engrailed: 4,
invecked: 3,
rayonne: 3,
embattled: 1,
raguly: 1,
urdy: 1,
dancetty: 1,
indented: 2,
dentilly: 1,
bevilled: 1,
angled: 1,
flechy: 1,
barby: 1,
enclavy: 1,
escartely: 1,
arched: 2,
archedReversed: 1,
nowy: 1,
nowyReversed: 1,
embattledGhibellin: 1,
embattledNotched: 1,
embattledGrady: 1,
dovetailedIndented: 1,
dovetailed: 1,
potenty: 1,
potentyDexter: 1,
potentySinister: 1,
nebuly: 2,
seaWaves: 1,
dragonTeeth: 1,
firTrees: 1
};
const divisions = {
variants: { perPale: 5, perFess: 5, perBend: 2, perBendSinister: 1, perChevron: 1, perChevronReversed: 1, perCross: 5, perPile: 1, perSaltire: 1, gyronny: 1, chevronny: 1 },
variants: {perPale: 5, perFess: 5, perBend: 2, perBendSinister: 1, perChevron: 1, perChevronReversed: 1, perCross: 5, perPile: 1, perSaltire: 1, gyronny: 1, chevronny: 1},
perPale: lines,
perFess: lines,
perBend: lines,
perBendSinister: lines,
perChevron: lines,
perChevronReversed: lines,
perCross: { straight: 20, wavy: 5, engrailed: 4, invecked: 3, rayonne: 1, embattled: 1, raguly: 1, urdy: 1, indented: 2, dentilly: 1, bevilled: 1, angled: 1, embattledGhibellin: 1, embattledGrady: 1, dovetailedIndented: 1, dovetailed: 1, potenty: 1, potentyDexter: 1, potentySinister: 1, nebuly: 1 },
perCross: {straight: 20, wavy: 5, engrailed: 4, invecked: 3, rayonne: 1, embattled: 1, raguly: 1, urdy: 1, indented: 2, dentilly: 1, bevilled: 1, angled: 1, embattledGhibellin: 1, embattledGrady: 1, dovetailedIndented: 1, dovetailed: 1, potenty: 1, potentyDexter: 1, potentySinister: 1, nebuly: 1},
perPile: lines
};
const ordinaries = {
lined: {
pale: 7, fess: 5, bend: 3, bendSinister: 2, chief: 5, bar: 2, gemelle: 1, fessCotissed: 1, fessDoubleCotissed: 1,
bendlet: 2, bendletSinister: 1, terrace: 3, cross: 6, crossParted: 1, saltire: 2, saltireParted: 1
pale: 7,
fess: 5,
bend: 3,
bendSinister: 2,
chief: 5,
bar: 2,
gemelle: 1,
fessCotissed: 1,
fessDoubleCotissed: 1,
bendlet: 2,
bendletSinister: 1,
terrace: 3,
cross: 6,
crossParted: 1,
saltire: 2,
saltireParted: 1
},
straight: {
bordure: 8, orle: 4, mount: 1, point: 2, flaunches: 1, gore: 1,
gyron: 1, quarter: 1, canton: 2, pall: 3, pallReversed: 2, chevron: 4, chevronReversed: 3,
pile: 2, pileInBend: 2, pileInBendSinister: 1, piles: 1, pilesInPoint: 2, label: 1
bordure: 8,
orle: 4,
mount: 1,
point: 2,
flaunches: 1,
gore: 1,
gyron: 1,
quarter: 1,
canton: 2,
pall: 3,
pallReversed: 2,
chevron: 4,
chevronReversed: 3,
pile: 2,
pileInBend: 2,
pileInBendSinister: 1,
piles: 1,
pilesInPoint: 2,
label: 1
}
};
@ -215,60 +468,61 @@
simple: {round: 12, oval: 6, vesicaPiscis: 1, square: 1, diamond: 2, no: 0},
fantasy: {fantasy1: 2, fantasy2: 2, fantasy3: 1, fantasy4: 1, fantasy5: 3},
middleEarth: {noldor: 1, gondor: 1, easterling: 1, erebor: 1, ironHills: 1, urukHai: 1, moriaOrc: 1}
}
};
const generate = function(parent, kinship, dominion, type) {
const generate = function (parent, kinship, dominion, type) {
if (!parent || parent === "custom") {
parent = null;
kinship = 0;
dominion = 0;
}
let usedPattern = null, usedTinctures = [];
let usedPattern = null,
usedTinctures = [];
const t1 = P(kinship) ? parent.t1 : getTincture("field");
if (t1.includes("-")) usedPattern = t1;
const coa = {t1};
let charge = P(usedPattern ? .5 : .93) ? true : false; // 80% for charge
const linedOrdinary = charge && P(.3) || P(.5) ? parent?.ordinaries && P(kinship) ? parent.ordinaries[0].ordinary : rw(ordinaries.lined) : null;
const ordinary = !charge && P(.65) || P(.3) ? linedOrdinary ? linedOrdinary : rw(ordinaries.straight) : null; // 36% for ordinary
let charge = P(usedPattern ? 0.5 : 0.93) ? true : false; // 80% for charge
const linedOrdinary = (charge && P(0.3)) || P(0.5) ? (parent?.ordinaries && P(kinship) ? parent.ordinaries[0].ordinary : rw(ordinaries.lined)) : null;
const ordinary = (!charge && P(0.65)) || P(0.3) ? (linedOrdinary ? linedOrdinary : rw(ordinaries.straight)) : null; // 36% for ordinary
const rareDivided = ["chief", "terrace", "chevron", "quarter", "flaunches"].includes(ordinary);
const divisioned = rareDivided ? P(.03) : charge && ordinary ? P(.03) : charge ? P(.3) : ordinary ? P(.7) : P(.995); // 33% for division
const division = divisioned ? parent?.division && P(kinship - .1) ? parent.division.division : rw(divisions.variants) : null;
if (charge) charge =
parent?.charges && P(kinship - .1) ? parent.charges[0].charge :
type && type !== "Generic" && P(.2) ? rw(charges[type]) :
selectCharge();
const divisioned = rareDivided ? P(0.03) : charge && ordinary ? P(0.03) : charge ? P(0.3) : ordinary ? P(0.7) : P(0.995); // 33% for division
const division = divisioned ? (parent?.division && P(kinship - 0.1) ? parent.division.division : rw(divisions.variants)) : null;
if (charge) charge = parent?.charges && P(kinship - 0.1) ? parent.charges[0].charge : type && type !== "Generic" && P(0.2) ? rw(charges[type]) : selectCharge();
if (division) {
const t = getTincture("division", usedTinctures, P(.98) ? coa.t1 : null);
const t = getTincture("division", usedTinctures, P(0.98) ? coa.t1 : null);
coa.division = {division, t};
if (divisions[division]) coa.division.line = usedPattern || (ordinary && P(.7)) ? "straight" : rw(divisions[division]);
if (divisions[division]) coa.division.line = usedPattern || (ordinary && P(0.7)) ? "straight" : rw(divisions[division]);
}
if (ordinary) {
coa.ordinaries = [{ordinary, t: getTincture("charge", usedTinctures, coa.t1)}];
if (linedOrdinary) coa.ordinaries[0].line = usedPattern || (division && P(.7)) ? "straight" : rw(lines);
if (division && !charge && !usedPattern && P(.5) && ordinary !== "bordure" && ordinary !== "orle") {
if (P(.8)) coa.ordinaries[0].divided = "counter"; // 40%
else if (P(.6)) coa.ordinaries[0].divided = "field"; // 6%
if (linedOrdinary) coa.ordinaries[0].line = usedPattern || (division && P(0.7)) ? "straight" : rw(lines);
if (division && !charge && !usedPattern && P(0.5) && ordinary !== "bordure" && ordinary !== "orle") {
if (P(0.8)) coa.ordinaries[0].divided = "counter";
// 40%
else if (P(0.6)) coa.ordinaries[0].divided = "field";
// 6%
else coa.ordinaries[0].divided = "division"; // 4%
}
}
if (charge) {
let p = "e", t = "gules";
let p = "e",
t = "gules";
const ordinaryT = coa.ordinaries ? coa.ordinaries[0].t : null;
if (positions.ordinariesOn[ordinary] && P(.8)) {
if (positions.ordinariesOn[ordinary] && P(0.8)) {
// place charge over ordinary (use tincture of field type)
p = rw(positions.ordinariesOn[ordinary]);
while (charges.natural[charge] === ordinaryT) charge = selectCharge();
t = !usedPattern && P(.3) ? coa.t1 : getTincture("charge", [], ordinaryT);
} else if (positions.ordinariesOff[ordinary] && P(.95)) {
t = !usedPattern && P(0.3) ? coa.t1 : getTincture("charge", [], ordinaryT);
} else if (positions.ordinariesOff[ordinary] && P(0.95)) {
// place charge out of ordinary (use tincture of ordinary type)
p = rw(positions.ordinariesOff[ordinary]);
while (charges.natural[charge] === coa.t1) charge = selectCharge();
t = !usedPattern && P(.3) ? ordinaryT : getTincture("charge", usedTinctures, coa.t1);
t = !usedPattern && P(0.3) ? ordinaryT : getTincture("charge", usedTinctures, coa.t1);
} else if (positions.divisions[division]) {
// place charge in fields made by division
p = rw(positions.divisions[division]);
@ -289,43 +543,41 @@
if (charges.natural[charge]) t = charges.natural[charge]; // natural tincture
coa.charges = [{charge, t, p}];
if (p === "ABCDEFGHIKL" && P(.95)) {
if (p === "ABCDEFGHIKL" && P(0.95)) {
// add central charge if charge is in bordure
coa.charges[0].charge = rw(charges.conventional);
const charge = selectCharge(charges.single);
const t = getTincture("charge", usedTinctures, coa.t1);
coa.charges.push({charge, t, p: "e"});
} else if (P(.8) && charge === "inescutcheon") {
} else if (P(0.8) && charge === "inescutcheon") {
// add charge to inescutcheon
const charge = selectCharge(charges.types);
const t2 = getTincture("charge", [], t);
coa.charges.push({charge, t: t2, p, size:.5});
coa.charges.push({charge, t: t2, p, size: 0.5});
} else if (division && !ordinary) {
const allowCounter = !usedPattern && (!coa.line || coa.line === "straight");
// dimidiation: second charge at division basic positons
if (P(.3) && ["perPale", "perFess"].includes(division) && coa.line === "straight") {
if (P(0.3) && ["perPale", "perFess"].includes(division) && coa.line === "straight") {
coa.charges[0].divided = "field";
if (P(.95)) {
const p2 = p === "e" || P(.5) ? "e" : rw(positions.divisions[division]);
if (P(0.95)) {
const p2 = p === "e" || P(0.5) ? "e" : rw(positions.divisions[division]);
const charge = selectCharge(charges.single);
const t = getTincture("charge", usedTinctures, coa.division.t);
coa.charges.push({charge, t, p: p2, divided: "division"});
}
}
else if (allowCounter && P(.4)) coa.charges[0].divided = "counter"; // counterchanged, 40%
else if (["perPale", "perFess", "perBend", "perBendSinister"].includes(division) && P(.8)) { // place 2 charges in division standard positions
const [p1, p2] = division === "perPale" ? ["p", "q"] :
division === "perFess" ? ["k", "n"] :
division === "perBend" ? ["l", "m"] :
["j", "o"]; // perBendSinister
} else if (allowCounter && P(0.4)) coa.charges[0].divided = "counter";
// counterchanged, 40%
else if (["perPale", "perFess", "perBend", "perBendSinister"].includes(division) && P(0.8)) {
// place 2 charges in division standard positions
const [p1, p2] = division === "perPale" ? ["p", "q"] : division === "perFess" ? ["k", "n"] : division === "perBend" ? ["l", "m"] : ["j", "o"]; // perBendSinister
coa.charges[0].p = p1;
const charge = selectCharge(charges.single);
const t = getTincture("charge", usedTinctures, coa.division.t);
coa.charges.push({charge, t, p: p2});
}
else if (["perCross", "perSaltire"].includes(division) && P(.5)) { // place 4 charges in division standard positions
} else if (["perCross", "perSaltire"].includes(division) && P(0.5)) {
// place 4 charges in division standard positions
const [p1, p2, p3, p4] = division === "perCross" ? ["j", "l", "m", "o"] : ["b", "d", "f", "h"];
coa.charges[0].p = p1;
@ -338,8 +590,7 @@
const c4 = selectCharge(charges.single);
const t4 = getTincture("charge", [], coa.t1);
coa.charges.push({charge: c2, t: t2, p: p2}, {charge: c3, t: t3, p: p3}, {charge: c4, t: t4, p: p4});
}
else if (allowCounter && p.length > 1) coa.charges[0].divided = "counter"; // counterchanged, 40%
} else if (allowCounter && p.length > 1) coa.charges[0].divided = "counter"; // counterchanged, 40%
}
coa.charges.forEach(c => defineChargeAttributes(c));
@ -351,8 +602,8 @@
c.p = [...new Set(c.p)].join("");
// define orientation
if (P(.02) && charges.sinister.includes(c.charge)) c.sinister = 1;
if (P(.02) && charges.reversed.includes(c.charge)) c.reversed = 1;
if (P(0.02) && charges.sinister.includes(c.charge)) c.sinister = 1;
if (P(0.02) && charges.reversed.includes(c.charge)) c.reversed = 1;
}
}
@ -380,24 +631,26 @@
if (!coa.charges) coa.charges = [];
coa.charges.push({charge, t: t2, p: "y", size: 0.5});
coa.ordinaries ? coa.ordinaries.push(canton) : coa.ordinaries = [canton];
coa.ordinaries ? coa.ordinaries.push(canton) : (coa.ordinaries = [canton]);
}
function selectCharge(set) {
const type = set ? rw(set) : ordinary || divisioned ? rw(charges.types): rw(charges.single);
const type = set ? rw(set) : ordinary || divisioned ? rw(charges.types) : rw(charges.single);
return type === "inescutcheon" ? "inescutcheon" : rw(charges[type]);
}
// select tincture: element type (field, division, charge), used field tinctures, field type to follow RoT
function getTincture(element, fields = [], RoT) {
const base = RoT ? RoT.includes("-") ? RoT.split("-")[1] : RoT : null;
const base = RoT ? (RoT.includes("-") ? RoT.split("-")[1] : RoT) : null;
let type = rw(tinctures[element]); // metals, colours, stains, patterns
if (RoT && type !== "patterns") type = getType(base) === "metals" ? "colours" : "metals"; // follow RoT
if (type === "metals" && fields.includes("or") && fields.includes("argent")) type = "colours"; // exclude metals overuse
let tincture = rw(tinctures[type]);
while (tincture === base || fields.includes(tincture)) {tincture = rw(tinctures[type]);} // follow RoT
while (tincture === base || fields.includes(tincture)) {
tincture = rw(tinctures[type]);
} // follow RoT
if (type !== "patterns" && element !== "charge") usedTinctures.push(tincture); // add field tincture
@ -425,39 +678,60 @@
if (Object.keys(tinctures.stains).includes(tincture)) return "stains";
else return "pattern";
}
}
function definePattern(pattern, element, size = "") {
let t1 = null, t2 = null;
if (P(.1)) size = "-small";
else if (P(.1)) size = "-smaller";
else if (P(.01)) size = "-big";
else if (P(.005)) size = "-smallest";
let t1 = null,
t2 = null;
if (P(0.1)) size = "-small";
else if (P(0.1)) size = "-smaller";
else if (P(0.01)) size = "-big";
else if (P(0.005)) size = "-smallest";
// apply standard tinctures
if (P(.5) && ["vair", "vairInPale", "vairEnPointe"].includes(pattern)) {t1 = "azure"; t2 = "argent";}
else if (P(.8) && pattern === "ermine") {t1 = "argent"; t2 = "sable";}
else if (pattern === "pappellony") {
if (P(.2)) {t1 = "gules"; t2 = "or";}
else if (P(.2)) {t1 = "argent"; t2 = "sable";}
else if (P(.2)) {t1 = "azure"; t2 = "argent";}
}
else if (pattern === "masoned") {
if (P(.3)) {t1 = "gules"; t2 = "argent";}
else if (P(.3)) {t1 = "argent"; t2 = "sable";}
else if (P(.1)) {t1 = "or"; t2 = "sable";}
}
else if (pattern === "fretty") {
if (t2 === "sable" || P(.35)) {t1 = "argent"; t2 = "gules";}
else if (P(.25)) {t1 = "sable"; t2 = "or";}
else if (P(.15)) {t1 = "gules"; t2 = "argent";}
}
else if (pattern === "semy") pattern += "_of_" + selectCharge(charges.semy);
if (P(0.5) && ["vair", "vairInPale", "vairEnPointe"].includes(pattern)) {
t1 = "azure";
t2 = "argent";
} else if (P(0.8) && pattern === "ermine") {
t1 = "argent";
t2 = "sable";
} else if (pattern === "pappellony") {
if (P(0.2)) {
t1 = "gules";
t2 = "or";
} else if (P(0.2)) {
t1 = "argent";
t2 = "sable";
} else if (P(0.2)) {
t1 = "azure";
t2 = "argent";
}
} else if (pattern === "masoned") {
if (P(0.3)) {
t1 = "gules";
t2 = "argent";
} else if (P(0.3)) {
t1 = "argent";
t2 = "sable";
} else if (P(0.1)) {
t1 = "or";
t2 = "sable";
}
} else if (pattern === "fretty") {
if (t2 === "sable" || P(0.35)) {
t1 = "argent";
t2 = "gules";
} else if (P(0.25)) {
t1 = "sable";
t2 = "or";
} else if (P(0.15)) {
t1 = "gules";
t2 = "argent";
}
} else if (pattern === "semy") pattern += "_of_" + selectCharge(charges.semy);
if (!t1 || !t2) {
const startWithMetal = P(.7);
const startWithMetal = P(0.7);
t1 = startWithMetal ? rw(tinctures.metals) : rw(tinctures.colours);
t2 = startWithMetal ? rw(tinctures.colours) : rw(tinctures.metals);
}
@ -474,28 +748,30 @@
function replaceTincture(t, n) {
const type = getType(t);
while (!n || n === t) {n = rw(tinctures[type]);}
while (!n || n === t) {
n = rw(tinctures[type]);
}
return n;
}
function getSize(p, o = null, d = null) {
if (p === "e" && (o === "bordure" || o === "orle")) return 1.1;
if (p === "e") return 1.5;
if (p === "jln" || p === "jlh") return .7;
if (p === "abcpqh" || p === "ez" || p === "be") return .5;
if (["a", "b", "c", "d", "f", "g", "h", "i", "bh", "df"].includes(p)) return .5;
if (["j", "l", "m", "o", "jlmo"].includes(p) && d === "perCross") return .6;
if (p.length > 10) return .18; // >10 (bordure)
if (p.length > 7) return .3; // 8, 9, 10
if (p.length > 4) return .4; // 5, 6, 7
if (p.length > 2) return .5; // 3, 4
return .7; // 1, 2
if (p === "jln" || p === "jlh") return 0.7;
if (p === "abcpqh" || p === "ez" || p === "be") return 0.5;
if (["a", "b", "c", "d", "f", "g", "h", "i", "bh", "df"].includes(p)) return 0.5;
if (["j", "l", "m", "o", "jlmo"].includes(p) && d === "perCross") return 0.6;
if (p.length > 10) return 0.18; // >10 (bordure)
if (p.length > 7) return 0.3; // 8, 9, 10
if (p.length > 4) return 0.4; // 5, 6, 7
if (p.length > 2) return 0.5; // 3, 4
return 0.7; // 1, 2
}
return coa;
}
};
const getShield = function(culture, state) {
const getShield = function (culture, state) {
const emblemShape = document.getElementById("emblemShape");
const shapeGroup = emblemShape.selectedOptions[0]?.parentNode.label || "Diversiform";
if (shapeGroup !== "Diversiform") return emblemShape.value;
@ -504,11 +780,10 @@
if (pack.cultures[culture].shield) return pack.cultures[culture].shield;
console.error("Shield shape is not defined on culture level", pack.cultures[culture]);
return "heater";
}
};
const toString = coa => JSON.stringify(coa).replaceAll("#", "%23");
const copy = coa => JSON.parse(JSON.stringify(coa));
return {generate, toString, copy, getShield, shields};
})));
})();

File diff suppressed because it is too large Load diff

View file

@ -1,13 +1,10 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.Cultures = factory());
}(this, (function () {'use strict';
"use strict";
window.Cultures = (function () {
let cells;
const generate = function() {
TIME && console.time('generateCultures');
const generate = function () {
TIME && console.time("generateCultures");
cells = pack.cells;
cells.culture = new Uint16Array(cells.i.length); // cell cultures
let count = Math.min(+culturesInput.value, +culturesSet.selectedOptions[0].dataset.max);
@ -17,13 +14,19 @@
count = Math.floor(populated.length / 50);
if (!count) {
WARN && console.warn(`There are no populated cells. Cannot generate cultures`);
pack.cultures = [{name:"Wildlands", i:0, base:1, shield:"round"}];
pack.cultures = [{name: "Wildlands", i: 0, base: 1, shield: "round"}];
alertMessage.innerHTML = `
The climate is harsh and people cannot live in this world.<br>
No cultures, states and burgs will be created.<br>
Please consider changing climate settings in the World Configurator`;
$("#alert").dialog({resizable: false, title: "Extreme climate warning",
buttons: {Ok: function() {$(this).dialog("close");}}
$("#alert").dialog({
resizable: false,
title: "Extreme climate warning",
buttons: {
Ok: function () {
$(this).dialog("close");
}
}
});
return;
} else {
@ -32,22 +35,28 @@
There are only ${populated.length} populated cells and it's insufficient livable area.<br>
Only ${count} out of ${culturesInput.value} requested cultures will be generated.<br>
Please consider changing climate settings in the World Configurator`;
$("#alert").dialog({resizable: false, title: "Extreme climate warning",
buttons: {Ok: function() {$(this).dialog("close");}}
$("#alert").dialog({
resizable: false,
title: "Extreme climate warning",
buttons: {
Ok: function () {
$(this).dialog("close");
}
}
});
}
}
const cultures = pack.cultures = selectCultures(count);
const cultures = (pack.cultures = selectCultures(count));
const centers = d3.quadtree();
const colors = getColors(count);
const emblemShape = document.getElementById("emblemShape").value;
const codes = [];
cultures.forEach(function(c, i) {
const cell = c.center = placeCenter(c.sort ? c.sort : (i) => cells.s[i]);
cultures.forEach(function (c, i) {
const cell = (c.center = placeCenter(c.sort ? c.sort : i => cells.s[i]));
centers.add(cells.p[cell]);
c.i = i+1;
c.i = i + 1;
delete c.odd;
delete c.sort;
c.color = colors[i];
@ -56,20 +65,24 @@
c.origin = 0;
c.code = abbreviate(c.name, codes);
codes.push(c.code);
cells.culture[cell] = i+1;
cells.culture[cell] = i + 1;
if (emblemShape === "random") c.shield = getRandomShield();
});
function placeCenter(v) {
let c, spacing = (graphWidth + graphHeight) / 2 / count;
const sorted = [...populated].sort((a, b) => v(b) - v(a)), max = Math.floor(sorted.length / 2);
do {c = sorted[biased(0, max, 5)]; spacing *= .9;}
while (centers.find(cells.p[c][0], cells.p[c][1], spacing) !== undefined);
let c,
spacing = (graphWidth + graphHeight) / 2 / count;
const sorted = [...populated].sort((a, b) => v(b) - v(a)),
max = Math.floor(sorted.length / 2);
do {
c = sorted[biased(0, max, 5)];
spacing *= 0.9;
} while (centers.find(cells.p[c][0], cells.p[c][1], spacing) !== undefined);
return c;
}
// the first culture with id 0 is for wildlands
cultures.unshift({name:"Wildlands", i:0, base:1, origin:null, shield:"round"});
cultures.unshift({name: "Wildlands", i: 0, base: 1, origin: null, shield: "round"});
// make sure all bases exist in nameBases
if (!nameBases.length) {
@ -77,7 +90,7 @@
nameBases = Names.getNameBases();
}
cultures.forEach(c => c.base = c.base % nameBases.length);
cultures.forEach(c => (c.base = c.base % nameBases.length));
function selectCultures(c) {
let def = getDefault(c);
@ -87,11 +100,11 @@
const count = Math.min(c, def.length);
const cultures = [];
for (let culture, rnd, i=0; cultures.length < count && i < 200; i++) {
for (let culture, rnd, i = 0; cultures.length < count && i < 200; i++) {
do {
rnd = rand(def.length-1);
rnd = rand(def.length - 1);
culture = def[rnd];
} while (!P(culture.odd))
} while (!P(culture.odd));
cultures.push(culture);
def.splice(rnd, 1);
}
@ -100,31 +113,31 @@
// set culture type based on culture center position
function defineCultureType(i) {
if (cells.h[i] < 70 && [1,2,4].includes(cells.biome[i])) return "Nomadic"; // high penalty in forest biomes and near coastline
if (cells.h[i] < 70 && [1, 2, 4].includes(cells.biome[i])) return "Nomadic"; // high penalty in forest biomes and near coastline
if (cells.h[i] > 50) return "Highland"; // no penalty for hills and moutains, high for other elevations
const f = pack.features[cells.f[cells.haven[i]]]; // opposite feature
if (f.type === "lake" && f.cells > 5) return "Lake" // low water cross penalty and high for growth not along coastline
if (cells.harbor[i] && f.type !== "lake" && P(.1) || (cells.harbor[i] === 1 && P(.6)) || (pack.features[cells.f[i]].group === "isle" && P(.4))) return "Naval"; // low water cross penalty and high for non-along-coastline growth
if (f.type === "lake" && f.cells > 5) return "Lake"; // low water cross penalty and high for growth not along coastline
if ((cells.harbor[i] && f.type !== "lake" && P(0.1)) || (cells.harbor[i] === 1 && P(0.6)) || (pack.features[cells.f[i]].group === "isle" && P(0.4))) return "Naval"; // low water cross penalty and high for non-along-coastline growth
if (cells.r[i] && cells.fl[i] > 100) return "River"; // no River cross penalty, penalty for non-River growth
if (cells.t[i] > 2 && [3,7,8,9,10,12].includes(cells.biome[i])) return "Hunting"; // high penalty in non-native biomes
if (cells.t[i] > 2 && [3, 7, 8, 9, 10, 12].includes(cells.biome[i])) return "Hunting"; // high penalty in non-native biomes
return "Generic";
}
function defineCultureExpansionism(type) {
let base = 1; // Generic
if (type === "Lake") base = .8; else
if (type === "Naval") base = 1.5; else
if (type === "River") base = .9; else
if (type === "Nomadic") base = 1.5; else
if (type === "Hunting") base = .7; else
if (type === "Highland") base = 1.2;
return rn((Math.random() * powerInput.value / 2 + 1) * base, 1);
if (type === "Lake") base = 0.8;
else if (type === "Naval") base = 1.5;
else if (type === "River") base = 0.9;
else if (type === "Nomadic") base = 1.5;
else if (type === "Hunting") base = 0.7;
else if (type === "Highland") base = 1.2;
return rn(((Math.random() * powerInput.value) / 2 + 1) * base, 1);
}
TIME && console.timeEnd('generateCultures');
}
TIME && console.timeEnd("generateCultures");
};
const add = function(center) {
const add = function (center) {
const defaultCultures = getDefault();
let culture, base, name;
@ -139,7 +152,10 @@
name = Names.getCulture(culture, 5, 8, "");
base = pack.cultures[culture].base;
}
const code = abbreviate(name, pack.cultures.map(c => c.code));
const code = abbreviate(
name,
pack.cultures.map(c => c.code)
);
const i = pack.cultures.length;
const color = d3.color(d3.scaleSequential(d3.interpolateRainbow)(Math.random())).hex();
@ -148,220 +164,231 @@
const emblemShape = document.getElementById("emblemShape").value;
if (emblemShape === "random") shield = getRandomShield();
pack.cultures.push({name, color, base, center, i, expansionism:1, type:"Generic", cells:0, area:0, rural:0, urban:0, origin:0, code, shield});
}
pack.cultures.push({name, color, base, center, i, expansionism: 1, type: "Generic", cells: 0, area: 0, rural: 0, urban: 0, origin: 0, code, shield});
};
const getDefault = function(count) {
const getDefault = function (count) {
// generic sorting functions
const cells = pack.cells, s = cells.s, sMax = d3.max(s), t = cells.t, h = cells.h, temp = grid.cells.temp;
const n = cell => Math.ceil(s[cell] / sMax * 3) // normalized cell score
const td = (cell, goal) => {const d = Math.abs(temp[cells.g[cell]] - goal); return d ? d+1 : 1;} // temperature difference fee
const bd = (cell, biomes, fee = 4) => biomes.includes(cells.biome[cell]) ? 1 : fee; // biome difference fee
const sf = (cell, fee = 4) => cells.haven[cell] && pack.features[cells.f[cells.haven[cell]]].type !== "lake" ? 1 : fee; // not on sea coast fee
const cells = pack.cells,
s = cells.s,
sMax = d3.max(s),
t = cells.t,
h = cells.h,
temp = grid.cells.temp;
const n = cell => Math.ceil((s[cell] / sMax) * 3); // normalized cell score
const td = (cell, goal) => {
const d = Math.abs(temp[cells.g[cell]] - goal);
return d ? d + 1 : 1;
}; // temperature difference fee
const bd = (cell, biomes, fee = 4) => (biomes.includes(cells.biome[cell]) ? 1 : fee); // biome difference fee
const sf = (cell, fee = 4) => (cells.haven[cell] && pack.features[cells.f[cells.haven[cell]]].type !== "lake" ? 1 : fee); // not on sea coast fee
if (culturesSet.value === "european") {
return [
{name:"Shwazen", base:0, odd:1, sort: i => n(i) / td(i, 10) / bd(i, [6, 8]), shield:"swiss"},
{name:"Angshire", base:1, odd:1, sort: i => n(i) / td(i, 10) / sf(i), shield:"wedged"},
{name:"Luari", base:2, odd:1, sort: i => n(i) / td(i, 12) / bd(i, [6, 8]), shield:"french"},
{name:"Tallian", base:3, odd:1, sort: i => n(i) / td(i, 15), shield:"horsehead"},
{name:"Astellian", base:4, odd:1, sort: i => n(i) / td(i, 16), shield:"spanish"},
{name:"Slovan", base:5, odd:1, sort: i => n(i) / td(i, 6) * t[i], shield:"polish"},
{name:"Norse", base:6, odd:1, sort: i => n(i) / td(i, 5), shield:"heater"},
{name:"Elladan", base:7, odd:1, sort: i => n(i) / td(i, 18) * h[i], shield:"boeotian"},
{name:"Romian", base:8, odd:.2, sort: i => n(i) / td(i, 15) / t[i], shield:"roman"},
{name:"Soumi", base:9, odd:1, sort: i => n(i) / td(i, 5) / bd(i, [9]) * t[i], shield:"pavise"},
{name:"Portuzian", base:13, odd:1, sort: i => n(i) / td(i, 17) / sf(i), shield:"renaissance"},
{name:"Vengrian", base: 15, odd:1, sort: i => n(i) / td(i, 11) / bd(i, [4]) * t[i], shield:"horsehead2"},
{name:"Turchian", base: 16, odd:.05, sort: i => n(i) / td(i, 14), shield:"round"},
{name:"Euskati", base: 20, odd:.05, sort: i => n(i) / td(i, 15) * h[i], shield:"oldFrench"},
{name:"Keltan", base: 22, odd:.05, sort: i => n(i) / td(i, 11) / bd(i, [6, 8]) * t[i], shield:"oval"}
{name: "Shwazen", base: 0, odd: 1, sort: i => n(i) / td(i, 10) / bd(i, [6, 8]), shield: "swiss"},
{name: "Angshire", base: 1, odd: 1, sort: i => n(i) / td(i, 10) / sf(i), shield: "wedged"},
{name: "Luari", base: 2, odd: 1, sort: i => n(i) / td(i, 12) / bd(i, [6, 8]), shield: "french"},
{name: "Tallian", base: 3, odd: 1, sort: i => n(i) / td(i, 15), shield: "horsehead"},
{name: "Astellian", base: 4, odd: 1, sort: i => n(i) / td(i, 16), shield: "spanish"},
{name: "Slovan", base: 5, odd: 1, sort: i => (n(i) / td(i, 6)) * t[i], shield: "polish"},
{name: "Norse", base: 6, odd: 1, sort: i => n(i) / td(i, 5), shield: "heater"},
{name: "Elladan", base: 7, odd: 1, sort: i => (n(i) / td(i, 18)) * h[i], shield: "boeotian"},
{name: "Romian", base: 8, odd: 0.2, sort: i => n(i) / td(i, 15) / t[i], shield: "roman"},
{name: "Soumi", base: 9, odd: 1, sort: i => (n(i) / td(i, 5) / bd(i, [9])) * t[i], shield: "pavise"},
{name: "Portuzian", base: 13, odd: 1, sort: i => n(i) / td(i, 17) / sf(i), shield: "renaissance"},
{name: "Vengrian", base: 15, odd: 1, sort: i => (n(i) / td(i, 11) / bd(i, [4])) * t[i], shield: "horsehead2"},
{name: "Turchian", base: 16, odd: 0.05, sort: i => n(i) / td(i, 14), shield: "round"},
{name: "Euskati", base: 20, odd: 0.05, sort: i => (n(i) / td(i, 15)) * h[i], shield: "oldFrench"},
{name: "Keltan", base: 22, odd: 0.05, sort: i => (n(i) / td(i, 11) / bd(i, [6, 8])) * t[i], shield: "oval"}
];
}
if (culturesSet.value === "oriental") {
return [
{name:"Koryo", base:10, odd:1, sort: i => n(i) / td(i, 12) / t[i], shield:"round"},
{name:"Hantzu", base:11, odd:1, sort: i => n(i) / td(i, 13), shield:"banner"},
{name:"Yamoto", base:12, odd:1, sort: i => n(i) / td(i, 15) / t[i], shield:"round"},
{name:"Turchian", base: 16, odd:1, sort: i => n(i) / td(i, 12), shield:"round"},
{name:"Berberan", base: 17, odd:.2, sort: i => n(i) / td(i, 19) / bd(i, [1, 2, 3], 7) * t[i], shield:"oval"},
{name:"Eurabic", base: 18, odd:1, sort: i => n(i) / td(i, 26) / bd(i, [1, 2], 7) * t[i], shield:"oval"},
{name:"Efratic", base: 23, odd:.1, sort: i => n(i) / td(i, 22) * t[i], shield:"round"},
{name:"Tehrani", base: 24, odd:1, sort: i => n(i) / td(i, 18) * h[i], shield:"round"},
{name:"Maui", base: 25, odd:.2, sort: i => n(i) / td(i, 24) / sf(i) / t[i], shield:"vesicaPiscis"},
{name:"Carnatic", base: 26, odd:.5, sort: i => n(i) / td(i, 26), shield:"round"},
{name:"Vietic", base: 29, odd:.8, sort: i => n(i) / td(i, 25) / bd(i, [7], 7) / t[i], shield:"banner"},
{name:"Guantzu", base:30, odd:.5, sort: i => n(i) / td(i, 17), shield:"banner"},
{name:"Ulus", base:31, odd:1, sort: i => n(i) / td(i, 5) / bd(i, [2, 4, 10], 7) * t[i], shield:"banner"}
{name: "Koryo", base: 10, odd: 1, sort: i => n(i) / td(i, 12) / t[i], shield: "round"},
{name: "Hantzu", base: 11, odd: 1, sort: i => n(i) / td(i, 13), shield: "banner"},
{name: "Yamoto", base: 12, odd: 1, sort: i => n(i) / td(i, 15) / t[i], shield: "round"},
{name: "Turchian", base: 16, odd: 1, sort: i => n(i) / td(i, 12), shield: "round"},
{name: "Berberan", base: 17, odd: 0.2, sort: i => (n(i) / td(i, 19) / bd(i, [1, 2, 3], 7)) * t[i], shield: "oval"},
{name: "Eurabic", base: 18, odd: 1, sort: i => (n(i) / td(i, 26) / bd(i, [1, 2], 7)) * t[i], shield: "oval"},
{name: "Efratic", base: 23, odd: 0.1, sort: i => (n(i) / td(i, 22)) * t[i], shield: "round"},
{name: "Tehrani", base: 24, odd: 1, sort: i => (n(i) / td(i, 18)) * h[i], shield: "round"},
{name: "Maui", base: 25, odd: 0.2, sort: i => n(i) / td(i, 24) / sf(i) / t[i], shield: "vesicaPiscis"},
{name: "Carnatic", base: 26, odd: 0.5, sort: i => n(i) / td(i, 26), shield: "round"},
{name: "Vietic", base: 29, odd: 0.8, sort: i => n(i) / td(i, 25) / bd(i, [7], 7) / t[i], shield: "banner"},
{name: "Guantzu", base: 30, odd: 0.5, sort: i => n(i) / td(i, 17), shield: "banner"},
{name: "Ulus", base: 31, odd: 1, sort: i => (n(i) / td(i, 5) / bd(i, [2, 4, 10], 7)) * t[i], shield: "banner"}
];
}
if (culturesSet.value === "english") {
const getName = () => Names.getBase(1, 5, 9, "", 0);
return [
{name:getName(), base:1, odd:1, shield:"heater"},
{name:getName(), base:1, odd:1, shield:"wedged"},
{name:getName(), base:1, odd:1, shield:"swiss"},
{name:getName(), base:1, odd:1, shield:"oldFrench"},
{name:getName(), base:1, odd:1, shield:"swiss"},
{name:getName(), base:1, odd:1, shield:"spanish"},
{name:getName(), base:1, odd:1, shield:"hessen"},
{name:getName(), base:1, odd:1, shield:"fantasy5"},
{name:getName(), base:1, odd:1, shield:"fantasy4"},
{name:getName(), base:1, odd:1, shield:"fantasy1"}
{name: getName(), base: 1, odd: 1, shield: "heater"},
{name: getName(), base: 1, odd: 1, shield: "wedged"},
{name: getName(), base: 1, odd: 1, shield: "swiss"},
{name: getName(), base: 1, odd: 1, shield: "oldFrench"},
{name: getName(), base: 1, odd: 1, shield: "swiss"},
{name: getName(), base: 1, odd: 1, shield: "spanish"},
{name: getName(), base: 1, odd: 1, shield: "hessen"},
{name: getName(), base: 1, odd: 1, shield: "fantasy5"},
{name: getName(), base: 1, odd: 1, shield: "fantasy4"},
{name: getName(), base: 1, odd: 1, shield: "fantasy1"}
];
}
if (culturesSet.value === "antique") {
return [
{name:"Roman", base:8, odd:1, sort: i => n(i) / td(i, 14) / t[i], shield:"roman"}, // Roman
{name:"Roman", base:8, odd:1, sort: i => n(i) / td(i, 15) / sf(i), shield:"roman"}, // Roman
{name:"Roman", base:8, odd:1, sort: i => n(i) / td(i, 16) / sf(i), shield:"roman"}, // Roman
{name:"Roman", base:8, odd:1, sort: i => n(i) / td(i, 17) / t[i], shield:"roman"}, // Roman
{name:"Hellenic", base:7, odd:1, sort: i => n(i) / td(i, 18) / sf(i) * h[i], shield:"boeotian"}, // Greek
{name:"Hellenic", base:7, odd:1, sort: i => n(i) / td(i, 19) / sf(i) * h[i], shield:"boeotian"}, // Greek
{name:"Macedonian", base:7, odd:.5, sort: i => n(i) / td(i, 12) * h[i], shield:"round"}, // Greek
{name:"Celtic", base:22, odd:1, sort: i => n(i) / td(i, 11) ** .5 / bd(i, [6, 8]), shield:"round"},
{name:"Germanic", base:0, odd:1, sort: i => n(i) / td(i, 10) ** .5 / bd(i, [6, 8]), shield:"round"},
{name:"Persian", base:24, odd:.8, sort: i => n(i) / td(i, 18) * h[i], shield:"oval"}, // Iranian
{name:"Scythian", base:24, odd:.5, sort: i => n(i) / td(i, 11) ** .5 / bd(i, [4]), shield:"round"}, // Iranian
{name:"Cantabrian", base: 20, odd:.5, sort: i => n(i) / td(i, 16) * h[i], shield:"oval"}, // Basque
{name:"Estian", base: 9, odd:.2, sort: i => n(i) / td(i, 5) * t[i], shield:"pavise"}, // Finnic
{name:"Carthaginian", base: 17, odd:.3, sort: i => n(i) / td(i, 19) / sf(i), shield:"oval"}, // Berber
{name:"Mesopotamian", base: 23, odd:.2, sort: i => n(i) / td(i, 22) / bd(i, [1, 2, 3]), shield:"oval"} // Mesopotamian
{name: "Roman", base: 8, odd: 1, sort: i => n(i) / td(i, 14) / t[i], shield: "roman"}, // Roman
{name: "Roman", base: 8, odd: 1, sort: i => n(i) / td(i, 15) / sf(i), shield: "roman"}, // Roman
{name: "Roman", base: 8, odd: 1, sort: i => n(i) / td(i, 16) / sf(i), shield: "roman"}, // Roman
{name: "Roman", base: 8, odd: 1, sort: i => n(i) / td(i, 17) / t[i], shield: "roman"}, // Roman
{name: "Hellenic", base: 7, odd: 1, sort: i => (n(i) / td(i, 18) / sf(i)) * h[i], shield: "boeotian"}, // Greek
{name: "Hellenic", base: 7, odd: 1, sort: i => (n(i) / td(i, 19) / sf(i)) * h[i], shield: "boeotian"}, // Greek
{name: "Macedonian", base: 7, odd: 0.5, sort: i => (n(i) / td(i, 12)) * h[i], shield: "round"}, // Greek
{name: "Celtic", base: 22, odd: 1, sort: i => n(i) / td(i, 11) ** 0.5 / bd(i, [6, 8]), shield: "round"},
{name: "Germanic", base: 0, odd: 1, sort: i => n(i) / td(i, 10) ** 0.5 / bd(i, [6, 8]), shield: "round"},
{name: "Persian", base: 24, odd: 0.8, sort: i => (n(i) / td(i, 18)) * h[i], shield: "oval"}, // Iranian
{name: "Scythian", base: 24, odd: 0.5, sort: i => n(i) / td(i, 11) ** 0.5 / bd(i, [4]), shield: "round"}, // Iranian
{name: "Cantabrian", base: 20, odd: 0.5, sort: i => (n(i) / td(i, 16)) * h[i], shield: "oval"}, // Basque
{name: "Estian", base: 9, odd: 0.2, sort: i => (n(i) / td(i, 5)) * t[i], shield: "pavise"}, // Finnic
{name: "Carthaginian", base: 17, odd: 0.3, sort: i => n(i) / td(i, 19) / sf(i), shield: "oval"}, // Berber
{name: "Mesopotamian", base: 23, odd: 0.2, sort: i => n(i) / td(i, 22) / bd(i, [1, 2, 3]), shield: "oval"} // Mesopotamian
];
}
if (culturesSet.value === "highFantasy") {
return [
// fantasy races
{name:"Quenian (Elfish)", base: 33, odd:1, sort: i => n(i) / bd(i, [6,7,8,9], 10) * t[i], shield:"gondor"}, // Elves
{name:"Eldar (Elfish)", base: 33, odd:1, sort: i => n(i) / bd(i, [6,7,8,9], 10) * t[i], shield:"noldor"}, // Elves
{name:"Trow (Dark Elfish)", base: 34, odd:.9, sort: i => n(i) / bd(i, [7,8,9,12], 10) * t[i], shield:"hessen"}, // Dark Elves
{name:"Lothian (Dark Elfish)", base: 34, odd:.3, sort: i => n(i) / bd(i, [7,8,9,12], 10) * t[i], shield:"wedged"}, // Dark Elves
{name:"Dunirr (Dwarven)", base: 35, odd:1, sort: i => n(i) + h[i], shield:"ironHills"}, // Dwarfs
{name:"Khazadur (Dwarven)", base: 35, odd:1, sort: i => n(i) + h[i], shield:"erebor"}, // Dwarfs
{name:"Kobold (Goblin)", base: 36, odd:1, sort: i => t[i] - s[i], shield:"moriaOrc"}, // Goblin
{name:"Uruk (Orkish)", base: 37, odd:1, sort: i => h[i] * t[i], shield:"urukHai"}, // Orc
{name:"Ugluk (Orkish)", base: 37, odd:.5, sort: i => h[i] * t[i] / bd(i, [1,2,10,11]), shield:"moriaOrc"}, // Orc
{name:"Yotunn (Giants)", base: 38, odd:.7, sort: i => td(i, -10), shield:"pavise"}, // Giant
{name:"Rake (Drakonic)", base: 39, odd:.7, sort: i => -s[i], shield:"fantasy2"}, // Draconic
{name:"Arago (Arachnid)", base: 40, odd:.7, sort: i => t[i] - s[i], shield:"horsehead2"}, // Arachnid
{name:"Aj'Snaga (Serpents)", base: 41, odd:.7, sort: i => n(i) / bd(i, [12], 10), shield:"fantasy1"}, // Serpents
{name: "Quenian (Elfish)", base: 33, odd: 1, sort: i => (n(i) / bd(i, [6, 7, 8, 9], 10)) * t[i], shield: "gondor"}, // Elves
{name: "Eldar (Elfish)", base: 33, odd: 1, sort: i => (n(i) / bd(i, [6, 7, 8, 9], 10)) * t[i], shield: "noldor"}, // Elves
{name: "Trow (Dark Elfish)", base: 34, odd: 0.9, sort: i => (n(i) / bd(i, [7, 8, 9, 12], 10)) * t[i], shield: "hessen"}, // Dark Elves
{name: "Lothian (Dark Elfish)", base: 34, odd: 0.3, sort: i => (n(i) / bd(i, [7, 8, 9, 12], 10)) * t[i], shield: "wedged"}, // Dark Elves
{name: "Dunirr (Dwarven)", base: 35, odd: 1, sort: i => n(i) + h[i], shield: "ironHills"}, // Dwarfs
{name: "Khazadur (Dwarven)", base: 35, odd: 1, sort: i => n(i) + h[i], shield: "erebor"}, // Dwarfs
{name: "Kobold (Goblin)", base: 36, odd: 1, sort: i => t[i] - s[i], shield: "moriaOrc"}, // Goblin
{name: "Uruk (Orkish)", base: 37, odd: 1, sort: i => h[i] * t[i], shield: "urukHai"}, // Orc
{name: "Ugluk (Orkish)", base: 37, odd: 0.5, sort: i => (h[i] * t[i]) / bd(i, [1, 2, 10, 11]), shield: "moriaOrc"}, // Orc
{name: "Yotunn (Giants)", base: 38, odd: 0.7, sort: i => td(i, -10), shield: "pavise"}, // Giant
{name: "Rake (Drakonic)", base: 39, odd: 0.7, sort: i => -s[i], shield: "fantasy2"}, // Draconic
{name: "Arago (Arachnid)", base: 40, odd: 0.7, sort: i => t[i] - s[i], shield: "horsehead2"}, // Arachnid
{name: "Aj'Snaga (Serpents)", base: 41, odd: 0.7, sort: i => n(i) / bd(i, [12], 10), shield: "fantasy1"}, // Serpents
// fantasy human
{name:"Anor (Human)", base:32, odd:1, sort: i => n(i) / td(i, 10), shield:"fantasy5"},
{name:"Dail (Human)", base:32, odd:1, sort: i => n(i) / td(i, 13), shield:"roman"},
{name:"Rohand (Human)", base:16, odd:1, sort: i => n(i) / td(i, 16), shield:"round"},
{name:"Dulandir (Human)", base:31, odd:1, sort: i => n(i) / td(i, 5) / bd(i, [2, 4, 10], 7) * t[i], shield:"easterling"},
{name: "Anor (Human)", base: 32, odd: 1, sort: i => n(i) / td(i, 10), shield: "fantasy5"},
{name: "Dail (Human)", base: 32, odd: 1, sort: i => n(i) / td(i, 13), shield: "roman"},
{name: "Rohand (Human)", base: 16, odd: 1, sort: i => n(i) / td(i, 16), shield: "round"},
{name: "Dulandir (Human)", base: 31, odd: 1, sort: i => (n(i) / td(i, 5) / bd(i, [2, 4, 10], 7)) * t[i], shield: "easterling"}
];
}
if (culturesSet.value === "darkFantasy") {
return [
// common real-world English
{name:"Angshire", base:1, odd:1, sort: i => n(i) / td(i, 10) / sf(i), shield:"heater"},
{name:"Enlandic", base:1, odd:1, sort: i => n(i) / td(i, 12), shield:"heater"},
{name:"Westen", base:1, odd:1, sort: i => n(i) / td(i, 10), shield:"heater"},
{name:"Nortumbic", base:1, odd:1, sort: i => n(i) / td(i, 7), shield:"heater"},
{name:"Mercian", base:1, odd:1, sort: i => n(i) / td(i, 9), shield:"heater"},
{name:"Kentian", base:1, odd:1, sort: i => n(i) / td(i, 12), shield:"heater"},
{name: "Angshire", base: 1, odd: 1, sort: i => n(i) / td(i, 10) / sf(i), shield: "heater"},
{name: "Enlandic", base: 1, odd: 1, sort: i => n(i) / td(i, 12), shield: "heater"},
{name: "Westen", base: 1, odd: 1, sort: i => n(i) / td(i, 10), shield: "heater"},
{name: "Nortumbic", base: 1, odd: 1, sort: i => n(i) / td(i, 7), shield: "heater"},
{name: "Mercian", base: 1, odd: 1, sort: i => n(i) / td(i, 9), shield: "heater"},
{name: "Kentian", base: 1, odd: 1, sort: i => n(i) / td(i, 12), shield: "heater"},
// rare real-world western
{name:"Norse", base:6, odd:.7, sort: i => n(i) / td(i, 5) / sf(i), shield:"oldFrench"},
{name:"Schwarzen", base:0, odd:.3, sort: i => n(i) / td(i, 10) / bd(i, [6, 8]), shield:"gonfalon"},
{name:"Luarian", base:2, odd:.3, sort: i => n(i) / td(i, 12) / bd(i, [6, 8]), shield:"oldFrench"},
{name:"Hetallian", base:3, odd:.3, sort: i => n(i) / td(i, 15), shield:"oval"},
{name:"Astellian", base:4, odd:.3, sort: i => n(i) / td(i, 16), shield:"spanish"},
{name: "Norse", base: 6, odd: 0.7, sort: i => n(i) / td(i, 5) / sf(i), shield: "oldFrench"},
{name: "Schwarzen", base: 0, odd: 0.3, sort: i => n(i) / td(i, 10) / bd(i, [6, 8]), shield: "gonfalon"},
{name: "Luarian", base: 2, odd: 0.3, sort: i => n(i) / td(i, 12) / bd(i, [6, 8]), shield: "oldFrench"},
{name: "Hetallian", base: 3, odd: 0.3, sort: i => n(i) / td(i, 15), shield: "oval"},
{name: "Astellian", base: 4, odd: 0.3, sort: i => n(i) / td(i, 16), shield: "spanish"},
// rare real-world exotic
{name:"Kiswaili", base:28, odd:.05, sort: i => n(i) / td(i, 29) / bd(i, [1, 3, 5, 7]), shield:"vesicaPiscis"},
{name:"Yoruba", base:21, odd:.05, sort: i => n(i) / td(i, 15) / bd(i, [5, 7]), shield:"vesicaPiscis"},
{name:"Koryo", base:10, odd:.05, sort: i => n(i) / td(i, 12) / t[i], shield:"round"},
{name:"Hantzu", base:11, odd:.05, sort: i => n(i) / td(i, 13), shield:"banner"},
{name:"Yamoto", base:12, odd:.05, sort: i => n(i) / td(i, 15) / t[i], shield:"round"},
{name:"Guantzu", base:30, odd:.05, sort: i => n(i) / td(i, 17), shield:"banner"},
{name:"Ulus", base:31, odd:.05, sort: i => n(i) / td(i, 5) / bd(i, [2, 4, 10], 7) * t[i], shield:"banner"},
{name:"Turan", base: 16, odd:.05, sort: i => n(i) / td(i, 12), shield:"round"},
{name:"Berberan", base: 17, odd:.05, sort: i => n(i) / td(i, 19) / bd(i, [1, 2, 3], 7) * t[i], shield:"round"},
{name:"Eurabic", base: 18, odd:.05, sort: i => n(i) / td(i, 26) / bd(i, [1, 2], 7) * t[i], shield:"round"},
{name:"Slovan", base:5, odd:.05, sort: i => n(i) / td(i, 6) * t[i], shield:"round"},
{name:"Keltan", base: 22, odd:.1, sort: i => n(i) / td(i, 11) ** .5 / bd(i, [6, 8]), shield:"vesicaPiscis"},
{name:"Elladan", base:7, odd:.2, sort: i => n(i) / td(i, 18) / sf(i) * h[i], shield:"boeotian"},
{name:"Romian", base:8, odd:.2, sort: i => n(i) / td(i, 14) / t[i], shield:"roman"},
{name: "Kiswaili", base: 28, odd: 0.05, sort: i => n(i) / td(i, 29) / bd(i, [1, 3, 5, 7]), shield: "vesicaPiscis"},
{name: "Yoruba", base: 21, odd: 0.05, sort: i => n(i) / td(i, 15) / bd(i, [5, 7]), shield: "vesicaPiscis"},
{name: "Koryo", base: 10, odd: 0.05, sort: i => n(i) / td(i, 12) / t[i], shield: "round"},
{name: "Hantzu", base: 11, odd: 0.05, sort: i => n(i) / td(i, 13), shield: "banner"},
{name: "Yamoto", base: 12, odd: 0.05, sort: i => n(i) / td(i, 15) / t[i], shield: "round"},
{name: "Guantzu", base: 30, odd: 0.05, sort: i => n(i) / td(i, 17), shield: "banner"},
{name: "Ulus", base: 31, odd: 0.05, sort: i => (n(i) / td(i, 5) / bd(i, [2, 4, 10], 7)) * t[i], shield: "banner"},
{name: "Turan", base: 16, odd: 0.05, sort: i => n(i) / td(i, 12), shield: "round"},
{name: "Berberan", base: 17, odd: 0.05, sort: i => (n(i) / td(i, 19) / bd(i, [1, 2, 3], 7)) * t[i], shield: "round"},
{name: "Eurabic", base: 18, odd: 0.05, sort: i => (n(i) / td(i, 26) / bd(i, [1, 2], 7)) * t[i], shield: "round"},
{name: "Slovan", base: 5, odd: 0.05, sort: i => (n(i) / td(i, 6)) * t[i], shield: "round"},
{name: "Keltan", base: 22, odd: 0.1, sort: i => n(i) / td(i, 11) ** 0.5 / bd(i, [6, 8]), shield: "vesicaPiscis"},
{name: "Elladan", base: 7, odd: 0.2, sort: i => (n(i) / td(i, 18) / sf(i)) * h[i], shield: "boeotian"},
{name: "Romian", base: 8, odd: 0.2, sort: i => n(i) / td(i, 14) / t[i], shield: "roman"},
// fantasy races
{name:"Eldar", base: 33, odd:.5, sort: i => n(i) / bd(i, [6,7,8,9], 10) * t[i], shield:"fantasy5"}, // Elves
{name:"Trow", base: 34, odd:.8, sort: i => n(i) / bd(i, [7,8,9,12], 10) * t[i], shield:"hessen"}, // Dark Elves
{name:"Durinn", base: 35, odd:.8, sort: i => n(i) + h[i], shield:"erebor"}, // Dwarven
{name:"Kobblin", base: 36, odd:.8, sort: i => t[i] - s[i], shield:"moriaOrc"}, // Goblin
{name:"Uruk", base: 37, odd:.8, sort: i => h[i] * t[i] / bd(i, [1,2,10,11]), shield:"urukHai"}, // Orc
{name:"Yotunn", base: 38, odd:.8, sort: i => td(i, -10), shield:"pavise"}, // Giant
{name:"Drake", base: 39, odd:.9, sort: i => -s[i], shield:"fantasy2"}, // Draconic
{name:"Rakhnid", base: 40, odd:.9, sort: i => t[i] - s[i], shield:"horsehead2"}, // Arachnid
{name:"Aj'Snaga", base: 41, odd:.9, sort: i => n(i) / bd(i, [12], 10), shield:"fantasy1"}, // Serpents
]
{name: "Eldar", base: 33, odd: 0.5, sort: i => (n(i) / bd(i, [6, 7, 8, 9], 10)) * t[i], shield: "fantasy5"}, // Elves
{name: "Trow", base: 34, odd: 0.8, sort: i => (n(i) / bd(i, [7, 8, 9, 12], 10)) * t[i], shield: "hessen"}, // Dark Elves
{name: "Durinn", base: 35, odd: 0.8, sort: i => n(i) + h[i], shield: "erebor"}, // Dwarven
{name: "Kobblin", base: 36, odd: 0.8, sort: i => t[i] - s[i], shield: "moriaOrc"}, // Goblin
{name: "Uruk", base: 37, odd: 0.8, sort: i => (h[i] * t[i]) / bd(i, [1, 2, 10, 11]), shield: "urukHai"}, // Orc
{name: "Yotunn", base: 38, odd: 0.8, sort: i => td(i, -10), shield: "pavise"}, // Giant
{name: "Drake", base: 39, odd: 0.9, sort: i => -s[i], shield: "fantasy2"}, // Draconic
{name: "Rakhnid", base: 40, odd: 0.9, sort: i => t[i] - s[i], shield: "horsehead2"}, // Arachnid
{name: "Aj'Snaga", base: 41, odd: 0.9, sort: i => n(i) / bd(i, [12], 10), shield: "fantasy1"} // Serpents
];
}
if (culturesSet.value === "random") {
return d3.range(count).map(function() {
const rnd = rand(nameBases.length-1);
return d3.range(count).map(function () {
const rnd = rand(nameBases.length - 1);
const name = Names.getBaseShort(rnd);
return {name, base:rnd, odd:1, shield:getRandomShield()}
return {name, base: rnd, odd: 1, shield: getRandomShield()};
});
}
// all-world
return [
{name:"Shwazen", base:0, odd:.7, sort: i => n(i) / td(i, 10) / bd(i, [6, 8]), shield:"hessen"},
{name:"Angshire", base:1, odd:1, sort: i => n(i) / td(i, 10) / sf(i), shield:"heater"},
{name:"Luari", base:2, odd:.6, sort: i => n(i) / td(i, 12) / bd(i, [6, 8]), shield:"oldFrench"},
{name:"Tallian", base:3, odd:.6, sort: i => n(i) / td(i, 15), shield:"horsehead2"},
{name:"Astellian", base:4, odd:.6, sort: i => n(i) / td(i, 16), shield:"spanish"},
{name:"Slovan", base:5, odd:.7, sort: i => n(i) / td(i, 6) * t[i], shield:"round"},
{name:"Norse", base:6, odd:.7, sort: i => n(i) / td(i, 5), shield:"heater"},
{name:"Elladan", base:7, odd:.7, sort: i => n(i) / td(i, 18) * h[i], shield:"boeotian"},
{name:"Romian", base:8, odd:.7, sort: i => n(i) / td(i, 15), shield:"roman"},
{name:"Soumi", base:9, odd:.3, sort: i => n(i) / td(i, 5) / bd(i, [9]) * t[i], shield:"pavise"},
{name:"Koryo", base:10, odd:.1, sort: i => n(i) / td(i, 12) / t[i], shield:"round"},
{name:"Hantzu", base:11, odd:.1, sort: i => n(i) / td(i, 13), shield:"banner"},
{name:"Yamoto", base:12, odd:.1, sort: i => n(i) / td(i, 15) / t[i], shield:"round"},
{name:"Portuzian", base:13, odd:.4, sort: i => n(i) / td(i, 17) / sf(i), shield:"spanish"},
{name:"Nawatli", base:14, odd:.1, sort: i => h[i] / td(i, 18) / bd(i, [7]), shield:"square"},
{name:"Vengrian", base: 15, odd:.2, sort: i => n(i) / td(i, 11) / bd(i, [4]) * t[i], shield:"wedged"},
{name:"Turchian", base: 16, odd:.2, sort: i => n(i) / td(i, 13), shield:"round"},
{name:"Berberan", base: 17, odd:.1, sort: i => n(i) / td(i, 19) / bd(i, [1, 2, 3], 7) * t[i], shield:"round"},
{name:"Eurabic", base: 18, odd:.2, sort: i => n(i) / td(i, 26) / bd(i, [1, 2], 7) * t[i], shield:"round"},
{name:"Inuk", base: 19, odd:.05, sort: i => td(i, -1) / bd(i, [10, 11]) / sf(i), shield:"square"},
{name:"Euskati", base: 20, odd:.05, sort: i => n(i) / td(i, 15) * h[i], shield:"spanish"},
{name:"Yoruba", base: 21, odd:.05, sort: i => n(i) / td(i, 15) / bd(i, [5, 7]), shield:"vesicaPiscis"},
{name:"Keltan", base: 22, odd:.05, sort: i => n(i) / td(i, 11) / bd(i, [6, 8]) * t[i], shield:"vesicaPiscis"},
{name:"Efratic", base: 23, odd:.05, sort: i => n(i) / td(i, 22) * t[i], shield:"diamond"},
{name:"Tehrani", base: 24, odd:.1, sort: i => n(i) / td(i, 18) * h[i], shield:"round"},
{name:"Maui", base: 25, odd:.05, sort: i => n(i) / td(i, 24) / sf(i) / t[i], shield:"round"},
{name:"Carnatic", base: 26, odd:.05, sort: i => n(i) / td(i, 26), shield:"round"},
{name:"Inqan", base: 27, odd:.05, sort: i => h[i] / td(i, 13), shield:"square"},
{name:"Kiswaili", base: 28, odd:.1, sort: i => n(i) / td(i, 29) / bd(i, [1, 3, 5, 7]), shield:"vesicaPiscis"},
{name:"Vietic", base: 29, odd:.1, sort: i => n(i) / td(i, 25) / bd(i, [7], 7) / t[i], shield:"banner"},
{name:"Guantzu", base:30, odd:.1, sort: i => n(i) / td(i, 17), shield:"banner"},
{name:"Ulus", base:31, odd:.1, sort: i => n(i) / td(i, 5) / bd(i, [2, 4, 10], 7) * t[i], shield:"banner"}
{name: "Shwazen", base: 0, odd: 0.7, sort: i => n(i) / td(i, 10) / bd(i, [6, 8]), shield: "hessen"},
{name: "Angshire", base: 1, odd: 1, sort: i => n(i) / td(i, 10) / sf(i), shield: "heater"},
{name: "Luari", base: 2, odd: 0.6, sort: i => n(i) / td(i, 12) / bd(i, [6, 8]), shield: "oldFrench"},
{name: "Tallian", base: 3, odd: 0.6, sort: i => n(i) / td(i, 15), shield: "horsehead2"},
{name: "Astellian", base: 4, odd: 0.6, sort: i => n(i) / td(i, 16), shield: "spanish"},
{name: "Slovan", base: 5, odd: 0.7, sort: i => (n(i) / td(i, 6)) * t[i], shield: "round"},
{name: "Norse", base: 6, odd: 0.7, sort: i => n(i) / td(i, 5), shield: "heater"},
{name: "Elladan", base: 7, odd: 0.7, sort: i => (n(i) / td(i, 18)) * h[i], shield: "boeotian"},
{name: "Romian", base: 8, odd: 0.7, sort: i => n(i) / td(i, 15), shield: "roman"},
{name: "Soumi", base: 9, odd: 0.3, sort: i => (n(i) / td(i, 5) / bd(i, [9])) * t[i], shield: "pavise"},
{name: "Koryo", base: 10, odd: 0.1, sort: i => n(i) / td(i, 12) / t[i], shield: "round"},
{name: "Hantzu", base: 11, odd: 0.1, sort: i => n(i) / td(i, 13), shield: "banner"},
{name: "Yamoto", base: 12, odd: 0.1, sort: i => n(i) / td(i, 15) / t[i], shield: "round"},
{name: "Portuzian", base: 13, odd: 0.4, sort: i => n(i) / td(i, 17) / sf(i), shield: "spanish"},
{name: "Nawatli", base: 14, odd: 0.1, sort: i => h[i] / td(i, 18) / bd(i, [7]), shield: "square"},
{name: "Vengrian", base: 15, odd: 0.2, sort: i => (n(i) / td(i, 11) / bd(i, [4])) * t[i], shield: "wedged"},
{name: "Turchian", base: 16, odd: 0.2, sort: i => n(i) / td(i, 13), shield: "round"},
{name: "Berberan", base: 17, odd: 0.1, sort: i => (n(i) / td(i, 19) / bd(i, [1, 2, 3], 7)) * t[i], shield: "round"},
{name: "Eurabic", base: 18, odd: 0.2, sort: i => (n(i) / td(i, 26) / bd(i, [1, 2], 7)) * t[i], shield: "round"},
{name: "Inuk", base: 19, odd: 0.05, sort: i => td(i, -1) / bd(i, [10, 11]) / sf(i), shield: "square"},
{name: "Euskati", base: 20, odd: 0.05, sort: i => (n(i) / td(i, 15)) * h[i], shield: "spanish"},
{name: "Yoruba", base: 21, odd: 0.05, sort: i => n(i) / td(i, 15) / bd(i, [5, 7]), shield: "vesicaPiscis"},
{name: "Keltan", base: 22, odd: 0.05, sort: i => (n(i) / td(i, 11) / bd(i, [6, 8])) * t[i], shield: "vesicaPiscis"},
{name: "Efratic", base: 23, odd: 0.05, sort: i => (n(i) / td(i, 22)) * t[i], shield: "diamond"},
{name: "Tehrani", base: 24, odd: 0.1, sort: i => (n(i) / td(i, 18)) * h[i], shield: "round"},
{name: "Maui", base: 25, odd: 0.05, sort: i => n(i) / td(i, 24) / sf(i) / t[i], shield: "round"},
{name: "Carnatic", base: 26, odd: 0.05, sort: i => n(i) / td(i, 26), shield: "round"},
{name: "Inqan", base: 27, odd: 0.05, sort: i => h[i] / td(i, 13), shield: "square"},
{name: "Kiswaili", base: 28, odd: 0.1, sort: i => n(i) / td(i, 29) / bd(i, [1, 3, 5, 7]), shield: "vesicaPiscis"},
{name: "Vietic", base: 29, odd: 0.1, sort: i => n(i) / td(i, 25) / bd(i, [7], 7) / t[i], shield: "banner"},
{name: "Guantzu", base: 30, odd: 0.1, sort: i => n(i) / td(i, 17), shield: "banner"},
{name: "Ulus", base: 31, odd: 0.1, sort: i => (n(i) / td(i, 5) / bd(i, [2, 4, 10], 7)) * t[i], shield: "banner"}
];
}
};
// expand cultures across the map (Dijkstra-like algorithm)
const expand = function() {
TIME && console.time('expandCultures');
const expand = function () {
TIME && console.time("expandCultures");
cells = pack.cells;
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
pack.cultures.forEach(function(c) {
pack.cultures.forEach(function (c) {
if (!c.i || c.removed) return;
queue.queue({e:c.center, p:0, c:c.i});
queue.queue({e: c.center, p: 0, c: c.i});
});
const neutral = cells.i.length / 5000 * 3000 * neutralInput.value; // limit cost for culture growth
const neutral = (cells.i.length / 5000) * 3000 * neutralInput.value; // limit cost for culture growth
const cost = [];
while (queue.length) {
const next = queue.dequeue(), n = next.e, p = next.p, c = next.c;
const next = queue.dequeue(),
n = next.e,
p = next.p,
c = next.c;
const type = pack.cultures[c].type;
cells.c[n].forEach(function(e) {
cells.c[n].forEach(function (e) {
const biome = cells.biome[e];
const biomeCost = getBiomeCost(c, biome, type);
const biomeChangeCost = biome === cells.biome[n] ? 0 : 20; // penalty on biome change
@ -375,13 +402,13 @@
if (!cost[e] || totalCost < cost[e]) {
if (cells.s[e] > 0) cells.culture[e] = c; // assign culture to populated cell
cost[e] = totalCost;
queue.queue({e, p:totalCost, c});
queue.queue({e, p: totalCost, c});
}
});
}
TIME && console.timeEnd('expandCultures');
}
TIME && console.timeEnd("expandCultures");
};
function getBiomeCost(c, biome, type) {
if (cells.biome[pack.cultures[c].center] === biome) return 10; // tiny penalty for native biome
@ -391,7 +418,8 @@
}
function getHeightCost(i, h, type) {
const f = pack.features[cells.f[i]], a = cells.area[i];
const f = pack.features[cells.f[i]],
a = cells.area[i];
if (type === "Lake" && f.type === "lake") return 10; // no lake crossing penalty for Lake cultures
if (type === "Naval" && h < 20) return a * 2; // low sea/lake crossing penalty for Naval cultures
if (type === "Nomadic" && h < 20) return a * 50; // giant sea/lake crossing penalty for Nomads
@ -407,21 +435,20 @@
function getRiverCost(r, i, type) {
if (type === "River") return r ? 0 : 100; // penalty for river cultures
if (!r) return 0; // no penalty for others if there is no river
return Math.min(Math.max(cells.fl[i] / 10, 20), 100) // river penalty from 20 to 100 based on flux
return Math.min(Math.max(cells.fl[i] / 10, 20), 100); // river penalty from 20 to 100 based on flux
}
function getTypeCost(t, type) {
if (t === 1) return type === "Naval" || type === "Lake" ? 0 : type === "Nomadic" ? 60 : 20; // penalty for coastline
if (t === 2) return type === "Naval" || type === "Nomadic" ? 30 : 0; // low penalty for land level 2 for Navals and nomads
if (t !== -1) return type === "Naval" || type === "Lake" ? 100 : 0; // penalty for mainland for navals
if (t !== -1) return type === "Naval" || type === "Lake" ? 100 : 0; // penalty for mainland for navals
return 0;
}
const getRandomShield = function() {
const getRandomShield = function () {
const type = rw(COA.shields.types);
return rw(COA.shields[type]);
}
};
return {generate, add, expand, getDefault, getRandomShield};
})));
})();

141
modules/fonts.js Normal file
View file

@ -0,0 +1,141 @@
// helper finctions to work with fonts
async function addFonts(url) {
$("head").append('<link rel="stylesheet" type="text/css" href="' + url + '">');
try {
const resp = await fetch(url);
const text = await resp.text();
let s = document.createElement("style");
s.innerHTML = text;
document.head.appendChild(s);
let styleSheet = Array.prototype.filter.call(document.styleSheets, sS => sS.ownerNode === s)[0];
let FontRule = rule_1 => {
let family = rule_1.style.getPropertyValue("font-family");
let font = family.replace(/['"]+/g, "").replace(/ /g, "+");
let weight = rule_1.style.getPropertyValue("font-weight");
if (weight && weight !== "400") font += ":" + weight;
if (fonts.indexOf(font) == -1) {
fonts.push(font);
fetched++;
}
};
let fetched = 0;
for (let r of styleSheet.cssRules) {
FontRule(r);
}
document.head.removeChild(s);
return fetched;
} catch (err) {
return ERROR && console.error(err);
}
}
function loadUsedFonts() {
const fontsInUse = getFontsList(svg);
const fontsToLoad = fontsInUse.filter(font => !fonts.includes(font));
if (fontsToLoad?.length) {
const url = "https://fonts.googleapis.com/css?family=" + fontsToLoad.join("|");
addFonts(url);
}
}
function getFontsList(svg) {
const fontsInUse = [];
svg.selectAll("#labels > g").each(function () {
if (!this.hasChildNodes()) return;
const font = this.dataset.font;
if (font) fontsInUse.push(font);
});
if (legend?.node()?.hasChildNodes()) fontsInUse.push(legend.attr("data-font"));
return [...new Set(fontsInUse)];
}
// code from Kaiido's answer https://stackoverflow.com/questions/42402584/how-to-use-google-fonts-in-canvas-when-drawing-dom-objects-in-svg
function GFontToDataURI(url) {
if (!url) return Promise.resolve();
return fetch(url) // first fecth the embed stylesheet page
.then(resp => resp.text()) // we only need the text of it
.then(text => {
let s = document.createElement("style");
s.innerHTML = text;
document.head.appendChild(s);
const styleSheet = Array.prototype.filter.call(document.styleSheets, sS => sS.ownerNode === s)[0];
const FontRule = rule => {
const src = rule.style.getPropertyValue("src");
const url = src ? src.split("url(")[1].split(")")[0] : "";
return {rule, src, url: url.substring(url.length - 1, 1)};
};
const fontProms = [];
for (const r of styleSheet.cssRules) {
let fR = FontRule(r);
if (!fR.url) continue;
fontProms.push(
fetch(fR.url) // fetch the actual font-file (.woff)
.then(resp => resp.blob())
.then(blob => {
return new Promise(resolve => {
let f = new FileReader();
f.onload = e => resolve(f.result);
f.readAsDataURL(blob);
});
})
.then(dataURL => fR.rule.cssText.replace(fR.url, dataURL))
);
}
document.head.removeChild(s); // clean up
return Promise.all(fontProms); // wait for all this has been done
});
}
// fetch default fonts if not done before
function loadDefaultFonts() {
if (!$('link[href="fonts.css"]').length) {
$("head").append('<link rel="stylesheet" type="text/css" href="fonts.css">');
const fontsToAdd = ["Amatic+SC:700", "IM+Fell+English", "Great+Vibes", "MedievalSharp", "Metamorphous", "Nova+Script", "Uncial+Antiqua", "Underdog", "Caesar+Dressing", "Bitter", "Yellowtail", "Montez", "Shadows+Into+Light", "Fredericka+the+Great", "Orbitron", "Dancing+Script:700", "Architects+Daughter", "Kaushan+Script", "Gloria+Hallelujah", "Satisfy", "Comfortaa:700", "Cinzel"];
fontsToAdd.forEach(function (f) {
if (fonts.indexOf(f) === -1) fonts.push(f);
});
updateFontOptions();
}
}
function fetchFonts(url) {
return new Promise((resolve, reject) => {
if (url === "") return tip("Use a direct link to any @font-face declaration or just font name to fetch from Google Fonts");
if (url.indexOf("http") === -1) {
url = url.replace(url.charAt(0), url.charAt(0).toUpperCase()).split(" ").join("+");
url = "https://fonts.googleapis.com/css?family=" + url;
}
addFonts(url).then(fetched => {
if (fetched === undefined) return tip("Cannot fetch font for this value!", false, "error");
if (fetched === 0) return tip("Already in the fonts list!", false, "error");
updateFontOptions();
if (fetched === 1) {
tip("Font " + fonts[fonts.length - 1] + " is fetched");
} else if (fetched > 1) {
tip(fetched + " fonts are added to the list");
}
resolve(fetched);
});
});
}
// Update font list for Label and Burg Editors
function updateFontOptions() {
styleSelectFont.innerHTML = "";
for (let i = 0; i < fonts.length; i++) {
const opt = document.createElement("option");
opt.value = i;
const font = fonts[i].split(":")[0].replace(/\+/g, " ");
opt.style.fontFamily = opt.innerHTML = font;
styleSelectFont.add(opt);
}
}

View file

@ -1,8 +1,6 @@
(function (global, factory) {
typeof exports === "object" && typeof module !== "undefined" ? (module.exports = factory()) : typeof define === "function" && define.amd ? define(factory) : (global.HeightmapGenerator = factory());
})(this, function () {
"use strict";
"use strict";
window.HeightmapGenerator = (function () {
let cells, p;
const generate = function () {
@ -12,262 +10,63 @@
cells.h = new Uint8Array(grid.points.length);
const template = document.getElementById("templateInput").value;
switch (template) {
case "Volcano":
templateVolcano();
break;
case "High Island":
templateHighIsland();
break;
case "Low Island":
templateLowIsland();
break;
case "Continents":
templateContinents();
break;
case "Archipelago":
templateArchipelago();
break;
case "Atoll":
templateAtoll();
break;
case "Mediterranean":
templateMediterranean();
break;
case "Peninsula":
templatePeninsula();
break;
case "Pangea":
templatePangea();
break;
case "Isthmus":
templateIsthmus();
break;
case "Shattered":
templateShattered();
break;
const templateString = HeightmapTemplates[template];
const steps = templateString.split("\n");
if (!steps.length) throw new Error(`Heightmap template: no steps. Template: ${template}. Steps: ${steps}`);
for (const step of steps) {
const elements = step.trim().split(" ");
if (elements.length < 2) throw new Error(`Heightmap template: steps < 2. Template: ${template}. Step: ${elements}`);
addStep(...elements);
}
TIME && console.timeEnd("generateHeightmap");
};
// parse template step
function addStep(a1, a2, a3, a4, a5) {
if (a1 === "Hill") return addHill(a2, a3, a4, a5);
if (a1 === "Pit") return addPit(a2, a3, a4, a5);
if (a1 === "Range") return addRange(a2, a3, a4, a5);
if (a1 === "Trough") return addTrough(a2, a3, a4, a5);
if (a1 === "Strait") return addStrait(a2, a3);
if (a1 === "Add") return modify(a3, a2, 1);
if (a1 === "Multiply") return modify(a3, 0, a2);
if (a1 === "Add") return modify(a3, +a2, 1);
if (a1 === "Multiply") return modify(a3, 0, +a2);
if (a1 === "Smooth") return smooth(a2);
}
// Heighmap Template: Volcano
function templateVolcano() {
addStep("Hill", "1", "90-100", "44-56", "40-60");
addStep("Multiply", 0.8, "50-100");
addStep("Range", "1.5", "30-55", "45-55", "40-60");
addStep("Smooth", 2);
addStep("Hill", "1.5", "25-35", "25-30", "20-75");
addStep("Hill", "1", "25-35", "75-80", "25-75");
addStep("Hill", "0.5", "20-25", "10-15", "20-25");
}
// Heighmap Template: High Island
function templateHighIsland() {
addStep("Hill", "1", "90-100", "65-75", "47-53");
addStep("Add", 5, "all");
addStep("Hill", "6", "20-23", "25-55", "45-55");
addStep("Range", "1", "40-50", "45-55", "45-55");
addStep("Smooth", 2);
addStep("Trough", "2-3", "20-30", "20-30", "20-30");
addStep("Trough", "2-3", "20-30", "60-80", "70-80");
addStep("Hill", "1", "10-15", "60-60", "50-50");
addStep("Hill", "1.5", "13-16", "15-20", "20-75");
addStep("Multiply", 0.8, "20-100");
addStep("Range", "1.5", "30-40", "15-85", "30-40");
addStep("Range", "1.5", "30-40", "15-85", "60-70");
addStep("Pit", "2-3", "10-15", "15-85", "20-80");
}
// Heighmap Template: Low Island
function templateLowIsland() {
addStep("Hill", "1", "90-99", "60-80", "45-55");
addStep("Hill", "4-5", "25-35", "20-65", "40-60");
addStep("Range", "1", "40-50", "45-55", "45-55");
addStep("Smooth", 3);
addStep("Trough", "1.5", "20-30", "15-85", "20-30");
addStep("Trough", "1.5", "20-30", "15-85", "70-80");
addStep("Hill", "1.5", "10-15", "5-15", "20-80");
addStep("Hill", "1", "10-15", "85-95", "70-80");
addStep("Pit", "3-5", "10-15", "15-85", "20-80");
addStep("Multiply", 0.4, "20-100");
}
// Heighmap Template: Continents
function templateContinents() {
addStep("Hill", "1", "80-85", "75-80", "40-60");
addStep("Hill", "1", "80-85", "20-25", "40-60");
addStep("Multiply", 0.22, "20-100");
addStep("Hill", "5-6", "15-20", "25-75", "20-82");
addStep("Range", ".8", "30-60", "5-15", "20-45");
addStep("Range", ".8", "30-60", "5-15", "55-80");
addStep("Range", "0-3", "30-60", "80-90", "20-80");
addStep("Trough", "3-4", "15-20", "15-85", "20-80");
addStep("Strait", "2", "vertical");
addStep("Smooth", 2);
addStep("Trough", "1-2", "5-10", "45-55", "45-55");
addStep("Pit", "3-4", "10-15", "15-85", "20-80");
addStep("Hill", "1", "5-10", "40-60", "40-60");
}
// Heighmap Template: Archipelago
function templateArchipelago() {
addStep("Add", 11, "all");
addStep("Range", "2-3", "40-60", "20-80", "20-80");
addStep("Hill", "5", "15-20", "10-90", "30-70");
addStep("Hill", "2", "10-15", "10-30", "20-80");
addStep("Hill", "2", "10-15", "60-90", "20-80");
addStep("Smooth", 3);
addStep("Trough", "10", "20-30", "5-95", "5-95");
addStep("Strait", "2", "vertical");
addStep("Strait", "2", "horizontal");
}
// Heighmap Template: Atoll
function templateAtoll() {
addStep("Hill", "1", "75-80", "50-60", "45-55");
addStep("Hill", "1.5", "30-50", "25-75", "30-70");
addStep("Hill", ".5", "30-50", "25-35", "30-70");
addStep("Smooth", 1);
addStep("Multiply", 0.2, "25-100");
addStep("Hill", ".5", "10-20", "50-55", "48-52");
}
// Heighmap Template: Mediterranean
function templateMediterranean() {
addStep("Range", "3-4", "30-50", "0-100", "0-10");
addStep("Range", "3-4", "30-50", "0-100", "90-100");
addStep("Hill", "5-6", "30-70", "0-100", "0-5");
addStep("Hill", "5-6", "30-70", "0-100", "95-100");
addStep("Smooth", 1);
addStep("Hill", "2-3", "30-70", "0-5", "20-80");
addStep("Hill", "2-3", "30-70", "95-100", "20-80");
addStep("Multiply", 0.8, "land");
addStep("Trough", "3-5", "40-50", "0-100", "0-10");
addStep("Trough", "3-5", "40-50", "0-100", "90-100");
}
// Heighmap Template: Peninsula
function templatePeninsula() {
addStep("Range", "2-3", "20-35", "40-50", "0-15");
addStep("Add", 5, "all");
addStep("Hill", "1", "90-100", "10-90", "0-5");
addStep("Add", 13, "all");
addStep("Hill", "3-4", "3-5", "5-95", "80-100");
addStep("Hill", "1-2", "3-5", "5-95", "40-60");
addStep("Trough", "5-6", "10-25", "5-95", "5-95");
addStep("Smooth", 3);
}
// Heighmap Template: Pangea
function templatePangea() {
addStep("Hill", "1-2", "25-40", "15-50", "0-10");
addStep("Hill", "1-2", "5-40", "50-85", "0-10");
addStep("Hill", "1-2", "25-40", "50-85", "90-100");
addStep("Hill", "1-2", "5-40", "15-50", "90-100");
addStep("Hill", "8-12", "20-40", "20-80", "48-52");
addStep("Smooth", 2);
addStep("Multiply", 0.7, "land");
addStep("Trough", "3-4", "25-35", "5-95", "10-20");
addStep("Trough", "3-4", "25-35", "5-95", "80-90");
addStep("Range", "5-6", "30-40", "10-90", "35-65");
}
// Heighmap Template: Isthmus
function templateIsthmus() {
addStep("Hill", "5-10", "15-30", "0-30", "0-20");
addStep("Hill", "5-10", "15-30", "10-50", "20-40");
addStep("Hill", "5-10", "15-30", "30-70", "40-60");
addStep("Hill", "5-10", "15-30", "50-90", "60-80");
addStep("Hill", "5-10", "15-30", "70-100", "80-100");
addStep("Smooth", 2);
addStep("Trough", "4-8", "15-30", "0-30", "0-20");
addStep("Trough", "4-8", "15-30", "10-50", "20-40");
addStep("Trough", "4-8", "15-30", "30-70", "40-60");
addStep("Trough", "4-8", "15-30", "50-90", "60-80");
addStep("Trough", "4-8", "15-30", "70-100", "80-100");
}
// Heighmap Template: Shattered
function templateShattered() {
addStep("Hill", "8", "35-40", "15-85", "30-70");
addStep("Trough", "10-20", "40-50", "5-95", "5-95");
addStep("Range", "5-7", "30-40", "10-90", "20-80");
addStep("Pit", "12-20", "30-40", "15-85", "20-80");
}
function getBlobPower() {
switch (+pointsInput.dataset.cells) {
case 1000:
return 0.93;
case 2000:
return 0.95;
case 5000:
return 0.96;
case 10000:
return 0.98;
case 20000:
return 0.985;
case 30000:
return 0.987;
case 40000:
return 0.9892;
case 50000:
return 0.9911;
case 60000:
return 0.9921;
case 70000:
return 0.9934;
case 80000:
return 0.9942;
case 90000:
return 0.9946;
case 100000:
return 0.995;
}
const cells = +pointsInput.dataset.cells;
if (cells === 1000) return 0.93;
if (cells === 2000) return 0.95;
if (cells === 5000) return 0.96;
if (cells === 10000) return 0.98;
if (cells === 20000) return 0.985;
if (cells === 30000) return 0.987;
if (cells === 40000) return 0.9892;
if (cells === 50000) return 0.9911;
if (cells === 60000) return 0.9921;
if (cells === 70000) return 0.9934;
if (cells === 80000) return 0.9942;
if (cells === 90000) return 0.9946;
if (cells === 100000) return 0.995;
}
function getLinePower() {
switch (+pointsInput.dataset.cells) {
case 1000:
return 0.74;
case 2000:
return 0.75;
case 5000:
return 0.78;
case 10000:
return 0.81;
case 20000:
return 0.82;
case 30000:
return 0.83;
case 40000:
return 0.84;
case 50000:
return 0.855;
case 60000:
return 0.87;
case 70000:
return 0.885;
case 80000:
return 0.91;
case 90000:
return 0.92;
case 100000:
return 0.93;
}
const cells = +pointsInput.dataset.cells;
if (cells === 1000) return 0.74;
if (cells === 2000) return 0.75;
if (cells === 5000) return 0.78;
if (cells === 10000) return 0.81;
if (cells === 20000) return 0.82;
if (cells === 30000) return 0.83;
if (cells === 40000) return 0.84;
if (cells === 50000) return 0.855;
if (cells === 60000) return 0.87;
if (cells === 70000) return 0.885;
if (cells === 80000) return 0.91;
if (cells === 90000) return 0.92;
if (cells === 100000) return 0.93;
}
const addHill = function (count, height, rangeX, rangeY) {
@ -422,7 +221,6 @@
if (d % 6 !== 0) return;
for (const l of d3.range(i)) {
const min = cells.c[cur][d3.scan(cells.c[cur], (a, b) => cells.h[a] - cells.h[b])]; // downhill cell
//debug.append("circle").attr("cx", p[min][0]).attr("cy", p[min][1]).attr("r", 1);
cells.h[min] = (cells.h[cur] * 2 + cells.h[min]) / 3;
cur = min;
}
@ -611,4 +409,4 @@
}
return {generate, addHill, addRange, addTrough, addStrait, addPit, smooth, modify};
});
})();

View file

@ -0,0 +1,126 @@
"use strict";
window.HeightmapTemplates = (function () {
const volcano = `Hill 1 90-100 44-56 40-60
Multiply 0.8 50-100 0 0
Range 1.5 30-55 45-55 40-60
Smooth 2 0 0 0
Hill 1.5 25-35 25-30 20-75
Hill 1 25-35 75-80 25-75
Hill 0.5 20-25 10-15 20-25`;
const highIsland = `Hill 1 90-100 65-75 47-53
Add 5 all 0 0
Hill 6 20-23 25-55 45-55
Range 1 40-50 45-55 45-55
Smooth 2 0 0 0
Trough 2-3 20-30 20-30 20-30
Trough 2-3 20-30 60-80 70-80
Hill 1 10-15 60-60 50-50
Hill 1.5 13-16 15-20 20-75
Multiply 0.8 20-100 0 0
Range 1.5 30-40 15-85 30-40
Range 1.5 30-40 15-85 60-70
Pit 2-3 10-15 15-85 20-80`;
const lowIsland = `Hill 1 90-99 60-80 45-55
Hill 4-5 25-35 20-65 40-60
Range 1 40-50 45-55 45-55
Smooth 3 0 0 0
Trough 1.5 20-30 15-85 20-30
Trough 1.5 20-30 15-85 70-80
Hill 1.5 10-15 5-15 20-80
Hill 1 10-15 85-95 70-80
Pit 3-5 10-15 15-85 20-80
Multiply 0.4 20-100 0 0`;
const continents = `Hill 1 80-85 75-80 40-60
Hill 1 80-85 20-25 40-60
Multiply 0.22 20-100 0 0
Hill 5-6 15-20 25-75 20-82
Range .8 30-60 5-15 20-45
Range .8 30-60 5-15 55-80
Range 0-3 30-60 80-90 20-80
Trough 3-4 15-20 15-85 20-80
Strait 2 vertical 0 0
Smooth 2 0 0 0
Trough 1-2 5-10 45-55 45-55
Pit 3-4 10-15 15-85 20-80
Hill 1 5-10 40-60 40-60`;
const archipelago = `Add 11 all 0 0
Range 2-3 40-60 20-80 20-80
Hill 5 15-20 10-90 30-70
Hill 2 10-15 10-30 20-80
Hill 2 10-15 60-90 20-80
Smooth 3 0 0 0
Trough 10 20-30 5-95 5-95
Strait 2 vertical 0 0
Strait 2 horizontal 0 0`;
const atoll = `Add 11 all 0 0
Range 2-3 40-60 20-80 20-80
Hill 5 15-20 10-90 30-70
Hill 2 10-15 10-30 20-80
Hill 2 10-15 60-90 20-80
Smooth 3 0 0 0
Trough 10 20-30 5-95 5-95
Strait 2 vertical 0 0
Strait 2 horizontal 0 0`;
const mediterranean = `Range 3-4 30-50 0-100 0-10
Range 3-4 30-50 0-100 90-100
Hill 5-6 30-70 0-100 0-5
Hill 5-6 30-70 0-100 95-100
Smooth 1 0 0 0
Hill 2-3 30-70 0-5 20-80
Hill 2-3 30-70 95-100 20-80
Multiply 0.8 land 0 0
Trough 3-5 40-50 0-100 0-10
Trough 3-5 40-50 0-100 90-100`;
const peninsula = `Range 2-3 20-35 40-50 0-15
Add 5 all 0 0
Hill 1 90-100 10-90 0-5
Add 13 all 0 0
Hill 3-4 3-5 5-95 80-100
Hill 1-2 3-5 5-95 40-60
Trough 5-6 10-25 5-95 5-95
Smooth 3 0 0 0`;
const pangea = `Hill 1-2 25-40 15-50 0-10
Hill 1-2 5-40 50-85 0-10
Hill 1-2 25-40 50-85 90-100
Hill 1-2 5-40 15-50 90-100
Hill 8-12 20-40 20-80 48-52
Smooth 2 0 0 0
Multiply 0.7 land 0 0
Trough 3-4 25-35 5-95 10-20
Trough 3-4 25-35 5-95 80-90
Range 5-6 30-40 10-90 35-65`;
const isthmus = `Hill 5-10 15-30 0-30 0-20
Hill 5-10 15-30 10-50 20-40
Hill 5-10 15-30 30-70 40-60
Hill 5-10 15-30 50-90 60-80
Hill 5-10 15-30 70-100 80-100
Smooth 2 0 0 0
Trough 4-8 15-30 0-30 0-20
Trough 4-8 15-30 10-50 20-40
Trough 4-8 15-30 30-70 40-60
Trough 4-8 15-30 50-90 60-80
Trough 4-8 15-30 70-100 80-100`;
const shattered = `Hill 8 35-40 15-85 30-70
Trough 10-20 40-50 5-95 5-95
Range 5-7 30-40 10-90 20-80
Pit 12-20 30-40 15-85 20-80`;
const taklamakan = `Hill 1-3 20-30 30-70 30-70
Hill 2-4 60-85 0-5 0-100
Hill 2-4 60-85 95-100 0-100
Hill 3-4 60-85 20-80 0-5
Hill 3-4 60-85 20-80 95-100`;
return {volcano, highIsland, lowIsland, continents, archipelago, atoll, mediterranean, peninsula, peninsula, pangea, isthmus, shattered, taklamakan};
})();

View file

@ -1,8 +1,6 @@
(function (global, factory) {
typeof exports === "object" && typeof module !== "undefined" ? (module.exports = factory()) : typeof define === "function" && define.amd ? define(factory) : (global.Lakes = factory());
})(this, function () {
"use strict";
"use strict";
window.Lakes = (function () {
const setClimateData = function (h) {
const cells = pack.cells;
const lakeOutCells = new Uint16Array(cells.i.length);
@ -10,8 +8,8 @@
pack.features.forEach(f => {
if (f.type !== "lake") return;
// default flux: sum of precipition around lake first cell
f.flux = rn(d3.sum(f.shoreline.map(c => grid.cells.prec[cells.g[c]])) / 2);
// default flux: sum of precipitation around lake
f.flux = f.shoreline.reduce((acc, c) => acc + grid.cells.prec[cells.g[c]], 0);
// temperature and evaporation to detect closed lakes
f.temp = f.cells < 6 ? grid.cells.temp[cells.g[f.firstCell]] : rn(d3.mean(f.shoreline.map(c => grid.cells.temp[cells.g[c]])), 1);
@ -96,7 +94,6 @@
if (feature.type !== "lake") continue;
delete feature.river;
delete feature.enteringFlux;
delete feature.shoreline;
delete feature.outCell;
delete feature.closed;
feature.height = rn(feature.height, 3);
@ -140,7 +137,7 @@
if (feature.height > 60 && feature.cells < 10 && feature.firstCell % 10 === 0) return "lava";
if (!feature.inlets && !feature.outlet) {
if (feature.evaporation / 2 > feature.flux) return "dry";
if (feature.evaporation > feature.flux * 4) return "dry";
if (feature.cells < 3 && feature.firstCell % 10 === 0) return "sinkhole";
}
@ -150,4 +147,4 @@
}
return {setClimateData, cleanupLakeData, prepareLakeData, defineGroup, generateName, getName, getShoreline};
});
})();

View file

@ -141,6 +141,8 @@ function parseLoadedData(data) {
if (settings[19]) options = JSON.parse(settings[19]);
if (settings[20]) mapName.value = settings[20];
if (settings[21]) hideLabels.checked = +settings[21];
if (settings[22]) stylePreset.value = settings[22];
if (settings[23]) rescaleLabels.checked = settings[23];
})();
void (function parseConfiguration() {
@ -220,6 +222,8 @@ function parseLoadedData(data) {
burgLabels = labels.select('#burgLabels');
})();
loadUsedFonts();
void (function parseGridData() {
grid = JSON.parse(data[6]);
calculateVoronoi(grid, grid.points);
@ -270,12 +274,13 @@ function parseLoadedData(data) {
}
})();
const notHidden = (selection) => selection.node() && selection.style('display') !== 'none';
const hasChildren = (selection) => selection.node()?.hasChildNodes();
const hasChild = (selection, selector) => selection.node()?.querySelector(selector);
const turnOn = (el) => document.getElementById(el).classList.remove('buttonoff');
void (function restoreLayersState() {
// helper functions
const notHidden = (selection) => selection.node() && selection.style('display') !== 'none';
const hasChildren = (selection) => selection.node()?.hasChildNodes();
const hasChild = (selection, selector) => selection.node()?.querySelector(selector);
const turnOn = (el) => document.getElementById(el).classList.remove('buttonoff');
// turn all layers off
document
.getElementById('mapLayers')
@ -290,7 +295,7 @@ function parseLoadedData(data) {
if (hasChildren(gridOverlay)) turnOn('toggleGrid');
if (hasChildren(coordinates)) turnOn('toggleCoordinates');
if (notHidden(compass) && hasChild(compass, 'use')) turnOn('toggleCompass');
if (notHidden(rivers)) turnOn('toggleRivers');
if (hasChildren(rivers)) turnOn('toggleRivers');
if (notHidden(terrain) && hasChildren(terrain)) turnOn('toggleRelief');
if (hasChildren(relig)) turnOn('toggleReligions');
if (hasChildren(cults)) turnOn('toggleCultures');
@ -303,7 +308,6 @@ function parseLoadedData(data) {
if (hasChild(population, 'line')) turnOn('togglePopulation');
if (hasChildren(ice)) turnOn('toggleIce');
if (hasChild(prec, 'circle')) turnOn('togglePrec');
if (hasChildren(goods)) turnOn('toggleResources');
if (notHidden(emblems) && hasChild(emblems, 'use')) turnOn('toggleEmblems');
if (notHidden(labels)) turnOn('toggleLabels');
if (notHidden(icons)) turnOn('toggleIcons');
@ -694,7 +698,7 @@ function parseLoadedData(data) {
}
if (version < 1.63) {
// v.1.63 change ocean pattern opacity element
// v.1.63 changed ocean pattern opacity element
const oceanPattern = document.getElementById('oceanPattern');
if (oceanPattern) oceanPattern.removeAttribute('opacity');
const oceanicPattern = document.getElementById('oceanicPattern');
@ -713,6 +717,50 @@ function parseLoadedData(data) {
defs.append('g').attr('id', 'defs-icons');
Resources.generate();
}
if (version < 1.64) {
// v.1.64 change states style
const opacity = regions.attr('opacity');
const filter = regions.attr('filter');
statesBody.attr('opacity', opacity).attr('filter', filter);
statesHalo.attr('opacity', opacity).attr('filter', 'blur(5px)');
regions.attr('opacity', null).attr('filter', null);
}
if (version < 1.65) {
// v 1.65 changed rivers data
rivers.attr('style', null); // remove style to unhide layer
for (const river of pack.rivers) {
const node = document.getElementById('river' + river.i);
if (node && !river.cells) {
const riverCells = new Set();
const length = node.getTotalLength() / 2;
const segments = Math.ceil(length / 6);
const increment = length / segments;
for (let i = increment * segments, c = i; i >= 0; i -= increment, c += increment) {
const p1 = node.getPointAtLength(i);
const p2 = node.getPointAtLength(c);
const x = (p1.x + p2.x) / 2;
const y = (p1.y + p2.y) / 2;
const cell = findCell(x, y, 6);
if (cell) riverCells.add(cell);
}
river.cells = Array.from(riverCells);
}
pack.cells.i.forEach((i) => {
if (pack.cells.r[i] && pack.cells.h[i] < 20) pack.cells.r[i] = 0;
});
}
}
if (version < 1.652) {
// remove style to unhide layers
rivers.attr('style', null);
borders.attr('style', null);
}
})();
void (function checkDataIntegrity() {
@ -812,6 +860,7 @@ function parseLoadedData(data) {
// set options
yearInput.value = options.year;
eraInput.value = options.era;
shapeRendering.value = viewbox.attr('shape-rendering') || 'geometricPrecision';
if (window.restoreDefaultEvents) restoreDefaultEvents();
focusOn(); // based on searchParams focus on point, cell or burg

View file

@ -1,8 +1,6 @@
(function (global, factory) {
typeof exports === "object" && typeof module !== "undefined" ? (module.exports = factory()) : typeof define === "function" && define.amd ? define(factory) : (global.Military = factory());
})(this, function () {
"use strict";
"use strict";
window.Military = (function () {
const generate = function () {
TIME && console.time("generateMilitaryForces");
const cells = pack.cells,
@ -371,4 +369,4 @@
};
return {generate, getDefaultOptions, getName, generateNote, drawRegiments, drawRegiment, moveRegiment, getTotal, getEmblem};
});
})();

View file

@ -1,7 +1,6 @@
(function (global, factory) {
typeof exports === "object" && typeof module !== "undefined" ? (module.exports = factory()) : typeof define === "function" && define.amd ? define(factory) : (global.Names = factory());
})(this, function () {
"use strict";
"use strict";
window.Names = (function () {
let chains = [];
// calculate Markov chain for a namesbase
@ -294,4 +293,4 @@
};
return {getBase, getCulture, getCultureShort, getBaseShort, getState, updateChain, clearChains, getNameBases, getMapName, calculateChain};
});
})();

View file

@ -1,8 +1,6 @@
(function (global, factory) {
typeof exports === "object" && typeof module !== "undefined" ? (module.exports = factory()) : typeof define === "function" && define.amd ? define(factory) : (global.OceanLayers = factory());
})(this, function () {
"use strict";
"use strict";
window.OceanLayers = (function () {
let cells, vertices, pointsN, used;
const OceanLayers = function OceanLayers() {
@ -91,4 +89,4 @@
}
return OceanLayers;
});
})();

View file

@ -1,41 +1,41 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.ReliefIcons = factory());
}(this, (function () {'use strict';
"use strict";
const ReliefIcons = function() {
TIME && console.time('drawRelief');
window.ReliefIcons = (function () {
const ReliefIcons = function () {
TIME && console.time("drawRelief");
terrain.selectAll("*").remove();
const density = terrain.attr("density") || .4;
const size = 1.6 * (terrain.attr("size") || 1);
const mod = .2 * size; // size modifier;s
const relief = []; // t: type, c: cell, x: centerX, y: centerY, s: size;
const cells = pack.cells;
const density = terrain.attr("density") || 0.4;
const size = 2 * (terrain.attr("size") || 1);
const mod = 0.2 * size; // size modifier
const relief = [];
for (const i of cells.i) {
const height = cells.h[i];
if (height < 20) continue; // no icons on water
if (cells.r[i]) continue; // no icons on rivers
const b = cells.biome[i];
if (height < 50 && biomesData.iconsDensity[b] === 0) continue; // no icons for this biome
const polygon = getPackPolygon(i);
const x = d3.extent(polygon, p => p[0]), y = d3.extent(polygon, p => p[1]);
const e = [Math.ceil(x[0]), Math.ceil(y[0]), Math.floor(x[1]), Math.floor(y[1])]; // polygon box
const biome = cells.biome[i];
if (height < 50 && biomesData.iconsDensity[biome] === 0) continue; // no icons for this biome
if (height < 50) placeBiomeIcons(i, b); else placeReliefIcons(i);
const polygon = getPackPolygon(i);
const [minX, maxX] = d3.extent(polygon, p => p[0]);
const [minY, maxY] = d3.extent(polygon, p => p[1]);
if (height < 50) placeBiomeIcons(i, biome);
else placeReliefIcons(i);
function placeBiomeIcons() {
const iconsDensity = biomesData.iconsDensity[b] / 100;
const iconsDensity = biomesData.iconsDensity[biome] / 100;
const radius = 2 / iconsDensity / density;
if (Math.random() > iconsDensity * 10) return;
for (const [cx, cy] of poissonDiscSampler(e[0], e[1], e[2], e[3], radius)) {
for (const [cx, cy] of poissonDiscSampler(minX, minY, maxX, maxY, radius)) {
if (!d3.polygonContains(polygon, [cx, cy])) continue;
let h = rn((4 + Math.random()) * size, 2);
const icon = getBiomeIcon(i, biomesData.icons[b]);
if (icon === "#relief-grass-1") h *= 1.3;
relief.push({i: icon, x: rn(cx-h, 2), y: rn(cy-h, 2), s: rn(h*2, 2)});
let h = (4 + Math.random()) * size;
const icon = getBiomeIcon(i, biomesData.icons[biome]);
if (icon === "#relief-grass-1") h *= 1.2;
relief.push({i: icon, x: rn(cx - h, 2), y: rn(cy - h, 2), s: rn(h * 2, 2)});
}
}
@ -43,9 +43,9 @@
const radius = 2 / density;
const [icon, h] = getReliefIcon(i, height);
for (const [cx, cy] of poissonDiscSampler(e[0], e[1], e[2], e[3], radius)) {
for (const [cx, cy] of poissonDiscSampler(minX, minY, maxX, maxY, radius)) {
if (!d3.polygonContains(polygon, [cx, cy])) continue;
relief.push({i: icon, x: rn(cx-h, 2), y: rn(cy-h, 2), s: rn(h*2, 2)});
relief.push({i: icon, x: rn(cx - h, 2), y: rn(cy - h, 2), s: rn(h * 2, 2)});
}
}
@ -58,17 +58,16 @@
}
// sort relief icons by y+size
relief.sort((a, b) => (a.y + a.s) - (b.y + b.s));
relief.sort((a, b) => a.y + a.s - (b.y + b.s));
// append relief icons at once using pure js
let reliefHTML = "";
for (const r of relief) {
reliefHTML += `<use href="${r.i}" x="${r.x}" y="${r.y}" width="${r.s}" height="${r.s}"/>`;
}
terrain.html(reliefHTML);
TIME && console.timeEnd('drawRelief');
}
TIME && console.timeEnd("drawRelief");
};
function getBiomeIcon(i, b) {
let type = b[Math.floor(Math.random() * b.length)];
@ -78,27 +77,42 @@
}
function getVariant(type) {
switch(type) {
case "mount": return rand(2,7);
case "mountSnow": return rand(1,6);
case "hill": return rand(2,5);
case "conifer": return 2;
case "coniferSnow": return 1;
case "swamp": return rand(2,3);
case "cactus": return rand(1,3);
case "deadTree": return rand(1,2);
default: return 2;
switch (type) {
case "mount":
return rand(2, 7);
case "mountSnow":
return rand(1, 6);
case "hill":
return rand(2, 5);
case "conifer":
return 2;
case "coniferSnow":
return 1;
case "swamp":
return rand(2, 3);
case "cactus":
return rand(1, 3);
case "deadTree":
return rand(1, 2);
default:
return 2;
}
}
function getOldIcon(type) {
switch(type) {
case "mountSnow": return "mount";
case "vulcan": return "mount";
case "coniferSnow": return "conifer";
case "cactus": return "dune";
case "deadTree": return "dune";
default: return type;
switch (type) {
case "mountSnow":
return "mount";
case "vulcan":
return "mount";
case "coniferSnow":
return "conifer";
case "cactus":
return "dune";
case "deadTree":
return "dune";
default:
return type;
}
}
@ -111,5 +125,4 @@
}
return ReliefIcons;
})));
})();

View file

@ -1,19 +1,13 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.Religions = factory());
}(this, (function () {'use strict';
"use strict";
window.Religions = (function () {
// name generation approach and relative chance to be selected
const approach = {"Number":1, "Being":3, "Adjective":5, "Color + Animal":5,
"Adjective + Animal":5, "Adjective + Being":5, "Adjective + Genitive":1,
"Color + Being":3, "Color + Genitive":3, "Being + of + Genitive":2, "Being + of the + Genitive":1,
"Animal + of + Genitive":1, "Adjective + Being + of + Genitive":2, "Adjective + Animal + of + Genitive":2};
const approach = {Number: 1, Being: 3, Adjective: 5, "Color + Animal": 5, "Adjective + Animal": 5, "Adjective + Being": 5, "Adjective + Genitive": 1, "Color + Being": 3, "Color + Genitive": 3, "Being + of + Genitive": 2, "Being + of the + Genitive": 1, "Animal + of + Genitive": 1, "Adjective + Being + of + Genitive": 2, "Adjective + Animal + of + Genitive": 2};
// turn weighted array into simple array
const approaches = [];
for (const a in approach) {
for (let j=0; j < approach[a]; j++) {
for (let j = 0; j < approach[a]; j++) {
approaches.push(a);
}
}
@ -21,7 +15,7 @@
const base = {
number: ["One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve"],
being: ["God", "Goddess", "Lord", "Lady", "Deity", "Creator", "Maker", "Overlord", "Ruler", "Chief", "Master", "Spirit", "Ancestor", "Father", "Forebear", "Forefather", "Mother", "Brother", "Sister", "Elder", "Numen", "Ancient", "Virgin", "Giver", "Council", "Guardian", "Reaper"],
animal: ["Dragon", "Wyvern", "Phoenix", "Unicorn", "Sphinx", "Centaur", "Pegasus", "Kraken", "Basilisk", "Chimera", "Cyclope", "Antelope", "Ape", "Badger", "Bear", "Beaver", "Bison", "Boar", "Buffalo", "Cat", "Cobra", "Crane", "Crocodile", "Crow", "Deer", "Dog", "Eagle", "Elk", "Fox", "Goat", "Goose", "Hare", "Hawk", "Heron", "Horse", "Hyena", "Ibis", "Jackal", "Jaguar", "Lark", "Leopard", "Lion", "Mantis", "Marten", "Moose", "Mule", "Narwhal", "Owl", "Panther", "Rat", "Raven", "Rook", "Scorpion", "Shark", "Sheep", "Snake", "Spider", "Swan", "Tiger", "Turtle", "Viper", "Vulture", "Walrus", "Wolf", "Wolverine", "Worm", "Camel", "Falcon", "Hound", "Ox", "Serpent"],
animal: ["Dragon", "Wyvern", "Phoenix", "Unicorn", "Sphinx", "Centaur", "Pegasus", "Kraken", "Basilisk", "Chimera", "Cyclope", "Antelope", "Ape", "Badger", "Bear", "Beaver", "Bison", "Boar", "Buffalo", "Cat", "Cobra", "Crane", "Crocodile", "Crow", "Deer", "Dog", "Eagle", "Elk", "Fox", "Goat", "Goose", "Hare", "Hawk", "Heron", "Horse", "Hyena", "Ibis", "Jackal", "Jaguar", "Lark", "Leopard", "Lion", "Mantis", "Marten", "Moose", "Mule", "Narwhal", "Owl", "Panther", "Rat", "Raven", "Rook", "Scorpion", "Shark", "Sheep", "Snake", "Spider", "Swan", "Tiger", "Turtle", "Viper", "Vulture", "Walrus", "Wolf", "Wolverine", "Worm", "Camel", "Falcon", "Hound", "Ox", "Serpent"],
adjective: ["New", "Good", "High", "Old", "Great", "Big", "Young", "Major", "Strong", "Happy", "Last", "Main", "Huge", "Far", "Beautiful", "Wild", "Fair", "Prime", "Crazy", "Ancient", "Proud", "Secret", "Lucky", "Sad", "Silent", "Latter", "Severe", "Fat", "Holy", "Pure", "Aggressive", "Honest", "Giant", "Mad", "Pregnant", "Distant", "Lost", "Broken", "Blind", "Friendly", "Unknown", "Sleeping", "Slumbering", "Loud", "Hungry", "Wise", "Worried", "Sacred", "Magical", "Superior", "Patient", "Dead", "Deadly", "Peaceful", "Grateful", "Frozen", "Evil", "Scary", "Burning", "Divine", "Bloody", "Dying", "Waking", "Brutal", "Unhappy", "Calm", "Cruel", "Favorable", "Blond", "Explicit", "Disturbing", "Devastating", "Brave", "Sunny", "Troubled", "Flying", "Sustainable", "Marine", "Fatal", "Inherent", "Selected", "Naval", "Cheerful", "Almighty", "Benevolent", "Eternal", "Immutable", "Infallible"],
genitive: ["Day", "Life", "Death", "Night", "Home", "Fog", "Snow", "Winter", "Summer", "Cold", "Springs", "Gates", "Nature", "Thunder", "Lightning", "War", "Ice", "Frost", "Fire", "Doom", "Fate", "Pain", "Heaven", "Justice", "Light", "Love", "Time", "Victory"],
theGenitive: ["World", "Word", "South", "West", "North", "East", "Sun", "Moon", "Peak", "Fall", "Dawn", "Eclipse", "Abyss", "Blood", "Tree", "Earth", "Harvest", "Rainbow", "Sea", "Sky", "Stars", "Storm", "Underworld", "Wild"],
@ -29,64 +23,70 @@
};
const forms = {
Folk:{"Shamanism":2, "Animism":2, "Ancestor worship":1, "Polytheism":2},
Organized:{"Polytheism":5, "Dualism":1, "Monotheism":4, "Non-theism":1},
Cult:{"Cult":1, "Dark Cult":1},
Heresy:{"Heresy":1}
Folk: {Shamanism: 2, Animism: 2, "Ancestor worship": 1, Polytheism: 2},
Organized: {Polytheism: 5, Dualism: 1, Monotheism: 4, "Non-theism": 1},
Cult: {Cult: 1, "Dark Cult": 1},
Heresy: {Heresy: 1}
};
const methods = {"Random + type":3, "Random + ism":1, "Supreme + ism":5, "Faith of + Supreme":5, "Place + ism":1, "Culture + ism":2, "Place + ian + type":6, "Culture + type":4};
const methods = {"Random + type": 3, "Random + ism": 1, "Supreme + ism": 5, "Faith of + Supreme": 5, "Place + ism": 1, "Culture + ism": 2, "Place + ian + type": 6, "Culture + type": 4};
const types = {
"Shamanism":{"Beliefs":3, "Shamanism":2, "Spirits":1},
"Animism":{"Spirits":1, "Beliefs":1},
"Ancestor worship":{"Beliefs":1, "Forefathers":2, "Ancestors":2},
"Polytheism":{"Deities":3, "Faith":1, "Gods":1, "Pantheon":1},
Shamanism: {Beliefs: 3, Shamanism: 2, Spirits: 1},
Animism: {Spirits: 1, Beliefs: 1},
"Ancestor worship": {Beliefs: 1, Forefathers: 2, Ancestors: 2},
Polytheism: {Deities: 3, Faith: 1, Gods: 1, Pantheon: 1},
"Dualism":{"Religion":3, "Faith":1, "Cult":1},
"Monotheism":{"Religion":1, "Church":1},
"Non-theism":{"Beliefs":3, "Spirits":1},
Dualism: {Religion: 3, Faith: 1, Cult: 1},
Monotheism: {Religion: 1, Church: 1},
"Non-theism": {Beliefs: 3, Spirits: 1},
"Cult":{"Cult":4, "Sect":4, "Worship":1, "Orden":1, "Coterie":1, "Arcanum":1},
"Dark Cult":{"Cult":2, "Sect":2, "Occultism":1, "Idols":1, "Coven":1, "Circle":1, "Blasphemy":1},
Cult: {Cult: 4, Sect: 4, Worship: 1, Orden: 1, Coterie: 1, Arcanum: 1},
"Dark Cult": {Cult: 2, Sect: 2, Occultism: 1, Idols: 1, Coven: 1, Circle: 1, Blasphemy: 1},
"Heresy":{"Heresy":3, "Sect":2, "Schism":1, "Dissenters":1, "Circle":1, "Brotherhood":1, "Society":1, "Iconoclasm":1, "Dissent":1, "Apostates":1}
Heresy: {Heresy: 3, Sect: 2, Schism: 1, Dissenters: 1, Circle: 1, Brotherhood: 1, Society: 1, Iconoclasm: 1, Dissent: 1, Apostates: 1}
};
const generate = function() {
TIME && console.time('generateReligions');
const cells = pack.cells, states = pack.states, cultures = pack.cultures;
const religions = pack.religions = [];
const generate = function () {
TIME && console.time("generateReligions");
const cells = pack.cells,
states = pack.states,
cultures = pack.cultures;
const religions = (pack.religions = []);
cells.religion = new Uint16Array(cells.culture); // cell religion; initially based on culture
// add folk religions
pack.cultures.forEach(c => {
if (!c.i) {religions.push({i: 0, name: "No religion"}); return;}
if (c.removed) {religions.push({i: c.i, name: "Extinct religion for "+c.name, color:getMixedColor(c.color, .1, 0), removed:true}); return;}
if (!c.i) {
religions.push({i: 0, name: "No religion"});
return;
}
if (c.removed) {
religions.push({i: c.i, name: "Extinct religion for " + c.name, color: getMixedColor(c.color, 0.1, 0), removed: true});
return;
}
const form = rw(forms.Folk);
const name = c.name + " " + rw(types[form]);
const deity = form === "Animism" ? null : getDeityName(c.i);
const color = getMixedColor(c.color, .1, 0); // `url(#hatch${rand(8,13)})`;
religions.push({i: c.i, name, color, culture: c.i, type:"Folk", form, deity, center: c.center, origin:0});
const color = getMixedColor(c.color, 0.1, 0); // `url(#hatch${rand(8,13)})`;
religions.push({i: c.i, name, color, culture: c.i, type: "Folk", form, deity, center: c.center, origin: 0});
});
if (religionsInput.value == 0 || pack.cultures.length < 2) {
religions.filter(r => r.i).forEach(r => r.code = abbreviate(r.name));
religions.filter(r => r.i).forEach(r => (r.code = abbreviate(r.name)));
return;
}
const burgs = pack.burgs.filter(b => b.i && !b.removed);
const sorted = burgs.length > +religionsInput.value
? burgs.sort((a, b) => b.population - a.population).map(b => b.cell)
: cells.i.filter(i => cells.s[i] > 2).sort((a, b) => cells.s[b] - cells.s[a]);
const sorted = burgs.length > +religionsInput.value ? burgs.sort((a, b) => b.population - a.population).map(b => b.cell) : cells.i.filter(i => cells.s[i] > 2).sort((a, b) => cells.s[b] - cells.s[a]);
const religionsTree = d3.quadtree();
const spacing = (graphWidth + graphHeight) / 6 / religionsInput.value; // base min distance between towns
const cultsCount = Math.floor(rand(10, 40) / 100 * religionsInput.value);
const cultsCount = Math.floor((rand(10, 40) / 100) * religionsInput.value);
const count = +religionsInput.value - cultsCount + religions.length;
// generate organized religions
for (let i=0; religions.length < count && i < 1000; i++) {
let center = sorted[biased(0, sorted.length-1, 5)]; // religion center
for (let i = 0; religions.length < count && i < 1000; i++) {
let center = sorted[biased(0, sorted.length - 1, 5)]; // religion center
const form = rw(forms.Organized);
const state = cells.state[center];
const culture = cells.culture[center];
@ -96,34 +96,36 @@
if (expansion === "state" && !state) expansion = "global";
if (expansion === "culture" && !culture) expansion = "global";
if (expansion === "state" && Math.random() > .5) center = states[state].center;
if (expansion === "culture" && Math.random() > .5) center = cultures[culture].center;
if (expansion === "state" && Math.random() > 0.5) center = states[state].center;
if (expansion === "culture" && Math.random() > 0.5) center = cultures[culture].center;
if (!cells.burg[center] && cells.c[center].some(c => cells.burg[c])) center = cells.c[center].find(c => cells.burg[c]);
const x = cells.p[center][0], y = cells.p[center][1];
const x = cells.p[center][0],
y = cells.p[center][1];
const s = spacing * gauss(1, .3, .2, 2, 2); // randomize to make the placement not uniform
const s = spacing * gauss(1, 0.3, 0.2, 2, 2); // randomize to make the placement not uniform
if (religionsTree.find(x, y, s) !== undefined) continue; // to close to existing religion
// add "Old" to name of the folk religion on this culture
const folk = religions.find(r => r.culture === culture && r.type === "Folk");
if (folk && expansion === "culture" && folk.name.slice(0,3) !== "Old") folk.name = "Old " + folk.name;
if (folk && expansion === "culture" && folk.name.slice(0, 3) !== "Old") folk.name = "Old " + folk.name;
const origin = folk ? folk.i : 0;
const expansionism = rand(3, 8);
const color = getMixedColor(religions[origin].color, .3, 0); // `url(#hatch${rand(0,5)})`;
religions.push({i: religions.length, name, color, culture, type:"Organized", form, deity, expansion, expansionism, center, origin});
const color = getMixedColor(religions[origin].color, 0.3, 0); // `url(#hatch${rand(0,5)})`;
religions.push({i: religions.length, name, color, culture, type: "Organized", form, deity, expansion, expansionism, center, origin});
religionsTree.add([x, y]);
}
// generate cults
for (let i=0; religions.length < count + cultsCount && i < 1000; i++) {
for (let i = 0; religions.length < count + cultsCount && i < 1000; i++) {
const form = rw(forms.Cult);
let center = sorted[biased(0, sorted.length-1, 1)]; // religion center
let center = sorted[biased(0, sorted.length - 1, 1)]; // religion center
if (!cells.burg[center] && cells.c[center].some(c => cells.burg[c])) center = cells.c[center].find(c => cells.burg[c]);
const x = cells.p[center][0], y = cells.p[center][1];
const x = cells.p[center][0],
y = cells.p[center][1];
const s = spacing * gauss(2, .3, 1, 3, 2); // randomize to make the placement not uniform
const s = spacing * gauss(2, 0.3, 1, 3, 2); // randomize to make the placement not uniform
if (religionsTree.find(x, y, s) !== undefined) continue; // to close to existing religion
const culture = cells.culture[center];
@ -131,9 +133,9 @@
const origin = folk ? folk.i : 0;
const deity = getDeityName(culture);
const name = getCultName(form, center);
const expansionism = gauss(1.1, .5, 0, 5);
const color = getMixedColor(cultures[culture].color, .5, 0); // "url(#hatch7)";
religions.push({i: religions.length, name, color, culture, type:"Cult", form, deity, expansion:"global", expansionism, center, origin});
const expansionism = gauss(1.1, 0.5, 0, 5);
const color = getMixedColor(cultures[culture].color, 0.5, 0); // "url(#hatch7)";
religions.push({i: religions.length, name, color, culture, type: "Cult", form, deity, expansion: "global", expansionism, center, origin});
religionsTree.add([x, y]);
//debug.append("circle").attr("cx", x).attr("cy", y).attr("r", 2).attr("fill", "red");
}
@ -141,72 +143,90 @@
expandReligions();
// generate heresies
religions.filter(r => r.type === "Organized").forEach(r => {
if (r.expansionism < 3) return;
const count = gauss(0, 1, 0, 3);
for (let i=0; i < count; i++) {
let center = ra(cells.i.filter(i => cells.religion[i] === r.i && cells.c[i].some(c => cells.religion[c] !== r.i)));
if (!center) continue;
if (!cells.burg[center] && cells.c[center].some(c => cells.burg[c])) center = cells.c[center].find(c => cells.burg[c]);
const x = cells.p[center][0], y = cells.p[center][1];
if (religionsTree.find(x, y, spacing / 10) !== undefined) continue; // to close to other
religions
.filter(r => r.type === "Organized")
.forEach(r => {
if (r.expansionism < 3) return;
const count = gauss(0, 1, 0, 3);
for (let i = 0; i < count; i++) {
let center = ra(cells.i.filter(i => cells.religion[i] === r.i && cells.c[i].some(c => cells.religion[c] !== r.i)));
if (!center) continue;
if (!cells.burg[center] && cells.c[center].some(c => cells.burg[c])) center = cells.c[center].find(c => cells.burg[c]);
const x = cells.p[center][0],
y = cells.p[center][1];
if (religionsTree.find(x, y, spacing / 10) !== undefined) continue; // to close to other
const culture = cells.culture[center];
const name = getCultName("Heresy", center);
const expansionism = gauss(1.2, .5, 0, 5);
const color = getMixedColor(r.color, .4, .2); // "url(#hatch6)";
religions.push({i: religions.length, name, color, culture, type:"Heresy", form:r.form, deity: r.deity, expansion:"global", expansionism, center, origin:r.i});
religionsTree.add([x, y]);
//debug.append("circle").attr("cx", x).attr("cy", y).attr("r", 2).attr("fill", "green");
}
});
const culture = cells.culture[center];
const name = getCultName("Heresy", center);
const expansionism = gauss(1.2, 0.5, 0, 5);
const color = getMixedColor(r.color, 0.4, 0.2); // "url(#hatch6)";
religions.push({i: religions.length, name, color, culture, type: "Heresy", form: r.form, deity: r.deity, expansion: "global", expansionism, center, origin: r.i});
religionsTree.add([x, y]);
//debug.append("circle").attr("cx", x).attr("cy", y).attr("r", 2).attr("fill", "green");
}
});
expandHeresies();
checkCenters();
TIME && console.timeEnd('generateReligions');
}
TIME && console.timeEnd("generateReligions");
};
const add = function(center) {
const cells = pack.cells, religions = pack.religions;
const add = function (center) {
const cells = pack.cells,
religions = pack.religions;
const r = cells.religion[center];
const i = religions.length;
const culture = cells.culture[center];
const color = getMixedColor(religions[r].color, .3, 0);
const color = getMixedColor(religions[r].color, 0.3, 0);
const type = religions[r].type === "Organized" ? rw({Organized:4, Cult:1, Heresy:2}) : rw({Organized:5, Cult:2});
const type = religions[r].type === "Organized" ? rw({Organized: 4, Cult: 1, Heresy: 2}) : rw({Organized: 5, Cult: 2});
const form = rw(forms[type]);
const deity = type === "Heresy" ? religions[r].deity : form === "Non-theism" ? null : getDeityName(culture);
let name, expansion;
if (type === "Organized") [name, expansion] = getReligionName(form, deity, center)
else {name = getCultName(form, center); expansion = "global";}
if (type === "Organized") [name, expansion] = getReligionName(form, deity, center);
else {
name = getCultName(form, center);
expansion = "global";
}
const formName = type === "Heresy" ? religions[r].form : form;
const code = abbreviate(name, religions.map(r => r.code));
religions.push({i, name, color, culture, type, form:formName, deity, expansion, expansionism:0, center, cells:0, area:0, rural:0, urban:0, origin:r, code});
const code = abbreviate(
name,
religions.map(r => r.code)
);
religions.push({i, name, color, culture, type, form: formName, deity, expansion, expansionism: 0, center, cells: 0, area: 0, rural: 0, urban: 0, origin: r, code});
cells.religion[center] = i;
}
};
// growth algorithm to assign cells to religions
const expandReligions = function() {
const cells = pack.cells, religions = pack.religions;
const expandReligions = function () {
const cells = pack.cells,
religions = pack.religions;
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const cost = [];
religions.filter(r => r.type === "Organized" || r.type === "Cult").forEach(r => {
cells.religion[r.center] = r.i;
queue.queue({e:r.center, p:0, r:r.i, s: cells.state[r.center], c:r.culture});
cost[r.center] = 1;
});
religions
.filter(r => r.type === "Organized" || r.type === "Cult")
.forEach(r => {
cells.religion[r.center] = r.i;
queue.queue({e: r.center, p: 0, r: r.i, s: cells.state[r.center], c: r.culture});
cost[r.center] = 1;
});
const neutral = cells.i.length / 5000 * 200 * gauss(1, .3, .2, 2, 2) * neutralInput.value; // limit cost for organized religions growth
const neutral = (cells.i.length / 5000) * 200 * gauss(1, 0.3, 0.2, 2, 2) * neutralInput.value; // limit cost for organized religions growth
const popCost = d3.max(cells.pop) / 3; // enougth population to spered religion without penalty
while (queue.length) {
const next = queue.dequeue(), n = next.e, p = next.p, r = next.r, c = next.c, s = next.s;
const next = queue.dequeue(),
n = next.e,
p = next.p,
r = next.r,
c = next.c,
s = next.s;
const expansion = religions[r].expansion;
cells.c[n].forEach(function(e) {
cells.c[n].forEach(function (e) {
if (expansion === "culture" && c !== cells.culture[e]) return;
if (expansion === "state" && s !== cells.state[e]) return;
@ -215,88 +235,101 @@
const biomeCost = cells.road[e] ? 1 : biomesData.cost[cells.biome[e]];
const populationCost = Math.max(rn(popCost - cells.pop[e]), 0);
const heightCost = Math.max(cells.h[e], 20) - 20;
const waterCost = cells.h[e] < 20 ? cells.road[e] ? 50 : 1000 : 0;
const waterCost = cells.h[e] < 20 ? (cells.road[e] ? 50 : 1000) : 0;
const totalCost = p + (cultureCost + stateCost + biomeCost + populationCost + heightCost + waterCost) / religions[r].expansionism;
if (totalCost > neutral) return;
if (!cost[e] || totalCost < cost[e]) {
if (cells.h[e] >= 20 && cells.culture[e]) cells.religion[e] = r; // assign religion to cell
cost[e] = totalCost;
queue.queue({e, p:totalCost, r, c, s});
queue.queue({e, p: totalCost, r, c, s});
}
});
}
}
};
// growth algorithm to assign cells to heresies
const expandHeresies = function() {
const cells = pack.cells, religions = pack.religions;
const expandHeresies = function () {
const cells = pack.cells,
religions = pack.religions;
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const cost = [];
religions.filter(r => r.type === "Heresy").forEach(r => {
const b = cells.religion[r.center]; // "base" religion id
cells.religion[r.center] = r.i; // heresy id
queue.queue({e:r.center, p:0, r:r.i, b});
cost[r.center] = 1;
});
religions
.filter(r => r.type === "Heresy")
.forEach(r => {
const b = cells.religion[r.center]; // "base" religion id
cells.religion[r.center] = r.i; // heresy id
queue.queue({e: r.center, p: 0, r: r.i, b});
cost[r.center] = 1;
});
const neutral = cells.i.length / 5000 * 500 * neutralInput.value; // limit cost for heresies growth
const neutral = (cells.i.length / 5000) * 500 * neutralInput.value; // limit cost for heresies growth
while (queue.length) {
const next = queue.dequeue(), n = next.e, p = next.p, r = next.r, b = next.b;
const next = queue.dequeue(),
n = next.e,
p = next.p,
r = next.r,
b = next.b;
cells.c[n].forEach(function(e) {
cells.c[n].forEach(function (e) {
const religionCost = cells.religion[e] === b ? 0 : 2000;
const biomeCost = cells.road[e] ? 0 : biomesData.cost[cells.biome[e]];
const heightCost = Math.max(cells.h[e], 20) - 20;
const waterCost = cells.h[e] < 20 ? cells.road[e] ? 50 : 1000 : 0;
const totalCost = p + (religionCost + biomeCost + heightCost + waterCost) / Math.max(religions[r].expansionism, .1);
const waterCost = cells.h[e] < 20 ? (cells.road[e] ? 50 : 1000) : 0;
const totalCost = p + (religionCost + biomeCost + heightCost + waterCost) / Math.max(religions[r].expansionism, 0.1);
if (totalCost > neutral) return;
if (!cost[e] || totalCost < cost[e]) {
if (cells.h[e] >= 20 && cells.culture[e]) cells.religion[e] = r; // assign religion to cell
cost[e] = totalCost;
queue.queue({e, p:totalCost, r});
queue.queue({e, p: totalCost, r});
}
});
}
}
};
function checkCenters() {
const cells = pack.cells, religions = pack.religions;
const cells = pack.cells,
religions = pack.religions;
const codes = religions.map(r => r.code);
religions.filter(r => r.i).forEach(r => {
r.code = abbreviate(r.name, codes);
religions
.filter(r => r.i)
.forEach(r => {
r.code = abbreviate(r.name, codes);
// move religion center if it's not within religion area after expansion
if (cells.religion[r.center] === r.i) return; // in area
const religCells = cells.i.filter(i => cells.religion[i] === r.i);
if (!religCells.length) return; // extinct religion
r.center = religCells.sort((a,b) => b.pop - a.pop)[0];
});
// move religion center if it's not within religion area after expansion
if (cells.religion[r.center] === r.i) return; // in area
const religCells = cells.i.filter(i => cells.religion[i] === r.i);
if (!religCells.length) return; // extinct religion
r.center = religCells.sort((a, b) => b.pop - a.pop)[0];
});
}
function updateCultures() {
TIME && console.time('updateCulturesForReligions');
pack.religions = pack.religions.map( (religion, index) => {
if(index === 0) {
TIME && console.time("updateCulturesForReligions");
pack.religions = pack.religions.map((religion, index) => {
if (index === 0) {
return religion;
}
return {...religion, culture: pack.cells.culture[religion.center]};
});
TIME && console.timeEnd('updateCulturesForReligions');
TIME && console.timeEnd("updateCulturesForReligions");
}
// get supreme deity name
const getDeityName = function(culture) {
if (culture === undefined) {ERROR && console.error("Please define a culture"); return;}
const getDeityName = function (culture) {
if (culture === undefined) {
ERROR && console.error("Please define a culture");
return;
}
const meaning = generateMeaning();
const cultureName = Names.getCulture(culture, null, null, "", .8);
const cultureName = Names.getCulture(culture, null, null, "", 0.8);
return cultureName + ", The " + meaning;
}
};
function generateMeaning() {
const a = ra(approaches); // select generation approach
@ -318,21 +351,29 @@
function getReligionName(form, deity, center) {
const cells = pack.cells;
const random = function() {return Names.getCulture(cells.culture[center], null, null, "", 0);}
const type = function() {return rw(types[form]);}
const supreme = function() {return deity.split(/[ ,]+/)[0];}
const place = function(adj) {
const random = function () {
return Names.getCulture(cells.culture[center], null, null, "", 0);
};
const type = function () {
return rw(types[form]);
};
const supreme = function () {
return deity.split(/[ ,]+/)[0];
};
const place = function (adj) {
const base = cells.burg[center] ? pack.burgs[cells.burg[center]].name : pack.states[cells.state[center]].name;
let name = trimVowels(base.split(/[ ,]+/)[0]);
return adj ? getAdjective(name) : name;
}
const culture = function() {return pack.cultures[cells.culture[center]].name;}
};
const culture = function () {
return pack.cultures[cells.culture[center]].name;
};
const m = rw(methods);
if (m === "Random + type") return [random() + " " + type(), "global"];
if (m === "Random + ism") return [trimVowels(random()) + "ism", "global"];
if (m === "Supreme + ism" && deity) return [trimVowels(supreme()) + "ism", "global"];
if (m === "Faith of + Supreme" && deity) return [ra(['Faith', 'Way', 'Path', 'Word', 'Witnesses']) + " of " + supreme(), "global"];
if (m === "Faith of + Supreme" && deity) return [ra(["Faith", "Way", "Path", "Word", "Witnesses"]) + " of " + supreme(), "global"];
if (m === "Place + ism") return [place() + "ism", "state"];
if (m === "Culture + ism") return [trimVowels(culture()) + "ism", "culture"];
if (m === "Place + ian + type") return [place("adj") + " " + type(), "state"];
@ -342,14 +383,19 @@
function getCultName(form, center) {
const cells = pack.cells;
const type = function() {return rw(types[form]);}
const random = function() {return trimVowels(Names.getCulture(cells.culture[center], null, null, "", 0).split(/[ ,]+/)[0]);}
const burg = function() {return trimVowels(pack.burgs[cells.burg[center]].name.split(/[ ,]+/)[0]);}
const type = function () {
return rw(types[form]);
};
const random = function () {
return trimVowels(Names.getCulture(cells.culture[center], null, null, "", 0).split(/[ ,]+/)[0]);
};
const burg = function () {
return trimVowels(pack.burgs[cells.burg[center]].name.split(/[ ,]+/)[0]);
};
if (cells.burg[center]) return burg() + "ian " + type();
if (Math.random() > .5) return random() + "ian " + type();
if (Math.random() > 0.5) return random() + "ian " + type();
return type() + " of the " + generateMeaning();
};
}
return {generate, add, getDeityName, expandReligions, updateCultures};
})));
})();

View file

@ -1,15 +1,18 @@
(function (global, factory) {
typeof exports === "object" && typeof module !== "undefined" ? (module.exports = factory()) : typeof define === "function" && define.amd ? define(factory) : (global.Rivers = factory());
})(this, function () {
"use strict";
"use strict";
window.Rivers = (function () {
const generate = function (allowErosion = true) {
TIME && console.time("generateRivers");
Math.random = aleaPRNG(seed);
const {cells, features} = pack;
const p = cells.p;
const riversData = []; // rivers data
const riversData = {}; // rivers data
const riverParents = {};
const addCellToRiver = function (cell, river) {
if (!riversData[river]) riversData[river] = [cell];
else riversData[river].push(cell);
};
cells.fl = new Uint16Array(cells.i.length); // water flux array
cells.r = new Uint16Array(cells.i.length); // rivers array
cells.conf = new Uint8Array(cells.i.length); // confluences array
@ -20,6 +23,7 @@
resolveDepressions(h);
drainWater();
defineRivers();
calculateConfluenceFlux();
Lakes.cleanupLakeData();
if (allowErosion) cells.h = Uint8Array.from(h); // apply changed heights as basic one
@ -28,99 +32,98 @@
function drainWater() {
const MIN_FLUX_TO_FORM_RIVER = 30;
const prec = grid.cells.prec;
const land = cells.i.filter(i => h[i] >= 20).sort((a, b) => h[b] - h[a]);
const lakeOutCells = Lakes.setClimateData(h);
// const flow = cells.i.length < 65535 ? new Uint16Array(cells.i.length) : new Uint32Array(cells.i.length);
// flow[i] = min;
// debug.append("path").attr("class", "arrow").attr("d", `M${cells.p[i][0]},${cells.p[i][1]}L${cells.p[min][0]},${cells.p[min][1]}`);
land.forEach(function (i) {
cells.fl[i] += grid.cells.prec[cells.g[i]]; // flux from precipitation
const [x, y] = p[i];
cells.fl[i] += prec[cells.g[i]]; // add flux from precipitation
// create lake outlet if lake is not in deep depression and flux > evaporation
const lakes = lakeOutCells[i] ? features.filter(feature => i === feature.outCell && feature.flux > feature.evaporation) : [];
for (const lake of lakes) {
const lakeCell = cells.c[i].find(c => h[c] < 20 && cells.f[c] === lake.i);
cells.fl[lakeCell] += Math.max(lake.flux - lake.evaporation, 0); // not evaporated lake water drains to outlet
// allow chain lakes to retain identity
if (cells.r[lakeCell] !== lake.river) {
const sameRiver = cells.c[lakeCell].some(c => cells.r[c] === lake.river);
if (sameRiver) {
cells.r[lakeCell] = lake.river;
riversData.push({river: lake.river, cell: lakeCell, x: p[lakeCell][0], y: p[lakeCell][1], flux: cells.fl[lakeCell]});
addCellToRiver(lakeCell, lake.river);
} else {
cells.r[lakeCell] = riverNext;
riversData.push({river: riverNext, cell: lakeCell, x: p[lakeCell][0], y: p[lakeCell][1], flux: cells.fl[lakeCell]});
addCellToRiver(lakeCell, riverNext);
riverNext++;
}
}
lake.outlet = cells.r[lakeCell];
flowDown(i, cells.fl[i], cells.fl[lakeCell], lake.outlet);
flowDown(i, cells.fl[lakeCell], lake.outlet);
}
// assign all tributary rivers to outlet basin
for (let outlet = lakes[0]?.outlet, l = 0; l < lakes.length; l++) {
lakes[l].inlets?.forEach(fork => (riversData.find(r => r.river === fork).parent = outlet));
const outlet = lakes[0]?.outlet;
for (const lake of lakes) {
if (!Array.isArray(lake.inlets)) continue;
for (const inlet of lake.inlets) {
riverParents[inlet] = outlet;
}
}
// near-border cell: pour water out of the screen
if (cells.b[i] && cells.r[i]) {
let to = [];
const min = Math.min(y, graphHeight - y, x, graphWidth - x);
if (min === y) to = [x, 0];
else if (min === graphHeight - y) to = [x, graphHeight];
else if (min === x) to = [0, y];
else if (min === graphWidth - x) to = [graphWidth, y];
riversData.push({river: cells.r[i], cell: i, x: to[0], y: to[1], flux: cells.fl[i]});
return;
}
if (cells.b[i] && cells.r[i]) return addCellToRiver(-1, cells.r[i]);
// downhill cell (make sure it's not in the source lake)
const filtered = lakeOutCells[i] ? cells.c[i].filter(c => !lakes.map(lake => lake.i).includes(cells.f[c])) : cells.c[i];
const min = filtered.sort((a, b) => h[a] - h[b])[0];
let min = null;
if (lakeOutCells[i]) {
const filtered = cells.c[i].filter(c => !lakes.map(lake => lake.i).includes(cells.f[c]));
min = filtered.sort((a, b) => h[a] - h[b])[0];
} else if (cells.haven[i]) {
min = cells.haven[i];
} else {
min = cells.c[i].sort((a, b) => h[a] - h[b])[0];
}
// cells is depressed
if (h[i] <= h[min]) return;
if (cells.fl[i] < MIN_FLUX_TO_FORM_RIVER) {
// flux is too small to operate as a river
if (h[min] >= 20) cells.fl[min] += cells.fl[i];
return; // flux is too small to operate as river
return;
}
// proclaim a new river
if (!cells.r[i]) {
cells.r[i] = riverNext;
riversData.push({river: riverNext, cell: i, x, y, flux: cells.fl[i]});
addCellToRiver(i, riverNext);
riverNext++;
}
flowDown(min, cells.fl[min], cells.fl[i], cells.r[i], i);
flowDown(min, cells.fl[i], cells.r[i]);
});
}
function flowDown(toCell, toFlux, fromFlux, river, fromCell = 0) {
if (cells.r[toCell]) {
function flowDown(toCell, fromFlux, river) {
const toFlux = cells.fl[toCell] - cells.conf[toCell];
const toRiver = cells.r[toCell];
if (toRiver) {
// downhill cell already has river assigned
if (toFlux < fromFlux) {
cells.conf[toCell] = cells.fl[toCell]; // mark confluence
if (h[toCell] >= 20) riversData.find(r => r.river === cells.r[toCell]).parent = river; // min river is a tributary of current river
if (fromFlux > toFlux) {
cells.conf[toCell] += cells.fl[toCell]; // mark confluence
if (h[toCell] >= 20) riverParents[toRiver] = river; // min river is a tributary of current river
cells.r[toCell] = river; // re-assign river if downhill part has less flux
} else {
cells.conf[toCell] += fromFlux; // mark confluence
if (h[toCell] >= 20) riversData.find(r => r.river === river).parent = cells.r[toCell]; // current river is a tributary of min river
if (h[toCell] >= 20) riverParents[river] = toRiver; // current river is a tributary of min river
}
} else cells.r[toCell] = river; // assign the river to the downhill cell
if (h[toCell] < 20) {
// pour water to the water body
const haven = fromCell ? cells.haven[fromCell] : toCell;
riversData.push({river, cell: haven, x: p[toCell][0], y: p[toCell][1], flux: fromFlux});
const waterBody = features[cells.f[toCell]];
if (waterBody.type === "lake") {
if (!waterBody.river || fromFlux > waterBody.enteringFlux) {
@ -128,58 +131,69 @@
waterBody.enteringFlux = fromFlux;
}
waterBody.flux = waterBody.flux + fromFlux;
waterBody.inlets ? waterBody.inlets.push(river) : (waterBody.inlets = [river]);
if (!waterBody.inlets) waterBody.inlets = [river];
else waterBody.inlets.push(river);
}
} else {
// propagate flux and add next river segment
cells.fl[toCell] += fromFlux;
riversData.push({river, cell: toCell, x: p[toCell][0], y: p[toCell][1], flux: fromFlux});
}
addCellToRiver(toCell, river);
}
function defineRivers() {
cells.r = new Uint16Array(cells.i.length); // re-initiate rivers array
pack.rivers = []; // rivers data
const riverPaths = [];
// re-initialize rivers and confluence arrays
cells.r = new Uint16Array(cells.i.length);
cells.conf = new Uint16Array(cells.i.length);
pack.rivers = [];
for (let r = 1; r <= riverNext; r++) {
const riverSegments = riversData.filter(d => d.river === r);
if (riverSegments.length < 3) continue;
for (const key in riversData) {
const riverCells = riversData[key];
if (riverCells.length < 3) continue; // exclude tiny rivers
for (const segment of riverSegments) {
const i = segment.cell;
if (cells.r[i]) continue;
if (cells.h[i] < 20) continue;
cells.r[i] = r;
const riverId = +key;
for (const cell of riverCells) {
if (cell < 0 || cells.h[cell] < 20) continue;
// mark real confluences and assign river to cells
if (cells.r[cell]) cells.conf[cell] = 1;
else cells.r[cell] = riverId;
}
const source = riverSegments[0].cell;
const mouth = riverSegments[riverSegments.length - 2].cell;
const source = riverCells[0];
const mouth = riverCells[riverCells.length - 2];
const parent = riverParents[key] || 0;
const widthFactor = rn(0.8 + Math.random() * 0.4, 1); // river width modifier [.8, 1.2]
const sourceWidth = cells.h[source] >= 20 ? 0.1 : rn(Math.min(Math.max((cells.fl[source] / 500) ** 0.4, 0.5), 1.7), 2);
const widthFactor = !parent || parent === riverId ? 1.2 : 1;
const meanderedPoints = addMeandering(riverCells);
const discharge = cells.fl[mouth]; // m3 in second
const length = rn(getApproximateLength(meanderedPoints), 2);
const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, 0));
const riverMeandered = addMeandering(riverSegments, sourceWidth * 10, 0.5);
const [path, length, offset] = getPath(riverMeandered, widthFactor, sourceWidth);
riverPaths.push([path, r]);
const parent = riverSegments[0].parent || 0;
const width = rn(offset ** 2, 2); // mounth width in km
const discharge = last(riverSegments).flux; // in m3/s
pack.rivers.push({i: r, source, mouth, discharge, length, width, widthFactor, sourceWidth, parent});
pack.rivers.push({i: riverId, source, mouth, discharge, length, width, widthFactor, sourceWidth: 0, parent, cells: riverCells});
}
}
// draw rivers
rivers.html(riverPaths.map(d => `<path id="river${d[1]}" d="${d[0]}"/>`).join(""));
function calculateConfluenceFlux() {
for (const i of cells.i) {
if (!cells.conf[i]) continue;
const sortedInflux = cells.c[i]
.filter(c => cells.r[c] && h[c] > h[i])
.map(c => cells.fl[c])
.sort((a, b) => b - a);
cells.conf[i] = sortedInflux.reduce((acc, flux, index) => (index ? acc + flux : acc), 0);
}
}
};
// add distance to water value to land cells to make map less depressed
const alterHeights = () => {
const cells = pack.cells;
return Array.from(cells.h).map((h, i) => {
if (h < 20 || cells.t[i] < 1) return h;
return h + cells.t[i] / 100 + d3.mean(cells.c[i].map(c => cells.t[c])) / 10000;
const {h, c, t} = pack.cells;
return Array.from(h).map((h, i) => {
if (h < 20 || t[i] < 1) return h;
return h + t[i] / 100 + d3.mean(c[i].map(c => t[c])) / 10000;
});
};
@ -242,118 +256,132 @@
depressions && WARN && console.warn(`Unresolved depressions: ${depressions}. Edit heightmap to fix`);
};
// add more river points on 1/3 and 2/3 of length
const addMeandering = function (segments, width = 1, meandering = 0.5) {
const riverMeandered = []; // to store enhanced segments
// add points at 1/3 and 2/3 of a line between adjacents river cells
const addMeandering = function (riverCells, riverPoints = null, meandering = 0.5) {
const {fl, conf, h} = pack.cells;
const meandered = [];
const lastStep = riverCells.length - 1;
const points = getRiverPoints(riverCells, riverPoints);
let step = h[riverCells[0]] < 20 ? 1 : 10;
for (let s = 0; s < segments.length; s++, width++) {
const sX = segments[s].x,
sY = segments[s].y; // segment start coordinates
const c = pack.cells.conf[segments[s].cell] || 0; // if segment is river confluence
riverMeandered.push([sX, sY, c]);
let fluxPrev = 0;
const getFlux = (step, flux) => (step === lastStep ? fluxPrev : flux);
if (s + 1 === segments.length) break; // do not meander last segment
for (let i = 0; i <= lastStep; i++, step++) {
const cell = riverCells[i];
const isLastCell = i === lastStep;
const eX = segments[s + 1].x,
eY = segments[s + 1].y; // segment end coordinates
const angle = Math.atan2(eY - sY, eX - sX);
const sin = Math.sin(angle),
cos = Math.cos(angle);
const [x1, y1] = points[i];
const flux1 = getFlux(i, fl[cell]);
fluxPrev = flux1;
const meander = meandering + 1 / width + Math.random() * Math.max(meandering - width / 100, 0);
const dist2 = (eX - sX) ** 2 + (eY - sY) ** 2; // square distance between segment start and end
meandered.push([x1, y1, flux1]);
if (isLastCell) break;
if (width < 10 && (dist2 > 64 || (dist2 > 36 && segments.length < 6))) {
const nextCell = riverCells[i + 1];
const [x2, y2] = points[i + 1];
if (nextCell === -1) {
meandered.push([x2, y2, fluxPrev]);
break;
}
const dist2 = (x2 - x1) ** 2 + (y2 - y1) ** 2; // square distance between cells
if (dist2 <= 25 && riverCells.length >= 6) continue;
const flux2 = getFlux(i + 1, fl[nextCell]);
const keepInitialFlux = conf[nextCell] || flux1 === flux2;
const meander = meandering + 1 / step + Math.max(meandering - step / 100, 0);
const angle = Math.atan2(y2 - y1, x2 - x1);
const sinMeander = Math.sin(angle) * meander;
const cosMeander = Math.cos(angle) * meander;
if (step < 10 && (dist2 > 64 || (dist2 > 36 && riverCells.length < 5))) {
// if dist2 is big or river is small add extra points at 1/3 and 2/3 of segment
const p1x = (sX * 2 + eX) / 3 + -sin * meander;
const p1y = (sY * 2 + eY) / 3 + cos * meander;
const p2x = (sX + eX * 2) / 3 + sin * meander;
const p2y = (sY + eY * 2) / 3 + cos * meander;
riverMeandered.push([p1x, p1y], [p2x, p2y]);
} else if (dist2 > 25 || segments.length < 6) {
const p1x = (x1 * 2 + x2) / 3 + -sinMeander;
const p1y = (y1 * 2 + y2) / 3 + cosMeander;
const p2x = (x1 + x2 * 2) / 3 + sinMeander / 2;
const p2y = (y1 + y2 * 2) / 3 - cosMeander / 2;
const [p1fl, p2fl] = keepInitialFlux ? [flux1, flux1] : [(flux1 * 2 + flux2) / 3, (flux1 + flux2 * 2) / 3];
meandered.push([p1x, p1y, p1fl], [p2x, p2y, p2fl]);
} else if (dist2 > 25 || riverCells.length < 6) {
// if dist is medium or river is small add 1 extra middlepoint
const p1x = (sX + eX) / 2 + -sin * meander;
const p1y = (sY + eY) / 2 + cos * meander;
riverMeandered.push([p1x, p1y]);
const p1x = (x1 + x2) / 2 + -sinMeander;
const p1y = (y1 + y2) / 2 + cosMeander;
const p1fl = keepInitialFlux ? flux1 : (flux1 + flux2) / 2;
meandered.push([p1x, p1y, p1fl]);
}
}
return riverMeandered;
return meandered;
};
const getPath = function (points, widthFactor = 1, sourceWidth = 0.1) {
let offset,
extraOffset = sourceWidth; // starting river width (to make river source visible)
const riverLength = points.reduce((s, v, i, p) => s + (i ? Math.hypot(v[0] - p[i - 1][0], v[1] - p[i - 1][1]) : 0), 0); // summ of segments length
const widening = 1000 + riverLength * 30;
const riverPointsLeft = [],
riverPointsRight = []; // store points on both sides to build a valid polygon
const last = points.length - 1;
const factor = riverLength / points.length;
const getRiverPoints = (riverCells, riverPoints) => {
const {p} = pack.cells;
return riverCells.map((cell, i) => {
if (riverPoints && riverPoints[i]) return riverPoints[i];
if (cell === -1) return getBorderPoint(riverCells[i - 1]);
return p[cell];
});
};
// first point
let x = points[0][0],
y = points[0][1],
c;
let angle = Math.atan2(y - points[1][1], x - points[1][0]);
let sin = Math.sin(angle),
cos = Math.cos(angle);
let xLeft = x + -sin * extraOffset,
yLeft = y + cos * extraOffset;
riverPointsLeft.push([xLeft, yLeft]);
let xRight = x + sin * extraOffset,
yRight = y + -cos * extraOffset;
riverPointsRight.unshift([xRight, yRight]);
const getBorderPoint = i => {
const [x, y] = pack.cells.p[i];
const min = Math.min(y, graphHeight - y, x, graphWidth - x);
if (min === y) return [x, 0];
else if (min === graphHeight - y) return [x, graphHeight];
else if (min === x) return [0, y];
return [graphWidth, y];
};
// middle points
for (let p = 1; p < last; p++) {
(x = points[p][0]), (y = points[p][1]), (c = points[p][2] || 0);
const xPrev = points[p - 1][0],
yPrev = points[p - 1][1];
const xNext = points[p + 1][0],
yNext = points[p + 1][1];
angle = Math.atan2(yPrev - yNext, xPrev - xNext);
(sin = Math.sin(angle)), (cos = Math.cos(angle));
offset = (Math.atan(Math.pow(p * factor, 2) / widening) / 2) * widthFactor + extraOffset;
const confOffset = Math.atan((c * 5) / widening);
extraOffset += confOffset;
(xLeft = x + -sin * offset), (yLeft = y + cos * (offset + confOffset));
riverPointsLeft.push([xLeft, yLeft]);
(xRight = x + sin * offset), (yRight = y + -cos * offset);
riverPointsRight.unshift([xRight, yRight]);
const FLUX_FACTOR = 500;
const MAX_FLUX_WIDTH = 2;
const LENGTH_FACTOR = 200;
const STEP_WIDTH = 1 / LENGTH_FACTOR;
const LENGTH_PROGRESSION = [1, 1, 2, 3, 5, 8, 13, 21, 34].map(n => n / LENGTH_FACTOR);
const MAX_PROGRESSION = last(LENGTH_PROGRESSION);
const getOffset = (flux, pointNumber, widthFactor = 1, startingWidth = 0) => {
const fluxWidth = Math.min(flux ** 0.9 / FLUX_FACTOR, MAX_FLUX_WIDTH);
const lengthWidth = pointNumber * STEP_WIDTH + (LENGTH_PROGRESSION[pointNumber] || MAX_PROGRESSION);
return widthFactor * (lengthWidth + fluxWidth) + startingWidth;
};
// build polygon from a list of points and calculated offset (width)
const getRiverPath = function (points, widthFactor = 1, startingWidth = 0) {
const riverPointsLeft = [];
const riverPointsRight = [];
for (let p = 0; p < points.length; p++) {
const [x0, y0] = points[p - 1] || points[p];
const [x1, y1, flux] = points[p];
const [x2, y2] = points[p + 1] || points[p];
const offset = getOffset(flux, p, widthFactor, startingWidth);
const angle = Math.atan2(y0 - y2, x0 - x2);
const sinOffset = Math.sin(angle) * offset;
const cosOffset = Math.cos(angle) * offset;
riverPointsLeft.push([x1 - sinOffset, y1 + cosOffset]);
riverPointsRight.push([x1 + sinOffset, y1 - cosOffset]);
}
// end point
(x = points[last][0]), (y = points[last][1]), (c = points[last][2]);
if (c) extraOffset += Math.atan((c * 10) / widening); // add extra width on river confluence
angle = Math.atan2(points[last - 1][1] - y, points[last - 1][0] - x);
(sin = Math.sin(angle)), (cos = Math.cos(angle));
(xLeft = x + -sin * offset), (yLeft = y + cos * offset);
riverPointsLeft.push([xLeft, yLeft]);
(xRight = x + sin * offset), (yRight = y + -cos * offset);
riverPointsRight.unshift([xRight, yRight]);
// generate polygon path and return
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
const right = lineGen(riverPointsRight);
const right = lineGen(riverPointsRight.reverse());
let left = lineGen(riverPointsLeft);
left = left.substring(left.indexOf("C"));
return [round(right + left, 2), rn(riverLength, 2), offset];
return round(right + left, 1);
};
const specify = function () {
const rivers = pack.rivers;
if (!rivers.length) return;
Math.random = aleaPRNG(seed);
const thresholdElement = Math.ceil(rivers.length * 0.15);
const smallLength = rivers.map(r => r.length || 0).sort((a, b) => a - b)[thresholdElement];
const smallType = {Creek: 9, River: 3, Brook: 3, Stream: 1}; // weighted small river types
for (const r of rivers) {
r.basin = getBasin(r.i);
r.name = getName(r.mouth);
const small = r.length < smallLength;
r.type = r.parent && !(r.i % 6) ? (small ? "Branch" : "Fork") : small ? rw(smallType) : "River";
for (const river of rivers) {
river.basin = getBasin(river.i);
river.name = getName(river.mouth);
river.type = getType(river);
}
};
@ -361,6 +389,36 @@
return Names.getCulture(pack.cells.culture[cell]);
};
// weighted arrays of river type names
const riverTypes = {
main: {
big: {River: 1},
small: {Creek: 9, River: 3, Brook: 3, Stream: 1}
},
fork: {
big: {Fork: 1},
small: {Branch: 1}
}
};
let smallLength = null;
const getType = function ({i, length, parent}) {
if (smallLength === null) {
const threshold = Math.ceil(pack.rivers.length * 0.15);
smallLength = pack.rivers.map(r => r.length || 0).sort((a, b) => a - b)[threshold];
}
const isSmall = length < smallLength;
const isFork = each(3)(i) && parent && parent !== i;
return rw(riverTypes[isFork ? "fork" : "main"][isSmall ? "small" : "big"]);
};
const getApproximateLength = points => points.reduce((s, v, i, p) => s + (i ? Math.hypot(v[0] - p[i - 1][0], v[1] - p[i - 1][1]) : 0), 0);
// Real mouth width examples: Amazon 6000m, Volga 6000m, Dniepr 3000m, Mississippi 1300m, Themes 900m,
// Danube 800m, Daugava 600m, Neva 500m, Nile 450m, Don 400m, Wisla 300m, Pripyat 150m, Bug 140m, Muchavets 40m
const getWidth = offset => rn((offset / 1.5) ** 1.8, 2); // mouth width in km
// remove river and all its tributaries
const remove = function (id) {
const cells = pack.cells;
@ -381,5 +439,5 @@
return getBasin(parent);
};
return {generate, alterHeights, resolveDepressions, addMeandering, getPath, specify, getName, getBasin, remove};
});
return {generate, alterHeights, resolveDepressions, addMeandering, getRiverPath, specify, getName, getType, getBasin, getWidth, getOffset, getApproximateLength, getRiverPoints, remove};
})();

View file

@ -1,24 +1,20 @@
(function (global, factory) {
typeof exports === "object" && typeof module !== "undefined" ? (module.exports = factory()) : typeof define === "function" && define.amd ? define(factory) : (global.Routes = factory());
})(this, function () {
"use strict";
window.Routes = (function () {
const getRoads = function () {
TIME && console.time("generateMainRoads");
const cells = pack.cells;
const burgs = pack.burgs.filter(b => b.i && !b.removed);
const capitals = burgs.filter(b => b.capital);
const capitals = burgs.filter(b => b.capital).sort((a, b) => a.population - b.population);
if (capitals.length < 2) return []; // not enough capitals to build main roads
const paths = []; // array to store path segments
for (const b of capitals) {
const connect = capitals.filter(c => c.i > b.i && c.feature === b.feature);
if (!connect.length) continue;
const farthest = d3.scan(connect, (a, c) => (c.y - b.y) ** 2 + (c.x - b.x) ** 2 - ((a.y - b.y) ** 2 + (a.x - b.x) ** 2));
const [from, exit] = findLandPath(b.cell, connect[farthest].cell, null);
const segments = restorePath(b.cell, exit, "main", from);
segments.forEach(s => paths.push(s));
const connect = capitals.filter(c => c.feature === b.feature && c !== b);
for (const t of connect) {
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
@ -41,11 +37,12 @@
isle.forEach(function (b, i) {
let path = [];
if (!i) {
// build trail from the first burg on island to the farthest one on the same island
// build trail from the first burg on island
// to the farthest one on the same island or the closest road
const farthest = d3.scan(isle, (a, c) => (c.y - b.y) ** 2 + (c.x - b.x) ** 2 - ((a.y - b.y) ** 2 + (a.x - b.x) ** 2));
const to = isle[farthest].cell;
if (cells.road[to]) return;
const [from, exit] = findLandPath(b.cell, to, null);
const [from, exit] = findLandPath(b.cell, to, true);
path = restorePath(b.cell, exit, "small", from);
} else {
// build trail from all other burgs to the closest road on the same island
@ -176,6 +173,7 @@
if (cells.h[c] < 20) continue; // ignore water cells
const stateChangeCost = cells.state && cells.state[c] !== cells.state[n] ? 400 : 0; // trails tend to lay within the same state
const habitability = biomesData.habitability[cells.biome[c]];
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
@ -198,7 +196,7 @@
let segment = [],
current = end,
prev = end;
const score = type === "main" ? 5 : 1; // to incrade road score at cell
const score = type === "main" ? 5 : 1; // to increase road score at cell
if (type === "ocean" || !cells.road[prev]) segment.push(end);
if (!cells.road[prev]) cells.road[prev] = score;
@ -268,4 +266,4 @@
}
return [from, exit, false];
}
});
})();

View file

@ -74,7 +74,7 @@ async function saveJPEG() {
async function saveTiles() {
return new Promise(async (resolve, reject) => {
// download schema
const urlSchema = await getMapURL('tiles', 'schema');
const urlSchema = await getMapURL('tiles', {debug: true});
const zip = new JSZip();
const canvas = document.createElement('canvas');
@ -138,20 +138,26 @@ async function saveTiles() {
}
// parse map svg to object url
async function getMapURL(type, subtype) {
async function getMapURL(type, options = {}) {
const {debug = false, globe = false, noLabels = false, noWater = false} = options;
const cloneEl = document.getElementById('map').cloneNode(true); // clone svg
cloneEl.id = 'fantasyMap';
document.body.appendChild(cloneEl);
const clone = d3.select(cloneEl);
if (subtype !== 'schema') clone.select('#debug').remove();
if (!debug) clone.select('#debug').remove();
const cloneDefs = cloneEl.getElementsByTagName('defs')[0];
const svgDefs = document.getElementById('defElements');
const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
if (isFirefox && type === 'mesh') clone.select('#oceanPattern').remove();
if (subtype === 'globe') clone.select('#scaleBar').remove();
if (subtype === 'noWater') {
if (globe) clone.select('#scaleBar').remove();
if (noLabels) {
clone.select('#labels #states').remove();
clone.select('#labels #burgLabels').remove();
clone.select('#icons #burgIcons').remove();
}
if (noWater) {
clone.select('#oceanBase').attr('opacity', 0);
clone.select('#oceanPattern').attr('opacity', 0);
}
@ -275,8 +281,16 @@ async function getMapURL(type, subtype) {
});
}
const fontStyle = await GFontToDataURI(getFontsToLoad(clone)); // load non-standard fonts
if (fontStyle) clone.select('defs').append('style').text(fontStyle.join('\n')); // add font to style
// load non-standard fonts
const usedFonts = getFontsList(clone);
const webSafe = ['Georgia', 'Times+New+Roman', 'Comic+Sans+MS', 'Lucida+Sans+Unicode', 'Courier+New', 'Verdana', 'Arial', 'Impact'];
const fontsToLoad = usedFonts.filter((font) => !webSafe.includes(font));
if (fontsToLoad.length) {
const url = 'https://fonts.googleapis.com/css?family=' + fontsToLoad.join('|');
const fontStyle = await GFontToDataURI(url);
if (fontStyle) clone.select('defs').append('style').text(fontStyle.join('\n'));
}
clone.remove();
const serialized = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>` + new XMLSerializer().serializeToString(cloneEl);
@ -357,63 +371,6 @@ function inlineStyle(clone) {
emptyG.remove();
}
// get non-standard fonts used for labels to fetch them from web
function getFontsToLoad(clone) {
const webSafe = ['Georgia', 'Times+New+Roman', 'Comic+Sans+MS', 'Lucida+Sans+Unicode', 'Courier+New', 'Verdana', 'Arial', 'Impact']; // fonts to not fetch
const fontsInUse = new Set(); // to store fonts currently in use
clone.selectAll('#labels > g').each(function () {
if (!this.hasChildNodes()) return;
const font = this.dataset.font;
if (!font || webSafe.includes(font)) return;
fontsInUse.add(font);
});
const legendFont = legend.attr('data-font');
if (legend.node().hasChildNodes() && !webSafe.includes(legendFont)) fontsInUse.add(legendFont);
const fonts = [...fontsInUse];
return fonts.length ? 'https://fonts.googleapis.com/css?family=' + fonts.join('|') : null;
}
// code from Kaiido's answer https://stackoverflow.com/questions/42402584/how-to-use-google-fonts-in-canvas-when-drawing-dom-objects-in-svg
function GFontToDataURI(url) {
if (!url) return Promise.resolve();
return fetch(url) // first fecth the embed stylesheet page
.then((resp) => resp.text()) // we only need the text of it
.then((text) => {
let s = document.createElement('style');
s.innerHTML = text;
document.head.appendChild(s);
const styleSheet = Array.prototype.filter.call(document.styleSheets, (sS) => sS.ownerNode === s)[0];
const FontRule = (rule) => {
const src = rule.style.getPropertyValue('src');
const url = src ? src.split('url(')[1].split(')')[0] : '';
return {rule, src, url: url.substring(url.length - 1, 1)};
};
const fontProms = [];
for (const r of styleSheet.cssRules) {
let fR = FontRule(r);
if (!fR.url) continue;
fontProms.push(
fetch(fR.url) // fetch the actual font-file (.woff)
.then((resp) => resp.blob())
.then((blob) => {
return new Promise((resolve) => {
let f = new FileReader();
f.onload = (e) => resolve(f.result);
f.readAsDataURL(blob);
});
})
.then((dataURL) => fR.rule.cssText.replace(fR.url, dataURL))
);
}
document.head.removeChild(s); // clean up
return Promise.all(fontProms); // wait for all this has been done
});
}
// prepare map data for saving
function getMapData() {
TIME && console.time('createMapDataBlob');
@ -445,7 +402,9 @@ function getMapData() {
precOutput.value,
JSON.stringify(options),
mapName.value,
+hideLabels.checked
+hideLabels.checked,
stylePreset.value,
+rescaleLabels.checked
].join('|');
const coords = JSON.stringify(mapCoordinates);
const biomes = [biomesData.color, biomesData.habitability, biomesData.name].join('|');
@ -667,10 +626,7 @@ function getRiverPoints(node) {
}
async function quickSave() {
if (customization) {
tip('Map cannot be saved when edit mode is active, please exit the mode and retry', false, 'error');
return;
}
if (customization) return tip('Map cannot be saved when edit mode is active, please exit the mode and retry', false, 'error');
const blob = await getMapData();
if (blob) ldb.set('lastMap', blob); // auto-save map
tip('Map is saved to browser memory. Please also save as .map file to secure progress', true, 'success', 2000);

File diff suppressed because one or more lines are too long

View file

@ -14,14 +14,14 @@ function restoreDefaultEvents() {
function clicked() {
const el = d3.event.target;
if (!el || !el.parentElement || !el.parentElement.parentElement) return;
const parent = el.parentElement,
grand = parent.parentElement,
great = grand.parentElement;
const parent = el.parentElement;
const grand = parent.parentElement;
const great = grand.parentElement;
const p = d3.mouse(this);
const i = findCell(p[0], p[1]);
if (grand.id === 'emblems') editEmblem();
else if (parent.id === 'rivers') editRiver();
else if (parent.id === 'rivers') editRiver(el.id);
else if (grand.id === 'routes') editRoute();
else if (el.tagName === 'tspan' && grand.parentNode.parentNode.id === 'labels') editLabel();
else if (grand.id === 'burgLabels') editBurg();
@ -118,33 +118,6 @@ function applySorting(headers) {
.forEach((line) => list.appendChild(line));
}
function confirmationDialog(options) {
const {
title = 'Confirm action',
message = 'Are you sure you want to continue? <br>The action cannot be reverted',
cancel = 'Cancel',
confirm = 'Continue',
onCancel = () => {},
onConfirm = () => {}
} = options;
alertMessage.innerHTML = message;
$('#alert').dialog({
resizable: false,
title,
buttons: {
[confirm]: function () {
onConfirm();
$(this).dialog('close');
},
[cancel]: function () {
onCancel();
$(this).dialog('close');
}
}
});
}
function addBurg(point) {
const cells = pack.cells;
const x = rn(point[0], 2),
@ -405,15 +378,14 @@ function clearLegend() {
}
// draw color (fill) picker
function createPicker(hatching) {
const COLORS_IN_ROW = 14;
function createPicker() {
const pos = () => tip('Drag to change the picker position');
const cl = () => tip('Click to close the picker');
const closePicker = () => container.remove();
const closePicker = () => contaiter.style('display', 'none');
const container = d3.select('body').append('svg').attr('id', 'pickerContainer').attr('width', '100%').attr('height', '100%');
container.append('rect').attr('width', '100%').attr('height', '100%').attr('opacity', 0).on('mousemove', cl).on('click', closePicker);
const picker = container
const contaiter = d3.select('body').append('svg').attr('id', 'pickerContainer').attr('width', '100%').attr('height', '100%');
contaiter.append('rect').attr('x', 0).attr('y', 0).attr('width', '100%').attr('height', '100%').attr('opacity', 0.2).on('mousemove', cl).on('click', closePicker);
const picker = contaiter
.append('g')
.attr('id', 'picker')
.call(
@ -469,7 +441,11 @@ function createPicker(hatching) {
spaces.selectAll('input').on('change', changePickerSpace);
const colors = picker.append('g').attr('id', 'pickerColors').attr('stroke', '#333333');
const clr = d3.range(COLORS_IN_ROW).map((i) => d3.hsl((i / COLORS_IN_ROW) * 360, 0.7, 0.7).hex());
const hatches = picker.append('g').attr('id', 'pickerHatches').attr('stroke', '#333333');
const hatching = d3.selectAll('g#hatching > pattern');
const number = hatching.size();
const clr = d3.range(number).map((i) => d3.hsl((i / number) * 360, 0.7, 0.7).hex());
clr.forEach(function (d, i) {
colors
.append('rect')
@ -481,28 +457,26 @@ function createPicker(hatching) {
.attr('width', 16)
.attr('height', 16);
});
hatching.each(function (d, i) {
hatches
.append('rect')
.attr('id', 'picker_' + this.id)
.attr('fill', 'url(#' + this.id + ')')
.attr('x', i * 22 + 4)
.attr('y', 61)
.attr('width', 16)
.attr('height', 16);
});
colors
.selectAll('rect')
.on('click', pickerFillClicked)
.on('mousemove', () => tip('Click to fill with the color'));
if (hatching) {
const hatches = picker.append('g').attr('id', 'pickerHatches').attr('stroke', '#333333');
d3.selectAll('g#hatching > pattern').each(function (d, i) {
hatches
.append('rect')
.attr('id', 'picker_' + this.id)
.attr('fill', 'url(#' + this.id + ')')
.attr('x', i * 22 + 4)
.attr('y', 61)
.attr('width', 16)
.attr('height', 16);
});
hatches
.selectAll('rect')
.on('click', pickerFillClicked)
.on('mousemove', () => tip('Click to fill with the hatching'));
}
hatches
.selectAll('rect')
.on('click', pickerFillClicked)
.on('mousemove', () => tip('Click to fill with the hatching'));
// append box
const bbox = picker.node().getBBox();
@ -973,6 +947,7 @@ function selectIcon(initial, callback) {
// Calls the refresh for all currently open editors
function refreshAllEditors() {
TIME && console.time('refreshAllEditors');
if (document.getElementById('culturesEditorRefresh').offsetParent) culturesEditorRefresh.click();
if (document.getElementById('biomesEditorRefresh').offsetParent) biomesEditorRefresh.click();
if (document.getElementById('diplomacyEditorRefresh').offsetParent) diplomacyEditorRefresh.click();
@ -980,5 +955,5 @@ function refreshAllEditors() {
if (document.getElementById('religionsEditorRefresh').offsetParent) religionsEditorRefresh.click();
if (document.getElementById('statesEditorRefresh').offsetParent) statesEditorRefresh.click();
if (document.getElementById('zonesEditorRefresh').offsetParent) zonesEditorRefresh.click();
if (document.getElementById('resourcesEditorRefresh').offsetParent) resourcesEditorRefresh.click();
TIME && console.timeEnd('refreshAllEditors');
}

View file

@ -22,7 +22,7 @@ document.getElementById('exitCustomization').addEventListener('mousemove', showD
/**
* @param {string} tip Tooltip text
* @param {boolean} main Show above other tooltips
* @param {string} type Message type (color): error, warn, success
* @param {string} type Message type (color): error / warn / success
* @param {number} time Timeout to auto hide, ms
*/
function tip(tip = 'Tip is undefined', main, type, time) {
@ -96,10 +96,7 @@ function showMapTooltip(point, e, i, g) {
const land = pack.cells.h[i] >= 20;
// specific elements
if (group === 'armies') {
tip(e.target.parentNode.dataset.name + '. Click to edit');
return;
}
if (group === 'armies') return tip(e.target.parentNode.dataset.name + '. Click to edit');
if (group === 'emblems' && e.target.tagName === 'use') {
const parent = e.target.parentNode;
@ -130,14 +127,11 @@ function showMapTooltip(point, e, i, g) {
if (riversOverview.offsetParent) highlightEditorLine(riversOverview, river, 5000);
return;
}
if (group === 'routes') {
tip('Click to edit the Route');
return;
}
if (group === 'terrain') {
tip('Click to edit the Relief Icon');
return;
}
if (group === 'routes') return tip('Click to edit the Route');
if (group === 'terrain') return tip('Click to edit the Relief Icon');
if (subgroup === 'burgLabels' || subgroup === 'burgIcons') {
const burg = +path[path.length - 10].dataset.id;
const b = pack.burgs[burg];
@ -146,50 +140,25 @@ function showMapTooltip(point, e, i, g) {
if (burgsOverview.offsetParent) highlightEditorLine(burgsOverview, burg, 5000);
return;
}
if (group === 'labels') {
tip('Click to edit the Label');
return;
}
if (group === 'markers') {
tip('Click to edit the Marker');
return;
}
if (group === 'labels') return tip('Click to edit the Label');
if (group === 'markers') return tip('Click to edit the Marker');
if (group === 'ruler') {
const tag = e.target.tagName;
const className = e.target.getAttribute('class');
if (tag === 'circle' && className === 'edge') {
tip('Drag to adjust. Hold Ctrl and drag to add a point. Click to remove the point');
return;
}
if (tag === 'circle' && className === 'control') {
tip('Drag to adjust. Hold Shifta and drag to keep axial direction. Click to remove the point');
return;
}
if (tag === 'circle') {
tip('Drag to adjust the measurer');
return;
}
if (tag === 'polyline') {
tip('Click on drag to add a control point');
return;
}
if (tag === 'path') {
tip('Drag to move the measurer');
return;
}
if (tag === 'text') {
tip('Drag to move, click to remove the measurer');
return;
}
}
if (subgroup === 'burgIcons') {
tip('Click to edit the Burg');
return;
}
if (subgroup === 'burgLabels') {
tip('Click to edit the Burg');
return;
if (tag === 'circle' && className === 'edge') return tip('Drag to adjust. Hold Ctrl and drag to add a point. Click to remove the point');
if (tag === 'circle' && className === 'control') return tip('Drag to adjust. Hold Shift and drag to keep axial direction. Click to remove the point');
if (tag === 'circle') return tip('Drag to adjust the measurer');
if (tag === 'polyline') return tip('Click on drag to add a control point');
if (tag === 'path') return tip('Drag to move the measurer');
if (tag === 'text') return tip('Drag to move, click to remove the measurer');
}
if (subgroup === 'burgIcons') return tip('Click to edit the Burg');
if (subgroup === 'burgLabels') return tip('Click to edit the Burg');
if (group === 'lakes' && !land) {
const lakeId = +e.target.dataset.f;
const name = pack.features[lakeId]?.name;
@ -197,20 +166,16 @@ function showMapTooltip(point, e, i, g) {
tip(`${fullName} lake. Click to edit`);
return;
}
if (group === 'coastline') {
tip('Click to edit the coastline');
return;
}
if (group === 'coastline') return tip('Click to edit the coastline');
if (group === 'zones') {
const zone = path[path.length - 8];
tip(zone.dataset.description);
if (zonesEditor.offsetParent) highlightEditorLine(zonesEditor, zone.id, 5000);
return;
}
if (group === 'ice') {
tip('Click to edit the Ice');
return;
}
if (group === 'ice') return tip('Click to edit the Ice');
// covering elements
if (layerIsOn('togglePrec') && land) tip('Annual Precipitation: ' + getFriendlyPrecipitation(i));

View file

@ -1,4 +1,3 @@
// heightmap-editor module. To be added to window as for now
'use strict';
function editHeightmap() {
@ -134,15 +133,8 @@ function editHeightmap() {
// Exit customization mode
function finalizeHeightmap() {
if (viewbox.select('#heights').selectAll('*').size() < 200) {
tip('Insufficient land area! There should be at least 200 land cells to finalize the heightmap', null, 'error');
return;
}
if (document.getElementById('imageConverter').offsetParent) {
tip('Please exit the Image Conversion mode first', null, 'error');
return;
}
if (viewbox.select('#heights').selectAll('*').size() < 200) return tip('Insufficient land area! There should be at least 200 land cells to finalize the heightmap', null, 'error');
if (document.getElementById('imageConverter').offsetParent) return tip('Please exit the Image Conversion mode first', null, 'error');
delete window.edits; // remove global variable
redo.disabled = templateRedo.disabled = true;
@ -207,6 +199,7 @@ function editHeightmap() {
}
}
drawRivers();
Lakes.defineGroup();
defineBiomes();
@ -586,6 +579,7 @@ function editHeightmap() {
document.getElementById('brushesSliders').style.display = 'none';
}
const dragBrushThrottled = throttle(dragBrush, 100);
function toggleBrushMode(e) {
if (e.target.classList.contains('pressed')) {
exitBrushMode();
@ -594,7 +588,7 @@ function editHeightmap() {
exitBrushMode();
document.getElementById('brushesSliders').style.display = 'block';
e.target.classList.add('pressed');
viewbox.style('cursor', 'crosshair').call(d3.drag().on('start', dragBrush));
viewbox.style('cursor', 'crosshair').call(d3.drag().on('start', dragBrushThrottled));
}
function dragBrush() {
@ -842,118 +836,15 @@ function editHeightmap() {
body.setAttribute('data-changed', 0);
body.innerHTML = '';
if (template === 'templateVolcano') {
addStep('Hill', '1', '90-100', '44-56', '40-60');
addStep('Multiply', 0.8, '50-100');
addStep('Range', '1.5', '30-55', '45-55', '40-60');
addStep('Smooth', 2);
addStep('Hill', '1.5', '25-35', '25-30', '20-75');
addStep('Hill', '1', '25-35', '75-80', '25-75');
addStep('Hill', '0.5', '20-25', '10-15', '20-25');
} else if (template === 'templateHighIsland') {
addStep('Hill', '1', '90-100', '65-75', '47-53');
addStep('Add', 5, 'all');
addStep('Hill', '6', '20-23', '25-55', '45-55');
addStep('Range', '1', '40-50', '45-55', '45-55');
addStep('Smooth', 2);
addStep('Trough', '2-3', '20-30', '20-30', '20-30');
addStep('Trough', '2-3', '20-30', '60-80', '70-80');
addStep('Hill', '1', '10-15', '60-60', '50-50');
addStep('Hill', '1.5', '13-16', '15-20', '20-75');
addStep('Multiply', 0.8, '20-100');
addStep('Range', '1.5', '30-40', '15-85', '30-40');
addStep('Range', '1.5', '30-40', '15-85', '60-70');
addStep('Pit', '2-3', '10-15', '15-85', '20-80');
} else if (template === 'templateLowIsland') {
addStep('Hill', '1', '90-99', '60-80', '45-55');
addStep('Hill', '4-5', '25-35', '20-65', '40-60');
addStep('Range', '1', '40-50', '45-55', '45-55');
addStep('Smooth', 3);
addStep('Trough', '1.5', '20-30', '15-85', '20-30');
addStep('Trough', '1.5', '20-30', '15-85', '70-80');
addStep('Hill', '1.5', '10-15', '5-15', '20-80');
addStep('Hill', '1', '10-15', '85-95', '70-80');
addStep('Pit', '3-5', '10-15', '15-85', '20-80');
addStep('Multiply', 0.4, '20-100');
} else if (template === 'templateContinents') {
addStep('Hill', '1', '80-85', '75-80', '40-60');
addStep('Hill', '1', '80-85', '20-25', '40-60');
addStep('Multiply', 0.22, '20-100');
addStep('Hill', '5-6', '15-20', '25-75', '20-82');
addStep('Range', '.8', '30-60', '5-15', '20-45');
addStep('Range', '.8', '30-60', '5-15', '55-80');
addStep('Range', '0-3', '30-60', '80-90', '20-80');
addStep('Trough', '3-4', '15-20', '15-85', '20-80');
addStep('Strait', '2', 'vertical');
addStep('Smooth', 2);
addStep('Trough', '1-2', '5-10', '45-55', '45-55');
addStep('Pit', '3-4', '10-15', '15-85', '20-80');
addStep('Hill', '1', '5-10', '40-60', '40-60');
} else if (template === 'templateArchipelago') {
addStep('Add', 11, 'all');
addStep('Range', '2-3', '40-60', '20-80', '20-80');
addStep('Hill', '5', '15-20', '10-90', '30-70');
addStep('Hill', '2', '10-15', '10-30', '20-80');
addStep('Hill', '2', '10-15', '60-90', '20-80');
addStep('Smooth', 3);
addStep('Trough', '10', '20-30', '5-95', '5-95');
addStep('Strait', '2', 'vertical');
addStep('Strait', '2', 'horizontal');
} else if (template === 'templateAtoll') {
addStep('Hill', '1', '75-80', '50-60', '45-55');
addStep('Hill', '1.5', '30-50', '25-75', '30-70');
addStep('Hill', '.5', '30-50', '25-35', '30-70');
addStep('Smooth', 1);
addStep('Multiply', 0.2, '25-100');
addStep('Hill', '.5', '10-20', '50-55', '48-52');
} else if (template === 'templateMediterranean') {
addStep('Range', '3-4', '30-50', '0-100', '0-10');
addStep('Range', '3-4', '30-50', '0-100', '90-100');
addStep('Hill', '5-6', '30-70', '0-100', '0-5');
addStep('Hill', '5-6', '30-70', '0-100', '95-100');
addStep('Smooth', 1);
addStep('Hill', '2-3', '30-70', '0-5', '20-80');
addStep('Hill', '2-3', '30-70', '95-100', '20-80');
addStep('Multiply', 0.8, 'land');
addStep('Trough', '3-5', '40-50', '0-100', '0-10');
addStep('Trough', '3-5', '40-50', '0-100', '90-100');
} else if (template === 'templatePeninsula') {
addStep('Range', '2-3', '20-35', '40-50', '0-15');
addStep('Add', 5, 'all');
addStep('Hill', '1', '90-100', '10-90', '0-5');
addStep('Add', 13, 'all');
addStep('Hill', '3-4', '3-5', '5-95', '80-100');
addStep('Hill', '1-2', '3-5', '5-95', '40-60');
addStep('Trough', '5-6', '10-25', '5-95', '5-95');
addStep('Smooth', 3);
} else if (template === 'templatePangea') {
addStep('Hill', '1-2', '25-40', '15-50', '0-10');
addStep('Hill', '1-2', '5-40', '50-85', '0-10');
addStep('Hill', '1-2', '25-40', '50-85', '90-100');
addStep('Hill', '1-2', '5-40', '15-50', '90-100');
addStep('Hill', '8-12', '20-40', '20-80', '48-52');
addStep('Smooth', 2);
addStep('Multiply', 0.7, 'land');
addStep('Trough', '3-4', '25-35', '5-95', '10-20');
addStep('Trough', '3-4', '25-35', '5-95', '80-90');
addStep('Range', '5-6', '30-40', '10-90', '35-65');
} else if (template === 'templateIsthmus') {
addStep('Hill', '5-10', '15-30', '0-30', '0-20');
addStep('Hill', '5-10', '15-30', '10-50', '20-40');
addStep('Hill', '5-10', '15-30', '30-70', '40-60');
addStep('Hill', '5-10', '15-30', '50-90', '60-80');
addStep('Hill', '5-10', '15-30', '70-100', '80-100');
addStep('Smooth', 2);
addStep('Trough', '4-8', '15-30', '0-30', '0-20');
addStep('Trough', '4-8', '15-30', '10-50', '20-40');
addStep('Trough', '4-8', '15-30', '30-70', '40-60');
addStep('Trough', '4-8', '15-30', '50-90', '60-80');
addStep('Trough', '4-8', '15-30', '70-100', '80-100');
} else if (template === 'templateShattered') {
addStep('Hill', '8', '35-40', '15-85', '30-70');
addStep('Trough', '10-20', '40-50', '5-95', '5-95');
addStep('Range', '5-7', '30-40', '10-90', '20-80');
addStep('Pit', '12-20', '30-40', '15-85', '20-80');
const templateString = HeightmapTemplates[template];
if (!templateString) return;
const steps = templateString.split('\n');
if (!steps.length) return tip(`Heightmap template: no steps defined`, false, 'error');
for (const step of steps) {
const elements = step.trim().split(' ');
addStep(...elements);
}
}
@ -1119,6 +1010,10 @@ function editHeightmap() {
const reader = new FileReader();
const img = new Image();
img.id = 'imageToConvert';
img.style.display = 'none';
document.body.appendChild(img);
img.onload = function () {
const ctx = document.getElementById('canvas').getContext('2d');
ctx.drawImage(img, 0, 0, graphWidth, graphHeight);
@ -1311,10 +1206,7 @@ function editHeightmap() {
}
function applyConversion() {
if (colorsAssigned.childElementCount < 3) {
tip('Please do the assignment first', false, 'error');
return;
}
if (colorsAssigned.childElementCount < 3) return tip('Please do the assignment first', false, 'error');
viewbox
.select('#heights')
@ -1340,6 +1232,9 @@ function editHeightmap() {
const canvas = document.getElementById('canvas');
if (canvas) canvas.remove();
const image = document.getElementById('imageToConvert');
if (image) image.remove();
d3.select('#imageConverter').selectAll('div.color-div').remove();
colorsAssigned.style.display = 'none';
colorsUnassigned.style.display = 'none';

View file

@ -123,9 +123,10 @@ function restoreLayers() {
if (layerIsOn('toggleIce')) drawIce();
if (layerIsOn('toggleEmblems')) drawEmblems();
// states are getting rendered each time, if it's not required than layers should be hidden
if (!layerIsOn('toggleBorders')) $('#borders').fadeOut();
if (!layerIsOn('toggleStates')) regions.style('display', 'none').selectAll('path').remove();
// some layers are rendered each time, remove them if they are not on
if (!layerIsOn('toggleBorders')) borders.selectAll('path').remove();
if (!layerIsOn('toggleStates')) regions.selectAll('path').remove();
if (!layerIsOn('toggleRivers')) rivers.selectAll('*').remove();
}
function toggleHeight(event) {
@ -876,35 +877,80 @@ function toggleStates(event) {
}
}
// draw states
function drawStates() {
TIME && console.time('drawStates');
regions.selectAll('path').remove();
const cells = pack.cells,
vertices = pack.vertices,
states = pack.states,
n = cells.i.length;
const {cells, vertices, features} = pack;
const states = pack.states;
const n = cells.i.length;
const used = new Uint8Array(cells.i.length);
const vArray = new Array(states.length); // store vertices array
const body = new Array(states.length).fill(''); // store path around each state
const gap = new Array(states.length).fill(''); // store path along water for each state to fill the gaps
const body = new Array(states.length).fill(''); // path around each state
const gap = new Array(states.length).fill(''); // path along water for each state to fill the gaps
const halo = new Array(states.length).fill(''); // path around states, but not lakes
const getStringPoint = (v) => vertices.p[v[0]].join(',');
// define inner-state lakes to omit on border render
const innerLakes = features.map((feature) => {
if (feature.type !== 'lake') return false;
if (!feature.shoreline) Lakes.getShoreline(feature);
const states = feature.shoreline.map((i) => cells.state[i]);
return new Set(states).size > 1 ? false : true;
});
for (const i of cells.i) {
if (!cells.state[i] || used[i]) continue;
const s = cells.state[i];
const onborder = cells.c[i].some((n) => cells.state[n] !== s);
const state = cells.state[i];
const onborder = cells.c[i].some((n) => cells.state[n] !== state);
if (!onborder) continue;
const borderWith = cells.c[i].map((c) => cells.state[c]).find((n) => n !== s);
const borderWith = cells.c[i].map((c) => cells.state[c]).find((n) => n !== state);
const vertex = cells.v[i].find((v) => vertices.c[v].some((i) => cells.state[i] === borderWith));
const chain = connectVertices(vertex, s, borderWith);
if (chain.length < 3) continue;
const points = chain.map((v) => vertices.p[v[0]]);
if (!vArray[s]) vArray[s] = [];
vArray[s].push(points);
body[s] += 'M' + points.join('L');
gap[s] += 'M' + vertices.p[chain[0][0]] + chain.reduce((r, v, i, d) => (!i ? r : !v[2] ? r + 'L' + vertices.p[v[0]] : d[i + 1] && !d[i + 1][2] ? r + 'M' + vertices.p[v[0]] : r), '');
const chain = connectVertices(vertex, state);
const noInnerLakes = chain.filter((v) => v[1] !== 'innerLake');
if (noInnerLakes.length < 3) continue;
// get path around the state
if (!vArray[state]) vArray[state] = [];
const points = noInnerLakes.map((v) => vertices.p[v[0]]);
vArray[state].push(points);
body[state] += 'M' + points.join('L');
// connect path for halo
let discontinued = true;
halo[state] += noInnerLakes
.map((v) => {
if (v[1] === 'border') {
discontinued = true;
return '';
}
const operation = discontinued ? 'M' : 'L';
discontinued = false;
return `${operation}${getStringPoint(v)}`;
})
.join('');
// connect gaps between state and water into a single path
discontinued = true;
gap[state] += chain
.map((v) => {
if (v[1] === 'land') {
discontinued = true;
return '';
}
const operation = discontinued ? 'M' : 'L';
discontinued = false;
return `${operation}${getStringPoint(v)}`;
})
.join('');
}
// find state visual center
@ -913,13 +959,14 @@ function drawStates() {
states[i].pole = polylabel(sorted, 1.0); // pole of inaccessibility
});
const bodyData = body.map((p, i) => [p.length > 10 ? p : null, i, states[i].color]).filter((d) => d[0]);
const gapData = gap.map((p, i) => [p.length > 10 ? p : null, i, states[i].color]).filter((d) => d[0]);
const bodyData = body.map((p, s) => [p.length > 10 ? p : null, s, states[s].color]).filter((d) => d[0]);
const gapData = gap.map((p, s) => [p.length > 10 ? p : null, s, states[s].color]).filter((d) => d[0]);
const haloData = halo.map((p, s) => [p.length > 10 ? p : null, s, states[s].color]).filter((d) => d[0]);
const bodyString = bodyData.map((d) => `<path id="state${d[1]}" d="${d[0]}" fill="${d[2]}" stroke="none"/>`).join('');
const gapString = gapData.map((d) => `<path id="state-gap${d[1]}" d="${d[0]}" fill="none" stroke="${d[2]}"/>`).join('');
const clipString = bodyData.map((d) => `<clipPath id="state-clip${d[1]}"><use href="#state${d[1]}"/></clipPath>`).join('');
const haloString = bodyData
const haloString = haloData
.map((d) => `<path id="state-border${d[1]}" d="${d[0]}" clip-path="url(#state-clip${d[1]})" stroke="${d3.color(d[2]) ? d3.color(d[2]).darker().hex() : '#666666'}"/>`)
.join('');
@ -928,57 +975,77 @@ function drawStates() {
statesHalo.html(haloString);
// connect vertices to chain
function connectVertices(start, t, state) {
function connectVertices(start, state) {
const chain = []; // vertices chain to form a path
let land = vertices.c[start].some((c) => cells.h[c] >= 20 && cells.state[c] !== t);
function check(i) {
state = cells.state[i];
land = cells.h[i] >= 20;
}
const getType = (c) => {
const borderCell = c.find((i) => cells.b[i]);
if (borderCell) return 'border';
const waterCell = c.find((i) => cells.h[i] < 20);
if (!waterCell) return 'land';
if (innerLakes[cells.f[waterCell]]) return 'innerLake';
return features[cells.f[waterCell]].type;
};
for (let i = 0, current = start; i === 0 || (current !== start && i < 20000); i++) {
const prev = chain[chain.length - 1] ? chain[chain.length - 1][0] : -1; // previous vertex in chain
chain.push([current, state, land]); // add current vertex to sequence
const prev = chain.length ? chain[chain.length - 1][0] : -1; // previous vertex in chain
const c = vertices.c[current]; // cells adjacent to vertex
c.filter((c) => cells.state[c] === t).forEach((c) => (used[c] = 1));
const c0 = c[0] >= n || cells.state[c[0]] !== t;
const c1 = c[1] >= n || cells.state[c[1]] !== t;
const c2 = c[2] >= n || cells.state[c[2]] !== t;
chain.push([current, getType(c)]); // add current vertex to sequence
c.filter((c) => cells.state[c] === state).forEach((c) => (used[c] = 1));
const c0 = c[0] >= n || cells.state[c[0]] !== state;
const c1 = c[1] >= n || cells.state[c[1]] !== state;
const c2 = c[2] >= n || cells.state[c[2]] !== state;
const v = vertices.v[current]; // neighboring vertices
if (v[0] !== prev && c0 !== c1) {
current = v[0];
check(c0 ? c[0] : c[1]);
} else if (v[1] !== prev && c1 !== c2) {
current = v[1];
check(c1 ? c[1] : c[2]);
} else if (v[2] !== prev && c0 !== c2) {
current = v[2];
check(c2 ? c[2] : c[0]);
}
if (current === chain[chain.length - 1][0]) {
if (v[0] !== prev && c0 !== c1) current = v[0];
else if (v[1] !== prev && c1 !== c2) current = v[1];
else if (v[2] !== prev && c0 !== c2) current = v[2];
if (current === prev) {
ERROR && console.error('Next vertex is not found');
break;
}
}
chain.push([start, state, land]); // add starting vertex to sequence to close the path
if (chain.length) chain.push(chain[0]);
return chain;
}
invokeActiveZooming();
TIME && console.timeEnd('drawStates');
}
function toggleBorders(event) {
if (!layerIsOn('toggleBorders')) {
turnButtonOn('toggleBorders');
drawBorders();
if (event && isCtrlClick(event)) editStyle('borders');
} else {
if (event && isCtrlClick(event)) {
editStyle('borders');
return;
}
turnButtonOff('toggleBorders');
borders.selectAll('path').remove();
}
}
// draw state and province borders
function drawBorders() {
TIME && console.time('drawBorders');
borders.selectAll('path').remove();
const cells = pack.cells,
vertices = pack.vertices,
n = cells.i.length;
const sPath = [],
pPath = [];
const sUsed = new Array(pack.states.length).fill('').map((a) => []);
const pUsed = new Array(pack.provinces.length).fill('').map((a) => []);
const {cells, vertices} = pack;
const n = cells.i.length;
const sPath = [];
const pPath = [];
const sUsed = new Array(pack.states.length).fill('').map((_) => []);
const pUsed = new Array(pack.provinces.length).fill('').map((_) => []);
for (let i = 0; i < cells.i.length; i++) {
if (!cells.state[i]) continue;
@ -1070,21 +1137,6 @@ function drawBorders() {
TIME && console.timeEnd('drawBorders');
}
function toggleBorders(event) {
if (!layerIsOn('toggleBorders')) {
turnButtonOn('toggleBorders');
$('#borders').fadeIn();
if (event && isCtrlClick(event)) editStyle('borders');
} else {
if (event && isCtrlClick(event)) {
editStyle('borders');
return;
}
turnButtonOff('toggleBorders');
$('#borders').fadeOut();
}
}
function toggleProvinces(event) {
if (!layerIsOn('toggleProvinces')) {
turnButtonOn('toggleProvinces');
@ -1396,18 +1448,30 @@ function toggleTexture(event) {
function toggleRivers(event) {
if (!layerIsOn('toggleRivers')) {
turnButtonOn('toggleRivers');
$('#rivers').fadeIn();
drawRivers();
if (event && isCtrlClick(event)) editStyle('rivers');
} else {
if (event && isCtrlClick(event)) {
editStyle('rivers');
return;
}
$('#rivers').fadeOut();
if (event && isCtrlClick(event)) return editStyle('rivers');
rivers.selectAll('*').remove();
turnButtonOff('toggleRivers');
}
}
function drawRivers() {
TIME && console.time('drawRivers');
const {addMeandering, getRiverPath} = Rivers;
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
const riverPaths = pack.rivers.map((river) => {
const meanderedPoints = addMeandering(river.cells, river.points);
const widthFactor = river.widthFactor || 1;
const startingWidth = river.sourceWidth || 0;
const path = getRiverPath(meanderedPoints, widthFactor, startingWidth);
return `<path id="river${river.i}" d="${path}"/>`;
});
rivers.html(riverPaths.join(''));
TIME && console.timeEnd('drawRivers');
}
function toggleRoutes(event) {
if (!layerIsOn('toggleRoutes')) {
turnButtonOn('toggleRoutes');
@ -1558,21 +1622,21 @@ function drawEmblems() {
const getStateEmblemsSize = () => {
const startSize = Math.min(Math.max((graphHeight + graphWidth) / 40, 10), 100);
const statesMod = 1 + validStates.length / 100 - (15 - validStates.length) / 200; // states number modifier
const sizeMod = +document.getElementById('styleEmblemsStateSizeInput').value || 1;
const sizeMod = +document.getElementById('emblemsStateSizeInput').value || 1;
return rn((startSize / statesMod) * sizeMod); // target size ~50px on 1536x754 map with 15 states
};
const getProvinceEmblemsSize = () => {
const startSize = Math.min(Math.max((graphHeight + graphWidth) / 100, 5), 70);
const provincesMod = 1 + validProvinces.length / 1000 - (115 - validProvinces.length) / 1000; // states number modifier
const sizeMod = +document.getElementById('styleEmblemsProvinceSizeInput').value || 1;
const sizeMod = +document.getElementById('emblemsProvinceSizeInput').value || 1;
return rn((startSize / provincesMod) * sizeMod); // target size ~20px on 1536x754 map with 115 provinces
};
const getBurgEmblemSize = () => {
const startSize = Math.min(Math.max((graphHeight + graphWidth) / 185, 2), 50);
const burgsMod = 1 + validBurgs.length / 1000 - (450 - validBurgs.length) / 1000; // states number modifier
const sizeMod = +document.getElementById('styleEmblemsBurgSizeInput').value || 1;
const sizeMod = +document.getElementById('emblemsBurgSizeInput').value || 1;
return rn((startSize / burgsMod) * sizeMod); // target size ~8.5px on 1536x754 map with 450 burgs
};

View file

@ -98,7 +98,8 @@ function showSupporters() {
PlayByMail.Net,Brad Wardell,Lance Saba,Egoensis,Brea Richards,Tiber,Chris Bloom,Maxim Lowe,Aquelion,Page One Project,Spencer Morris,Paul Ingram,
Dust Bunny,Adrian Wright,Eric Alexander Cartaya,GameNight,Thomas Mortensen Hansen,Zklaus,Drinarius,Ed Wright,Lon Varnadore,Crys Cain,Heaven N Lee,
Jeffrey Henning,Lazer Elf,Jordan Bellah,Alex Beard,Kass Frisson,Petro Lombaard,Emanuel Pietri,Rox,PinkEvil,Gavin Madrigal,Martin Lorber,Prince of Morgoth,
Jaryd Armstrong,Andrew Pirkola,ThyHolyDevil,Gary Smith,Tyshaun Wise,Ethan Cook,Jon Stroman,Nobody679,良义 ,Chris Gray`;
Jaryd Armstrong,Andrew Pirkola,ThyHolyDevil,Gary Smith,Tyshaun Wise,Ethan Cook,Jon Stroman,Nobody679,良义 ,Chris Gray,Phoenix Boatwright,Mackenzie,
"Milo Cohen,Jason Matthew Wuerfel,Rasmus Legêne,Andrew Hines,Wexxler,Espen Sæverud,Binks,Dominick Ormsby,Linn Browning,Václav Švec,Alan Buehne,George J.Lekkas"`;
const array = supporters
.replace(/(?:\r\n|\r|\n)/g, '')
@ -157,6 +158,7 @@ optionsContent.addEventListener('change', function (event) {
if (id === 'zoomExtentMin' || id === 'zoomExtentMax') changeZoomExtent(value);
else if (id === 'optionsSeed') generateMapWithSeed();
else if (id === 'uiSizeInput' || id === 'uiSizeOutput') changeUIsize(value);
if (id === 'shapeRendering') viewbox.attr('shape-rendering', value);
else if (id === 'yearInput') changeYear();
else if (id === 'eraInput') changeEra();
});
@ -494,6 +496,9 @@ function applyStoredOptions() {
const height = +params.get('height');
if (width) mapWidthInput.value = width;
if (height) mapHeightInput.value = height;
// set shape rendering
viewbox.attr('shape-rendering', shapeRendering.value);
}
// randomize options if randomization is allowed (not locked or options='default')
@ -537,17 +542,18 @@ function randomizeOptions() {
// select heightmap template pseudo-randomly
function randomizeHeightmapTemplate() {
const templates = {
Volcano: 3,
'High Island': 22,
'Low Island': 9,
Continents: 20,
Archipelago: 25,
Mediterranean: 3,
Peninsula: 3,
Pangea: 5,
Isthmus: 2,
Atoll: 1,
Shattered: 7
volcano: 3,
highIsland: 22,
lowIsland: 9,
continents: 19,
archipelago: 23,
mediterranean: 5,
peninsula: 3,
pangea: 5,
isthmus: 2,
atoll: 1,
shattered: 7,
taklamakan: 1
};
document.getElementById('templateInput').value = rw(templates);
}
@ -773,6 +779,12 @@ document
.forEach((el) => el.addEventListener('input', updateTilesOptions));
function updateTilesOptions() {
if (this?.tagName === 'INPUT') {
const {nextElementSibling: next, previousElementSibling: prev} = this;
if (next?.tagName === 'INPUT') next.value = this.value;
if (prev?.tagName === 'INPUT') prev.value = this.value;
}
const tileSize = document.getElementById('tileSize');
const tilesX = +document.getElementById('tileColsOutput').value;
const tilesY = +document.getElementById('tileRowsOutput').value;
@ -908,6 +920,7 @@ function toggle3dOptions() {
document.getElementById('options3dMeshRotationNumber').addEventListener('change', changeRotation);
document.getElementById('options3dGlobeRotationRange').addEventListener('input', changeRotation);
document.getElementById('options3dGlobeRotationNumber').addEventListener('change', changeRotation);
document.getElementById('options3dMeshLabels3d').addEventListener('change', toggleLabels3d);
document.getElementById('options3dMeshSkyMode').addEventListener('change', toggleSkyMode);
document.getElementById('options3dMeshSky').addEventListener('input', changeColors);
document.getElementById('options3dMeshWater').addEventListener('input', changeColors);
@ -924,6 +937,7 @@ function toggle3dOptions() {
options3dSunZ.value = ThreeD.options.sun.z;
options3dMeshRotationRange.value = options3dMeshRotationNumber.value = ThreeD.options.rotateMesh;
options3dGlobeRotationRange.value = options3dGlobeRotationNumber.value = ThreeD.options.rotateGlobe;
options3dMeshLabels3d.value = ThreeD.options.labels3d;
options3dMeshSkyMode.value = ThreeD.options.extendedWater;
options3dColorSection.style.display = ThreeD.options.extendedWater ? 'block' : 'none';
options3dMeshSky.value = ThreeD.options.skyColor;
@ -954,6 +968,10 @@ function toggle3dOptions() {
ThreeD.setRotation(speed);
}
function toggleLabels3d() {
ThreeD.toggleLabels();
}
function toggleSkyMode() {
const hide = ThreeD.options.extendedWater;
options3dColorSection.style.display = hide ? 'none' : 'block';

View file

@ -81,6 +81,7 @@ function editReliefIcon() {
reliefSpacingDiv.style.display = 'none';
reliefIconsSeletionAny.style.display = 'none';
removeCircle();
updateReliefSizeInput();
restoreDefaultEvents();
clearMainTip();
@ -115,10 +116,7 @@ function editReliefIcon() {
function dragToAdd() {
const pressed = reliefIconsDiv.querySelector('svg.pressed');
if (!pressed) {
tip('Please select an icon', false, error);
return;
}
if (!pressed) return tip('Please select an icon', false, error);
const type = pressed.dataset.type;
const r = +reliefRadiusNumber.value;
@ -188,10 +186,7 @@ function editReliefIcon() {
function dragToRemove() {
const pressed = reliefIconsDiv.querySelector('svg.pressed');
if (!pressed) {
tip('Please select an icon', false, error);
return;
}
if (!pressed) return tip('Please select an icon', false, error);
const r = +reliefRadiusNumber.value;
const type = pressed.dataset.type;
@ -256,12 +251,32 @@ function editReliefIcon() {
}
function removeIcon() {
const message = 'Are you sure you want to remove the relief icon? <br>This action cannot be reverted';
const onConfirm = () => {
elSelected.remove();
$('#reliefEditor').dialog('close');
};
confirmationDialog({title: 'Remove relief icon', message, confirm: 'Remove', onConfirm});
let selection = null;
const pressed = reliefTools.querySelector('button.pressed');
if (pressed.id === 'reliefIndividual') {
alertMessage.innerHTML = `Are you sure you want to remove the icon?`;
selection = elSelected;
} else {
const type = reliefIconsDiv.querySelector('svg.pressed')?.dataset.type;
selection = type ? terrain.selectAll("use[href='" + type + "']") : terrain.selectAll('use');
const size = selection.size();
alertMessage.innerHTML = type ? `Are you sure you want to remove all ${type} icons (${size})?` : `Are you sure you want to remove all icons (${size})?`;
}
$('#alert').dialog({
resizable: false,
title: 'Remove relief icons',
buttons: {
Remove: function () {
if (selection) selection.remove();
$(this).dialog('close');
$('#reliefEditor').dialog('close');
},
Cancel: function () {
$(this).dialog('close');
}
}
});
}
function closeReliefEditor() {

View file

@ -0,0 +1,125 @@
"use strict";
function createRiver() {
if (customization) return;
closeDialogs();
if (!layerIsOn("toggleRivers")) toggleRivers();
document.getElementById("toggleCells").dataset.forced = +!layerIsOn("toggleCells");
if (!layerIsOn("toggleCells")) toggleCells();
tip("Click to add river point, click again to remove", true);
debug.append("g").attr("id", "controlCells");
viewbox.style("cursor", "crosshair").on("click", onCellClick);
createRiver.cells = [];
const body = document.getElementById("riverCreatorBody");
$("#riverCreator").dialog({
title: "Create River",
resizable: false,
position: {my: "left top", at: "left+10 top+10", of: "#map"},
close: closeRiverCreator
});
if (modules.createRiver) return;
modules.createRiver = true;
// add listeners
document.getElementById("riverCreatorComplete").addEventListener("click", addRiver);
document.getElementById("riverCreatorCancel").addEventListener("click", () => $("#riverCreator").dialog("close"));
body.addEventListener("click", function (ev) {
const el = ev.target;
const cl = el.classList;
const cell = +el.parentNode.dataset.cell;
if (cl.contains("editFlux")) pack.cells.fl[cell] = +el.value;
else if (cl.contains("icon-trash-empty")) removeCell(cell);
});
function onCellClick() {
const cell = findCell(...d3.mouse(this));
if (createRiver.cells.includes(cell)) removeCell(cell);
else addCell(cell);
}
function addCell(cell) {
createRiver.cells.push(cell);
drawCells(createRiver.cells);
const flux = pack.cells.fl[cell];
const line = `<div class="editorLine" data-cell="${cell}">
<span>Cell ${cell}</span>
<span data-tip="Set flux affects river width" style="margin-left: 0.4em">Flux</span>
<input type="number" min=0 value="${flux}" class="editFlux" style="width: 5em"/>
<span data-tip="Remove the cell" class="icon-trash-empty pointer"></span>
</div>`;
body.innerHTML += line;
}
function removeCell(cell) {
createRiver.cells = createRiver.cells.filter(c => c !== cell);
drawCells(createRiver.cells);
body.querySelector(`div[data-cell='${cell}']`)?.remove();
}
function drawCells(cells) {
debug
.select("#controlCells")
.selectAll(`polygon`)
.data(cells)
.join("polygon")
.attr("points", d => getPackPolygon(d))
.attr("class", "current");
}
function addRiver() {
const {rivers, cells} = pack;
const {addMeandering, getApproximateLength, getWidth, getOffset, getName, getRiverPath, getBasin} = Rivers;
const riverCells = createRiver.cells;
if (riverCells.length < 2) return tip("Add at least 2 cells", false, "error");
const riverId = rivers.length ? last(rivers).i + 1 : 1;
const parent = cells.r[last(riverCells)] || riverId;
riverCells.forEach(cell => {
if (!cells.r[cell]) cells.r[cell] = riverId;
});
const source = riverCells[0];
const mouth = parent === riverId ? last(riverCells) : riverCells[riverCells.length - 2];
const sourceWidth = 0.05;
const widthFactor = 1.2;
const meanderedPoints = addMeandering(riverCells);
const discharge = cells.fl[mouth]; // m3 in second
const length = getApproximateLength(meanderedPoints);
const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, sourceWidth));
const name = getName(mouth);
const basin = getBasin(parent);
rivers.push({i: riverId, source, mouth, discharge, length, width, widthFactor, sourceWidth, parent, cells: riverCells, basin, name, type: "River"});
// render river
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
viewbox
.select("#rivers")
.append("path")
.attr("id", "river" + riverId)
.attr("d", getRiverPath(meanderedPoints, widthFactor, sourceWidth));
editRiver(riverId);
}
function closeRiverCreator() {
body.innerHTML = "";
debug.select("#controlCells").remove();
restoreDefaultEvents();
clearMainTip();
const forced = +document.getElementById("toggleCells").dataset.forced;
document.getElementById("toggleCells").dataset.forced = 0;
if (forced && layerIsOn("toggleCells")) toggleCells();
}
}

View file

@ -1,21 +1,31 @@
'use strict';
function editRiver(id) {
if (customization) return;
if (elSelected && d3.event && d3.event.target.id === elSelected.attr('id')) return;
if (elSelected && id === elSelected.attr('id')) return;
closeDialogs('.stable');
if (!layerIsOn('toggleRivers')) toggleRivers();
const node = id ? document.getElementById(id) : d3.event.target;
elSelected = d3.select(node).on('click', addInterimControlPoint);
viewbox.on('touchmove mousemove', showEditorTips);
debug.append('g').attr('id', 'controlPoints').attr('transform', elSelected.attr('transform'));
document.getElementById('toggleCells').dataset.forced = +!layerIsOn('toggleCells');
if (!layerIsOn('toggleCells')) toggleCells();
elSelected = d3.select('#' + id);
tip('Drag control points to change the river course. For major changes please create a new river instead', true);
debug.append('g').attr('id', 'controlCells');
debug.append('g').attr('id', 'controlPoints');
updateRiverData();
drawControlPoints(node);
const river = getRiver();
const {cells, points} = river;
const riverPoints = Rivers.getRiverPoints(cells, points);
drawControlPoints(riverPoints, cells);
drawCells(cells, 'current');
$('#riverEditor').dialog({
title: 'Edit River',
resizable: false,
position: {my: 'center top+80', at: 'top', of: node, collision: 'fit'},
position: {my: 'left top', at: 'left+10 top+10', of: '#map'},
close: closeRiverEditor
});
@ -23,27 +33,19 @@ function editRiver(id) {
modules.editRiver = true;
// add listeners
document.getElementById('riverCreateSelectingCells').addEventListener('click', createRiver);
document.getElementById('riverEditStyle').addEventListener('click', () => editStyle('rivers'));
document.getElementById('riverElevationProfile').addEventListener('click', showElevationProfile);
document.getElementById('riverLegend').addEventListener('click', editRiverLegend);
document.getElementById('riverRemove').addEventListener('click', removeRiver);
document.getElementById('riverName').addEventListener('input', changeName);
document.getElementById('riverType').addEventListener('input', changeType);
document.getElementById('riverNameCulture').addEventListener('click', generateNameCulture);
document.getElementById('riverNameRandom').addEventListener('click', generateNameRandom);
document.getElementById('riverMainstem').addEventListener('change', changeParent);
document.getElementById('riverSourceWidth').addEventListener('input', changeSourceWidth);
document.getElementById('riverWidthFactor').addEventListener('input', changeWidthFactor);
document.getElementById('riverNew').addEventListener('click', toggleRiverCreationMode);
document.getElementById('riverEditStyle').addEventListener('click', () => editStyle('rivers'));
document.getElementById('riverElevationProfile').addEventListener('click', showElevationProfile);
document.getElementById('riverLegend').addEventListener('click', editRiverLegend);
document.getElementById('riverRemove').addEventListener('click', removeRiver);
function showEditorTips() {
showMainTip();
if (d3.event.target.parentNode.id === elSelected.attr('id')) tip('Drag to move, 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 getRiver() {
const riverId = +elSelected.attr('id').slice(5);
const river = pack.rivers.find((r) => r.i === riverId);
@ -67,87 +69,107 @@ function editRiver(id) {
document.getElementById('riverBasin').value = pack.rivers.find((river) => river.i === r.basin).name;
document.getElementById('riverDischarge').value = r.discharge + ' m³/s';
r.length = elSelected.node().getTotalLength() / 2;
const length = rn(r.length * distanceScaleInput.value) + ' ' + distanceUnitInput.value;
document.getElementById('riverLength').value = length;
const width = rn(r.width * distanceScaleInput.value, 3) + ' ' + distanceUnitInput.value;
document.getElementById('riverWidth').value = width;
document.getElementById('riverSourceWidth').value = r.sourceWidth;
document.getElementById('riverWidthFactor').value = r.widthFactor;
updateRiverLength(r);
updateRiverWidth(r);
}
function drawControlPoints(node) {
const length = getRiver().length;
const segments = Math.ceil(length / 4);
const increment = rn((length / segments) * 1e5);
for (let i = increment * segments, c = i; i >= 0; i -= increment, c += increment) {
const p1 = node.getPointAtLength(i / 1e5);
const p2 = node.getPointAtLength(c / 1e5);
addControlPoint([(p1.x + p2.x) / 2, (p1.y + p2.y) / 2]);
}
function updateRiverLength(river) {
river.length = rn(elSelected.node().getTotalLength() / 2, 2);
const lengthUI = `${rn(river.length * distanceScaleInput.value)} ${distanceUnitInput.value}`;
document.getElementById('riverLength').value = lengthUI;
}
function addControlPoint(point, before = null) {
debug.select('#controlPoints').insert('circle', before).attr('cx', point[0]).attr('cy', point[1]).attr('r', 0.6).call(d3.drag().on('drag', dragControlPoint)).on('click', clickControlPoint);
function updateRiverWidth(river) {
const {addMeandering, getWidth, getOffset} = Rivers;
const {cells, discharge, widthFactor, sourceWidth} = river;
const meanderedPoints = addMeandering(cells);
river.width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, sourceWidth));
const width = `${rn(river.width * distanceScaleInput.value, 3)} ${distanceUnitInput.value}`;
document.getElementById('riverWidth').value = width;
}
function dragControlPoint() {
this.setAttribute('cx', d3.event.x);
this.setAttribute('cy', d3.event.y);
redrawRiver();
}
function redrawRiver() {
const points = [];
function drawControlPoints(points, cells) {
debug
.select('#controlPoints')
.selectAll('circle')
.each(function () {
points.push([+this.getAttribute('cx'), +this.getAttribute('cy')]);
});
.data(points)
.enter()
.append('circle')
.attr('cx', (d) => d[0])
.attr('cy', (d) => d[1])
.attr('r', 0.6)
.attr('data-cell', (d, i) => cells[i])
.attr('data-i', (d, i) => i)
.call(d3.drag().on('start', dragControlPoint));
}
if (points.length < 2) return;
if (points.length === 2) {
const p0 = points[0],
p1 = points[1];
const angle = Math.atan2(p1[1] - p0[1], p1[0] - p0[0]);
const sin = Math.sin(angle),
cos = Math.cos(angle);
elSelected.attr('d', `M${p0[0]},${p0[1]} L${p1[0]},${p1[1]} l${-sin / 2},${cos / 2} Z`);
return;
}
function drawCells(cells, type) {
debug
.select('#controlCells')
.selectAll(`polygon.${type}`)
.data(cells.filter((i) => pack.cells.i[i]))
.join('polygon')
.attr('points', (d) => getPackPolygon(d))
.attr('class', type);
}
const widthFactor = +document.getElementById('riverWidthFactor').value;
const sourceWidth = +document.getElementById('riverSourceWidth').value;
const [path, length, offset] = Rivers.getPath(points, widthFactor, sourceWidth);
function dragControlPoint() {
const {i, r, fl} = pack.cells;
const river = getRiver();
const initCell = +this.dataset.cell;
const index = +this.dataset.i;
let movedToCell = null;
d3.event.on('drag', function () {
const {x, y} = d3.event;
const currentCell = findCell(x, y);
movedToCell = initCell !== currentCell ? currentCell : null;
this.setAttribute('cx', x);
this.setAttribute('cy', y);
this.__data__ = [rn(x, 1), rn(y, 1)];
redrawRiver();
});
d3.event.on('end', () => {
if (movedToCell) {
this.dataset.cell = movedToCell;
river.cells[index] = movedToCell;
drawCells(river.cells, 'current');
if (!r[movedToCell]) {
// swap river data
r[initCell] = 0;
r[movedToCell] = river.i;
const sourceFlux = fl[initCell];
fl[initCell] = fl[movedToCell];
fl[movedToCell] = sourceFlux;
}
}
});
}
function redrawRiver() {
const river = getRiver();
river.points = debug.selectAll('#controlPoints > *').data();
const {cells, widthFactor, sourceWidth} = river;
const meanderedPoints = Rivers.addMeandering(cells, river.points);
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
const path = Rivers.getRiverPath(meanderedPoints, widthFactor, sourceWidth);
elSelected.attr('d', path);
const r = getRiver();
if (r) {
r.width = rn(offset ** 2, 2);
r.length = length;
updateRiverData();
}
updateRiverLength(river);
if (modules.elevation) showEPForRiver(elSelected.node());
}
function clickControlPoint() {
this.remove();
redrawRiver();
}
function addInterimControlPoint() {
const point = d3.mouse(this);
const controls = document.getElementById('controlPoints').querySelectorAll('circle');
const points = Array.from(controls).map((circle) => [+circle.getAttribute('cx'), +circle.getAttribute('cy')]);
const index = getSegmentId(points, point, 2);
addControlPoint(point, ':nth-child(' + (index + 1) + ')');
redrawRiver();
}
function changeName() {
getRiver().name = this.value;
}
@ -174,12 +196,16 @@ function editRiver(id) {
}
function changeSourceWidth() {
getRiver().sourceWidth = +this.value;
const river = getRiver();
river.sourceWidth = +this.value;
updateRiverWidth(river);
redrawRiver();
}
function changeWidthFactor() {
getRiver().widthFactor = +this.value;
const river = getRiver();
river.widthFactor = +this.value;
updateRiverWidth(river);
redrawRiver();
}
@ -194,81 +220,35 @@ function editRiver(id) {
editNotes(id, river.name + ' ' + river.type);
}
function toggleRiverCreationMode() {
if (document.getElementById('riverNew').classList.contains('pressed')) exitRiverCreationMode();
else {
document.getElementById('riverNew').classList.add('pressed');
tip('Click on map to add control points', true, 'warn');
viewbox.on('click', addPointOnClick).style('cursor', 'crosshair');
elSelected.on('click', null);
}
}
function addPointOnClick() {
if (!elSelected.attr('data-new')) {
debug.select('#controlPoints').selectAll('circle').remove();
const id = getNextId('river');
elSelected = d3.select(elSelected.node().parentNode).append('path').attr('id', id).attr('data-new', 1);
}
// add control point
const point = d3.mouse(this);
addControlPoint([point[0], point[1]]);
redrawRiver();
}
function exitRiverCreationMode() {
riverNew.classList.remove('pressed');
clearMainTip();
viewbox.on('click', clicked).style('cursor', 'default');
elSelected.on('click', addInterimControlPoint);
if (!elSelected.attr('data-new')) return; // no need to create a new river
elSelected.attr('data-new', null);
// add a river
const r = +elSelected.attr('id').slice(5);
const node = elSelected.node(),
length = node.getTotalLength() / 2;
const cells = [];
const segments = Math.ceil(length / 4),
increment = rn((length / segments) * 1e5);
for (let i = increment * segments, c = i; i >= 0; i -= increment, c += increment) {
const p = node.getPointAtLength(i / 1e5);
const cell = findCell(p.x, p.y);
if (!pack.cells.r[cell]) pack.cells.r[cell] = r;
cells.push(cell);
}
const source = cells[0],
mouth = last(cells);
const name = Rivers.getName(mouth);
const smallLength = pack.rivers.map((r) => r.length || 0).sort((a, b) => a - b)[Math.ceil(pack.rivers.length * 0.15)];
const type = length < smallLength ? rw({Creek: 9, River: 3, Brook: 3, Stream: 1}) : 'River';
const discharge = rn(cells.length * 20 * Math.random());
const widthFactor = +document.getElementById('riverWidthFactor').value;
const sourceWidth = +document.getElementById('riverSourceWidth').value;
pack.rivers.push({i: r, source, mouth, discharge, length, width: sourceWidth, widthFactor, sourceWidth, parent: 0, name, type, basin: r});
}
function removeRiver() {
const message = 'Are you sure you want to remove the river? <br>All tributaries will be auto-removed';
const onConfirm = () => {
const river = +elSelected.attr('id').slice(5);
Rivers.remove(river);
elSelected.remove(); // if river if missed in pack.rivers
$('#riverEditor').dialog('close');
};
confirmationDialog({title: 'Remove river', message, confirm: 'Remove', onConfirm});
alertMessage.innerHTML = 'Are you sure you want to remove the river and all its tributaries';
$('#alert').dialog({
resizable: false,
width: '22em',
title: 'Remove river and tributaries',
buttons: {
Remove: function () {
$(this).dialog('close');
const river = +elSelected.attr('id').slice(5);
Rivers.remove(river);
elSelected.remove();
$('#riverEditor').dialog('close');
},
Cancel: function () {
$(this).dialog('close');
}
}
});
}
function closeRiverEditor() {
exitRiverCreationMode();
elSelected.on('click', null);
debug.select('#controlPoints').remove();
debug.select('#controlCells').remove();
unselect();
clearMainTip();
const forced = +document.getElementById('toggleCells').dataset.forced;
document.getElementById('toggleCells').dataset.forced = 0;
if (forced && layerIsOn('toggleCells')) toggleCells();
}
}

View file

@ -21,6 +21,7 @@ function overviewRivers() {
// add listeners
document.getElementById('riversOverviewRefresh').addEventListener('click', riversOverviewAddLines);
document.getElementById('addNewRiver').addEventListener('click', toggleAddRiver);
document.getElementById('riverCreateNew').addEventListener('click', createRiver);
document.getElementById('riversBasinHighlight').addEventListener('click', toggleBasinsHightlight);
document.getElementById('riversExport').addEventListener('click', downloadRiversData);
document.getElementById('riversRemoveAll').addEventListener('click', triggerAllRiversRemove);
@ -129,27 +130,53 @@ function overviewRivers() {
}
function openRiverEditor() {
editRiver('river' + this.parentNode.dataset.id);
const id = 'river' + this.parentNode.dataset.id;
editRiver(id);
}
function triggerRiverRemove() {
const river = +this.parentNode.dataset.id;
alertMessage.innerHTML = `Are you sure you want to remove the river?
All tributaries will be auto-removed`;
const message = 'Are you sure you want to remove the river? <br>All tributaries will be auto-removed';
const onConfirm = () => {
Rivers.remove(river);
riversOverviewAddLines();
};
confirmationDialog({title: 'Remove river', message, confirm: 'Remove', onConfirm});
$('#alert').dialog({
resizable: false,
width: '22em',
title: 'Remove river',
buttons: {
Remove: function () {
Rivers.remove(river);
riversOverviewAddLines();
$(this).dialog('close');
},
Cancel: function () {
$(this).dialog('close');
}
}
});
}
function triggerAllRiversRemove() {
const message = 'Are you sure you want to remove all rivers? <br>This action cannot be reverted';
const onConfirm = () => {
pack.rivers = [];
rivers.selectAll('*').remove();
riversOverviewAddLines();
};
confirmationDialog({title: 'Remove all rivers', message, confirm: 'Remove', onConfirm});
alertMessage.innerHTML = `Are you sure you want to remove all rivers?`;
$('#alert').dialog({
resizable: false,
title: 'Remove all rivers',
buttons: {
Remove: function () {
$(this).dialog('close');
removeAllRivers();
},
Cancel: function () {
$(this).dialog('close');
}
}
});
}
function removeAllRivers() {
pack.rivers = [];
pack.cells.r = new Uint16Array(pack.cells.i.length);
rivers.selectAll('*').remove();
riversOverviewAddLines();
}
}

View file

@ -849,18 +849,21 @@ function editStates() {
}
function adjustProvinces(affectedProvinces) {
const cells = pack.cells,
provinces = pack.provinces,
states = pack.states;
const {cells, provinces, states} = pack;
const form = {Zone: 1, Area: 1, Territory: 2, Province: 1};
<<<<<<< HEAD
affectedProvinces.forEach((p) => {
// do nothing if neutral lands are captured
if (!p) return;
=======
affectedProvinces.forEach(p => {
if (!p) return; // do nothing if neutral lands are captured
const old = provinces[p].state;
>>>>>>> 597f9ae038fbcc149315df9b1618e64744fb929d
// remove province from state provinces list
const old = provinces[p].state;
if (states[old].provinces.includes(p)) states[old].provinces.splice(states[old].provinces.indexOf(p), 1);
if (states[old]?.provinces?.includes(p)) states[old].provinces.splice(states[old].provinces.indexOf(p), 1);
// find states owning at least 1 province cell
const provCells = cells.i.filter((i) => cells.province[i] === p);
@ -871,8 +874,13 @@ function editStates() {
if (owner) {
const name = provinces[p].name;
<<<<<<< HEAD
// if province is historical part of abouther state province, unite with old province
const part = states[owner].provinces.find((n) => name.includes(provinces[n].name));
=======
// if province is a historical part of another state's province, unite with old province
const part = states[owner].provinces.find(n => name.includes(provinces[n].name));
>>>>>>> 597f9ae038fbcc149315df9b1618e64744fb929d
if (part) {
provinces[p].removed = true;
provCells.filter((i) => cells.state[i] === owner).forEach((i) => (cells.province[i] = part));

File diff suppressed because one or more lines are too long

View file

@ -551,94 +551,120 @@ function toggleAddRiver() {
}
function addRiverOnClick() {
const cells = pack.cells;
const point = d3.mouse(this);
let i = findCell(point[0], point[1]);
if (cells.r[i] || cells.h[i] < 20 || cells.b[i]) return;
const {cells, rivers} = pack;
let i = findCell(...d3.mouse(this));
const dataRiver = []; // to store river points
let river = +getNextId('river').slice(5); // river id
cells.fl[i] = grid.cells.prec[cells.g[i]]; // initial flux
if (cells.r[i]) return tip('There is already a river here', false, 'error');
if (cells.h[i] < 20) return tip('Cannot create river in water cell', false, 'error');
if (cells.b[i]) return;
const h = Rivers.alterHeights();
Lakes.prepareLakeData(h);
Rivers.resolveDepressions(h);
const {alterHeights, resolveDepressions, addMeandering, getRiverPath, getBasin, getName, getType, getWidth, getOffset, getApproximateLength} = Rivers;
const riverCells = [];
let riverId = rivers.length ? last(rivers).i + 1 : 1;
let parent = riverId;
const initialFlux = grid.cells.prec[cells.g[i]];
cells.fl[i] = initialFlux;
const h = alterHeights();
resolveDepressions(h);
while (i) {
cells.r[i] = river;
const [x, y] = cells.p[i];
dataRiver.push({x, y, cell: i});
cells.r[i] = riverId;
riverCells.push(i);
const min = cells.c[i].sort((a, b) => h[a] - h[b])[0]; // downhill cell
if (h[i] <= h[min]) return tip(`Cell ${i} is depressed, river cannot flow further`, false, 'error');
const [tx, ty] = cells.p[min];
// pour to water body
if (h[min] < 20) {
// pour to water body
dataRiver.push({x: tx, y: ty, cell: i});
riverCells.push(min);
const feature = pack.features[cells.f[min]];
if (feature.type === 'lake') {
if (feature.outlet) parent = feature.outlet;
feature.inlets ? feature.inlets.push(riverId) : (feature.inlets = [riverId]);
}
break;
}
// pour outside of map from border cell
if (cells.b[min]) {
cells.fl[min] += cells.fl[i];
riverCells.push(-1);
break;
}
// continue propagation if min cell has no river
if (!cells.r[min]) {
// continue if next cell has not river
cells.fl[min] += cells.fl[i];
i = min;
continue;
}
// handle case when lowest cell already has a river
const r = cells.r[min];
const riverCells = cells.i.filter((i) => cells.r[i] === r);
const riverCellsUpper = riverCells.filter((i) => h[i] > h[min]);
const oldRiverId = cells.r[min];
const oldRiver = rivers.find((river) => river.i === oldRiverId);
const oldRiverCells = oldRiver?.cells || cells.i.filter((i) => cells.r[i] === oldRiverId);
const oldRiverCellsUpper = oldRiverCells.filter((i) => h[i] > h[min]);
// finish new river if old river is longer
if (dataRiver.length <= riverCellsUpper.length) {
// create new river as a tributary
if (riverCells.length <= oldRiverCellsUpper.length) {
cells.conf[min] += cells.fl[i];
dataRiver.push({x: tx, y: ty, cell: min});
dataRiver[0].parent = r; // new river is tributary
riverCells.push(min);
parent = oldRiverId;
break;
}
// extend old river
rivers.select('#river' + r).remove();
cells.i.filter((i) => cells.r[i] === river).forEach((i) => (cells.r[i] = r));
riverCells.forEach((i) => (cells.r[i] = 0));
river = r;
cells.fl[min] = cells.fl[i] + grid.cells.prec[cells.g[min]];
i = min;
// continue old river
document.getElementById('river' + oldRiverId)?.remove();
riverCells.forEach((i) => (cells.r[i] = oldRiverId));
oldRiverCells.forEach((cell) => {
if (h[cell] > h[min]) {
cells.r[cell] = 0;
cells.fl[cell] = grid.cells.prec[cells.g[cell]];
} else {
riverCells.push(cell);
cells.fl[cell] += cells.fl[i];
}
});
riverId = oldRiverId;
break;
}
const points = Rivers.addMeandering(dataRiver, 1, 0.5);
const widthFactor = rn(0.8 + Math.random() * 0.4, 1); // river width modifier [.8, 1.2]
const sourceWidth = 0.1;
const [path, length, offset] = Rivers.getPath(points, widthFactor, sourceWidth);
rivers
.append('path')
.attr('d', path)
.attr('id', 'river' + river);
const river = rivers.find((r) => r.i === riverId);
// add new river to data or change extended river attributes
const r = pack.rivers.find((r) => r.i === river);
const mouth = last(dataRiver).cell;
const discharge = cells.fl[mouth]; // in m3/s
const source = riverCells[0];
const mouth = riverCells[riverCells.length - 2];
const widthFactor = river?.widthFactor || (!parent || parent === riverId ? 1.2 : 1);
const meanderedPoints = addMeandering(riverCells);
if (r) {
r.source = dataRiver[0].cell;
r.length = length;
r.discharge = discharge;
const discharge = cells.fl[mouth]; // m3 in second
const length = getApproximateLength(meanderedPoints);
const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor));
if (river) {
river.source = source;
river.length = length;
river.discharge = discharge;
river.width = width;
river.cells = riverCells;
} else {
const parent = dataRiver[0].parent || 0;
const basin = Rivers.getBasin(river);
const source = dataRiver[0].cell;
const width = rn(offset ** 2, 2); // mounth width in km
const name = Rivers.getName(mouth);
const smallLength = pack.rivers.map((r) => r.length || 0).sort((a, b) => a - b)[Math.ceil(pack.rivers.length * 0.15)];
const type = length < smallLength ? rw({Creek: 9, River: 3, Brook: 3, Stream: 1}) : 'River';
const basin = getBasin(parent);
const name = getName(mouth);
const type = getType({i: riverId, length, parent});
pack.rivers.push({i: river, source, mouth, discharge, length, width, widthFactor, sourceWidth, parent, basin, name, type});
rivers.push({i: riverId, source, mouth, discharge, length, width, widthFactor, sourceWidth: 0, parent, cells: riverCells, basin, name, type});
}
// render river
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
const path = getRiverPath(meanderedPoints, widthFactor);
const id = 'river' + riverId;
const riversG = viewbox.select('#rivers');
riversG.append('path').attr('id', id).attr('d', path);
if (d3.event.shiftKey === false) {
Lakes.cleanupLakeData();
unpressClickToAddButton();

View file

@ -1,4 +1,5 @@
"use strict";
function editZones() {
closeDialogs();
if (!layerIsOn("toggleZones")) toggleZones();

View file

@ -11,11 +11,11 @@ function getBoundaryPoints(width, height, spacing) {
const numberY = Math.ceil(h / bSpacing) - 1;
let points = [];
for (let i = 0.5; i < numberX; i++) {
let x = Math.ceil(w * i / numberX + offset);
let x = Math.ceil((w * i) / numberX + offset);
points.push([x, offset], [x, h + offset]);
}
for (let i = 0.5; i < numberY; i++) {
let y = Math.ceil(h * i / numberY + offset);
let y = Math.ceil((h * i) / numberY + offset);
points.push([offset, y], [w + offset, y]);
}
return points;
@ -24,7 +24,7 @@ function getBoundaryPoints(width, height, spacing) {
// get points on a regular square grid and jitter them a bit
function getJitteredGrid(width, height, spacing) {
const radius = spacing / 2; // square radius
const jittering = radius * .9; // max deviation
const jittering = radius * 0.9; // max deviation
const jitter = () => Math.random() * 2 * jittering - jittering;
let points = [];
@ -40,7 +40,7 @@ function getJitteredGrid(width, height, spacing) {
// return cell index on a regular square grid
function findGridCell(x, y) {
return Math.floor(Math.min(y / grid.spacing, grid.cellsY -1)) * grid.cellsX + Math.floor(Math.min(x / grid.spacing, grid.cellsX-1));
return Math.floor(Math.min(y / grid.spacing, grid.cellsY - 1)) * grid.cellsX + Math.floor(Math.min(x / grid.spacing, grid.cellsX - 1));
}
// return array of cell indexes in radius on a regular square grid
@ -55,14 +55,12 @@ function findGridAll(x, y, radius) {
while (r > 1) {
let cycle = frontier.slice();
frontier = [];
cycle.forEach(function(s) {
c[s].forEach(function(e) {
cycle.forEach(function (s) {
c[s].forEach(function (e) {
if (found.indexOf(e) !== -1) return;
found.push(e);
frontier.push(e);
});
});
r--;
}
@ -100,7 +98,7 @@ function getGridPolygon(i) {
// mbostock's poissonDiscSampler
function* poissonDiscSampler(x0, y0, x1, y1, r, k = 3) {
if (!(x1 >= x0) || !(y1 >= y0) || !(r > 0)) throw new Error;
if (!(x1 >= x0) || !(y1 >= y0) || !(r > 0)) throw new Error();
const width = x1 - x0;
const height = y1 - y0;
@ -113,8 +111,8 @@ function* poissonDiscSampler(x0, y0, x1, y1, r, k = 3) {
const queue = [];
function far(x, y) {
const i = x / cellSize | 0;
const j = y / cellSize | 0;
const i = (x / cellSize) | 0;
const j = (y / cellSize) | 0;
const i0 = Math.max(i - 2, 0);
const j0 = Math.max(j - 2, 0);
const i1 = Math.min(i + 3, gridWidth);
@ -134,14 +132,14 @@ function* poissonDiscSampler(x0, y0, x1, y1, r, k = 3) {
}
function sample(x, y) {
queue.push(grid[gridWidth * (y / cellSize | 0) + (x / cellSize | 0)] = [x, y]);
queue.push((grid[gridWidth * ((y / cellSize) | 0) + ((x / cellSize) | 0)] = [x, y]));
return [x + x0, y + y0];
}
yield sample(width / 2, height / 2);
pick: while (queue.length) {
const i = Math.random() * queue.length | 0;
const i = (Math.random() * queue.length) | 0;
const parent = queue[i];
for (let j = 0; j < k; ++j) {
@ -171,20 +169,19 @@ function isWater(i) {
}
// convert RGB color string to HEX without #
function toHEX(rgb){
if (rgb.charAt(0) === "#") {return rgb;}
function toHEX(rgb) {
if (rgb.charAt(0) === "#") {
return rgb;
}
rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
return (rgb && rgb.length === 4) ? "#" +
("0" + parseInt(rgb[1],10).toString(16)).slice(-2) +
("0" + parseInt(rgb[2],10).toString(16)).slice(-2) +
("0" + parseInt(rgb[3],10).toString(16)).slice(-2) : '';
return rgb && rgb.length === 4 ? "#" + ("0" + parseInt(rgb[1], 10).toString(16)).slice(-2) + ("0" + parseInt(rgb[2], 10).toString(16)).slice(-2) + ("0" + parseInt(rgb[3], 10).toString(16)).slice(-2) : "";
}
// return array of standard shuffled colors
function getColors(number) {
const c12 = ["#dababf","#fb8072","#80b1d3","#fdb462","#b3de69","#fccde5","#c6b9c1","#bc80bd","#ccebc5","#ffed6f","#8dd3c7","#eb8de7"];
const c12 = ["#dababf", "#fb8072", "#80b1d3", "#fdb462", "#b3de69", "#fccde5", "#c6b9c1", "#bc80bd", "#ccebc5", "#ffed6f", "#8dd3c7", "#eb8de7"];
const cRB = d3.scaleSequential(d3.interpolateRainbow);
const colors = d3.shuffle(d3.range(number).map(i => i < 12 ? c12[i] : d3.color(cRB((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;
}
@ -193,30 +190,42 @@ function getRandomColor() {
}
// mix a color with a random color
function getMixedColor(color, mix = .2, bright = .3) {
function getMixedColor(color, mix = 0.2, bright = 0.3) {
const c = color && color[0] === "#" ? color : getRandomColor(); // if provided color is not hex (e.g. harching), generate random one
return d3.color(d3.interpolate(c, getRandomColor())(mix)).brighter(bright).hex();
}
// conver temperature from °C to other scales
function convertTemperature(c) {
switch(temperatureScale.value) {
case "°C": return c + "°C";
case "°F": return rn(c * 9 / 5 + 32) + "°F";
case "K": return rn(c + 273.15) + "K";
case "°R": return rn((c + 273.15) * 9 / 5) + "°R";
case "°De": return rn((100 - c) * 3 / 2) + "°De";
case "°N": return rn(c * 33 / 100) + "°N";
case "°Ré": return rn(c * 4 / 5) + "°Ré";
case "°Rø": return rn(c * 21 / 40 + 7.5) + "°Rø";
default: return c + "°C";
switch (temperatureScale.value) {
case "°C":
return c + "°C";
case "°F":
return rn((c * 9) / 5 + 32) + "°F";
case "K":
return rn(c + 273.15) + "K";
case "°R":
return rn(((c + 273.15) * 9) / 5) + "°R";
case "°De":
return rn(((100 - c) * 3) / 2) + "°De";
case "°N":
return rn((c * 33) / 100) + "°N";
case "°Ré":
return rn((c * 4) / 5) + "°Ré";
case "°Rø":
return rn((c * 21) / 40 + 7.5) + "°Rø";
default:
return c + "°C";
}
}
// random number in a range
function rand(min, max) {
if (min === undefined && max === undefined) return Math.random();
if (max === undefined) {max = min; min = 0;}
if (max === undefined) {
max = min;
min = 0;
}
return Math.floor(Math.random() * (max - min + 1)) + min;
}
@ -227,6 +236,10 @@ function P(probability) {
return Math.random() < probability;
}
function each(n) {
return i => i % n === 0;
}
// random number (normal or gaussian distribution)
function gauss(expected = 100, deviation = 30, min = 0, max = 300, round = 0) {
return rn(Math.max(Math.min(d3.randomNormal(expected, deviation)(), max), min), round);
@ -245,7 +258,9 @@ function rn(v, d = 0) {
// round string to d decimals
function round(s, d = 1) {
return s.replace(/[\d\.-][\d\.e-]*/g, function(n) {return rn(n, d);})
return s.replace(/[\d\.-][\d\.e-]*/g, function (n) {
return rn(n, d);
});
}
// corvent number to short string with SI postfix
@ -279,52 +294,50 @@ function capitalize(string) {
// transform string to array [translateX,translateY,rotateDeg,rotateX,rotateY,scale]
function parseTransform(string) {
if (!string) {return [0,0,0,0,0,1];}
const a = string.replace(/[a-z()]/g, "").replace(/[ ]/g, ",").split(",");
if (!string) {
return [0, 0, 0, 0, 0, 1];
}
const a = string
.replace(/[a-z()]/g, "")
.replace(/[ ]/g, ",")
.split(",");
return [a[0] || 0, a[1] || 0, a[2] || 0, a[3] || 0, a[4] || 0, a[5] || 1];
}
// findAll d3.quandtree search from https://bl.ocks.org/lwthatcher/b41479725e0ff2277c7ac90df2de2b5e
void function addFindAll() {
const Quad = function(node, x0, y0, x1, y1) {
void (function addFindAll() {
const Quad = function (node, x0, y0, x1, y1) {
this.node = node;
this.x0 = x0;
this.y0 = y0;
this.x1 = x1;
this.y1 = y1;
}
};
const tree_filter = function(x, y, radius) {
const tree_filter = function (x, y, radius) {
var t = {x, y, x0: this._x0, y0: this._y0, x3: this._x1, y3: this._y1, quads: [], node: this._root};
if (t.node) {t.quads.push(new Quad(t.node, t.x0, t.y0, t.x3, t.y3))};
if (t.node) {
t.quads.push(new Quad(t.node, t.x0, t.y0, t.x3, t.y3));
}
radiusSearchInit(t, radius);
var i = 0;
while (t.q = t.quads.pop()) {
while ((t.q = t.quads.pop())) {
i++;
// Stop searching if this quadrant cant contain a closer node.
if (!(t.node = t.q.node)
|| (t.x1 = t.q.x0) > t.x3
|| (t.y1 = t.q.y0) > t.y3
|| (t.x2 = t.q.x1) < t.x0
|| (t.y2 = t.q.y1) < t.y0) continue;
if (!(t.node = t.q.node) || (t.x1 = t.q.x0) > t.x3 || (t.y1 = t.q.y0) > t.y3 || (t.x2 = t.q.x1) < t.x0 || (t.y2 = t.q.y1) < t.y0) continue;
// Bisect the current quadrant.
if (t.node.length) {
t.node.explored = true;
var xm = (t.x1 + t.x2) / 2,
ym = (t.y1 + t.y2) / 2;
ym = (t.y1 + t.y2) / 2;
t.quads.push(
new Quad(t.node[3], xm, ym, t.x2, t.y2),
new Quad(t.node[2], t.x1, ym, xm, t.y2),
new Quad(t.node[1], xm, t.y1, t.x2, ym),
new Quad(t.node[0], t.x1, t.y1, xm, ym)
);
t.quads.push(new Quad(t.node[3], xm, ym, t.x2, t.y2), new Quad(t.node[2], t.x1, ym, xm, t.y2), new Quad(t.node[1], xm, t.y1, t.x2, ym), new Quad(t.node[0], t.x1, t.y1, xm, ym));
// Visit the closest quadrant first.
if (t.i = (y >= ym) << 1 | (x >= xm)) {
if ((t.i = ((y >= ym) << 1) | (x >= xm))) {
t.q = t.quads[t.quads.length - 1];
t.quads[t.quads.length - 1] = t.quads[t.quads.length - 1 - t.i];
t.quads[t.quads.length - 1 - t.i] = t.q;
@ -334,29 +347,32 @@ void function addFindAll() {
// Visit this point. (Visiting coincident points isnt necessary!)
else {
var dx = x - +this._x.call(null, t.node.data),
dy = y - +this._y.call(null, t.node.data),
d2 = dx * dx + dy * dy;
dy = y - +this._y.call(null, t.node.data),
d2 = dx * dx + dy * dy;
radiusSearchVisit(t, d2);
}
}
return t.result;
}
};
d3.quadtree.prototype.findAll = tree_filter;
var radiusSearchInit = function(t, radius) {
var radiusSearchInit = function (t, radius) {
t.result = [];
t.x0 = t.x - radius, t.y0 = t.y - radius;
t.x3 = t.x + radius, t.y3 = t.y + radius;
(t.x0 = t.x - radius), (t.y0 = t.y - radius);
(t.x3 = t.x + radius), (t.y3 = t.y + radius);
t.radius = radius * radius;
}
};
var radiusSearchVisit = function(t, d2) {
var radiusSearchVisit = function (t, d2) {
t.node.data.scanned = true;
if (d2 < t.radius) {
do {t.result.push(t.node.data); t.node.data.selected = true;} while (t.node = t.node.next);
do {
t.result.push(t.node.data);
t.node.data.selected = true;
} while ((t.node = t.node.next));
}
}
}()
};
})();
// get segment of any point on polyline
function getSegmentId(points, point, step = 10) {
@ -366,23 +382,23 @@ function getSegmentId(points, point, step = 10) {
let minSegment = 1;
let minDist = Infinity;
for (let i=0; i < points.length-1; i++) {
for (let i = 0; i < points.length - 1; i++) {
const p1 = points[i];
const p2 = points[i+1];
const p2 = points[i + 1];
const length = Math.sqrt(d2(p1, p2));
const segments = Math.ceil(length / step);
const dx = (p2[0] - p1[0]) / segments;
const dy = (p2[1] - p1[1]) / segments;
for (let s=0; s < segments; s++) {
for (let s = 0; s < segments; s++) {
const x = p1[0] + s * dx;
const y = p1[1] + s * dy;
const dist2 = d2(point, [x, y]);
if (dist2 >= minDist) continue;
minDist = dist2;
minSegment = i+1;
minSegment = i + 1;
}
}
@ -418,7 +434,9 @@ function vowel(c) {
// remove vowels from the end of the string
function trimVowels(string) {
while (string.length > 3 && vowel(last(string))) {string = string.slice(0,-1);}
while (string.length > 3 && vowel(last(string))) {
string = string.slice(0, -1);
}
return string;
}
@ -427,7 +445,7 @@ function getAdjective(string) {
// special cases for some suffixes
if (string.length > 8 && string.slice(-6) === "orszag") return string.slice(0, -6);
if (string.length > 6 && string.slice(-4) === "stan") return string.slice(0, -4);
if (P(.5) && string.slice(-4) === "land") return string + "ic";
if (P(0.5) && string.slice(-4) === "land") return string + "ic";
if (string.slice(-4) === " Guo") string = string.slice(0, -4);
// don't change is name ends on suffix
@ -436,16 +454,16 @@ function getAdjective(string) {
if (string.slice(-1) === "i") return string;
const end = string.slice(-1); // last letter of string
if (end === "a") return string += "n";
if (end === "o") return string = trimVowels(string) + "an";
if (vowel(end) || end === "c") return string += "an"; // ceiuy
if (end === "m" || end === "n") return string += "ese";
if (end === "q") return string += "i";
if (end === "a") return (string += "n");
if (end === "o") return (string = trimVowels(string) + "an");
if (vowel(end) || end === "c") return (string += "an"); // ceiuy
if (end === "m" || end === "n") return (string += "ese");
if (end === "q") return (string += "i");
return trimVowels(string) + "ian";
}
// get ordinal out of integer: 1 => 1st
const nth = n => n+(["st","nd","rd"][((n+90)%100-10)%10-1]||"th");
const nth = n => n + (["st", "nd", "rd"][((((n + 90) % 100) - 10) % 10) - 1] || "th");
// get two-letters code (abbreviation) from string
function abbreviate(name, restricted = []) {
@ -453,8 +471,8 @@ function abbreviate(name, restricted = []) {
const words = parsed.split(" ");
const letters = words.join("");
let code = words.length === 2 ? words[0][0]+words[1][0] : letters.slice(0,2);
for (let i = 1; i < letters.length-1 && restricted.includes(code); i++) {
let code = words.length === 2 ? words[0][0] + words[1][0] : letters.slice(0, 2);
for (let i = 1; i < letters.length - 1 && restricted.includes(code); i++) {
code = letters[0] + letters[i].toUpperCase();
}
return code;
@ -463,7 +481,7 @@ function abbreviate(name, restricted = []) {
// conjunct array: [A,B,C] => "A, B and C"
function list(array) {
if (!Intl.ListFormat) return array.join(", ");
const conjunction = new Intl.ListFormat(window.lang || "en", {style:"long", type:"conjunction"});
const conjunction = new Intl.ListFormat(window.lang || "en", {style: "long", type: "conjunction"});
return conjunction.format(array);
}
@ -472,7 +490,10 @@ function splitInTwo(str) {
const half = str.length / 2;
const ar = str.split(" ");
if (ar.length < 2) return ar; // only one word
let first = "", last = "", middle = "", rest = "";
let first = "",
last = "",
middle = "",
rest = "";
ar.forEach((w, d) => {
if (d + 1 !== ar.length) w += " ";
@ -501,10 +522,10 @@ function ra(array) {
function rw(object) {
const array = [];
for (const key in object) {
for (let i=0; i < object[key]; i++) {
for (let i = 0; i < object[key]; i++) {
array.push(key);
}
};
}
return array[Math.floor(Math.random() * array.length)];
}
@ -515,33 +536,69 @@ function lim(v) {
// get number from string in format "1-3" or "2" or "0.5"
function getNumberInRange(r) {
if (typeof r !== "string") {ERROR && console.error("The value should be a string", r); return 0;}
if (typeof r !== "string") {
ERROR && console.error("The value should be a string", r);
return 0;
}
if (!isNaN(+r)) return ~~r + +P(r - ~~r);
const sign = r[0] === "-" ? -1 : 1;
if (isNaN(+r[0])) r = r.slice(1);
const range = r.includes("-") ? r.split("-") : null;
if (!range) {ERROR && console.error("Cannot parse the number. Check the format", r); return 0;}
if (!range) {
ERROR && console.error("Cannot parse the number. Check the format", r);
return 0;
}
const count = rand(range[0] * sign, +range[1]);
if (isNaN(count) || count < 0) {ERROR && console.error("Cannot parse number. Check the format", r); return 0;}
if (isNaN(count) || count < 0) {
ERROR && console.error("Cannot parse number. Check the format", r);
return 0;
}
return count;
}
// 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];
}
// 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);
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 generation
function drawPolygons(data) {
const max = d3.max(data), min = d3.min(data), scheme = getColorScheme();
const max = d3.max(data),
min = d3.min(data),
scheme = getColorScheme();
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) => getPackPolygon(i))
.attr("fill", d => scheme(d)).attr("stroke", d => scheme(d));
debug
.selectAll("polygon")
.data(data)
.enter()
.append("polygon")
.attr("points", (d, i) => getPackPolygon(i))
.attr("fill", d => scheme(d))
.attr("stroke", d => scheme(d));
}
// polyfill for composedPath
@ -552,56 +609,86 @@ function getComposedPath(node) {
else if (node.defaultView) parent = node.defaultView;
if (parent !== undefined) return [node].concat(getComposedPath(parent));
return [node];
};
}
// polyfill for replaceAll
if (!String.prototype.replaceAll) {
String.prototype.replaceAll = function(str, newStr){
if (Object.prototype.toString.call(str).toLowerCase() === '[object regexp]') return this.replace(str, newStr);
return this.replace(new RegExp(str, 'g'), newStr);
String.prototype.replaceAll = function (str, newStr) {
if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") return this.replace(str, newStr);
return this.replace(new RegExp(str, "g"), newStr);
};
}
// get next unused id
function getNextId(core, i = 1) {
while (document.getElementById(core+i)) i++;
while (document.getElementById(core + i)) i++;
return core + i;
}
function debounce(f, ms) {
function debounce(func, ms) {
let isCooldown = false;
return function() {
return function () {
if (isCooldown) return;
f.apply(this, arguments);
func.apply(this, arguments);
isCooldown = true;
setTimeout(() => isCooldown = false, ms);
setTimeout(() => (isCooldown = false), ms);
};
}
function throttle(func, ms) {
let isThrottled = false;
let savedArgs;
let savedThis;
function wrapper() {
if (isThrottled) {
savedArgs = arguments;
savedThis = this;
return;
}
func.apply(this, arguments);
isThrottled = true;
setTimeout(function () {
isThrottled = false;
if (savedArgs) {
wrapper.apply(savedThis, savedArgs);
savedArgs = savedThis = null;
}
}, ms);
}
return wrapper;
}
// parse error to get the readable string in Chrome and Firefox
function parseError(error) {
const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
const isFirefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
const errorString = isFirefox ? error.toString() + " " + error.stack : error.stack;
const regex =/(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
const errorNoURL = errorString.replace(regex, url => '<i>' + last(url.split("/")) + '</i>');
const errorParsed = errorNoURL.replace(/at /ig, "<br>&nbsp;&nbsp;at ");
const regex = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gi;
const errorNoURL = errorString.replace(regex, url => "<i>" + last(url.split("/")) + "</i>");
const errorParsed = errorNoURL.replace(/at /gi, "<br>&nbsp;&nbsp;at ");
return errorParsed;
}
// polyfills
if (Array.prototype.flat === undefined) {
Array.prototype.flat = function() {
return this.reduce((acc, val) => Array.isArray(val) ? acc.concat(val.flat()) : acc.concat(val), []);
}
Array.prototype.flat = function () {
return this.reduce((acc, val) => (Array.isArray(val) ? acc.concat(val.flat()) : acc.concat(val)), []);
};
}
// check if string is a valid for JSON parse
JSON.isValid = str => {
try {JSON.parse(str);}
catch(e) {return false;}
try {
JSON.parse(str);
} catch (e) {
return false;
}
return true;
}
};
function getBase64(url, callback) {
const xhr = new XMLHttpRequest();
@ -640,16 +727,16 @@ function wiki(page) {
// wrap URL into html a element
function link(URL, description) {
return `<a href="${URL}" rel="noopener" target="_blank">${description}</a>`
return `<a href="${URL}" rel="noopener" target="_blank">${description}</a>`;
}
function isCtrlClick(event) {
// meta key is cmd key on MacOs
return event.ctrlKey || event.metaKey;
return event.ctrlKey || event.metaKey;
}
function generateDate(from = 100, to = 1000) {
return new Date(rand(from, to),rand(12),rand(31)).toLocaleDateString("en", {year:'numeric', month:'long', day:'numeric'});
return new Date(rand(from, to), rand(12), rand(31)).toLocaleDateString("en", {year: "numeric", month: "long", day: "numeric"});
}
function getQGIScoordinates(x, y) {
@ -659,15 +746,18 @@ function getQGIScoordinates(x, y) {
}
// prompt replacer (prompt does not work in Electron)
void function() {
void (function () {
const prompt = document.getElementById("prompt");
const form = prompt.querySelector("#promptForm");
window.prompt = function(promptText = "Please provide an input", options = {default:1, step:.01, min:0, max:100}, callback) {
if (options.default === undefined) {ERROR && console.error("Prompt: options object does not have default value defined"); return;}
window.prompt = function (promptText = "Please provide an input", options = {default: 1, step: 0.01, min: 0, max: 100}, callback) {
if (options.default === undefined) {
ERROR && console.error("Prompt: options object does not have default value defined");
return;
}
const input = prompt.querySelector("#promptInput");
prompt.querySelector("#promptText").innerHTML = promptText;
const type = typeof(options.default) === "number" ? "number" : "text";
const type = typeof options.default === "number" ? "number" : "text";
input.type = type;
if (options.step !== undefined) input.step = options.step;
if (options.min !== undefined) input.min = options.min;
@ -676,17 +766,56 @@ void function() {
input.value = options.default;
prompt.style.display = "block";
form.addEventListener("submit", event => {
prompt.style.display = "none";
const v = type === "number" ? +input.value : input.value;
event.preventDefault();
if (callback) callback(v);
}, {once: true});
}
form.addEventListener(
"submit",
event => {
prompt.style.display = "none";
const v = type === "number" ? +input.value : input.value;
event.preventDefault();
if (callback) callback(v);
},
{once: true}
);
};
const cancel = prompt.querySelector("#promptCancel");
cancel.addEventListener("click", () => prompt.style.display = "none");
}()
cancel.addEventListener("click", () => (prompt.style.display = "none"));
})();
// indexedDB; ldb object
!function(){function e(t,o){return n?void(n.transaction("s").objectStore("s").get(t).onsuccess=function(e){var t=e.target.result&&e.target.result.v||null;o(t)}):void setTimeout(function(){e(t,o)},100)}var t=window.indexedDB||window.mozIndexedDB||window.webkitIndexedDB||window.msIndexedDB;if(!t)return void ERROR && console.error("indexedDB not supported");var n,o={k:"",v:""},r=t.open("d2",1);r.onsuccess=function(e){n=this.result},r.onerror=function(e){ERROR && console.error("indexedDB request error"),INFO && console.log(e)},r.onupgradeneeded=function(e){n=null;var t=e.target.result.createObjectStore("s",{keyPath:"k"});t.transaction.oncomplete=function(e){n=e.target.db}},window.ldb={get:e,set:function(e,t){o.k=e,o.v=t,n.transaction("s","readwrite").objectStore("s").put(o)}}}();
!(function () {
function e(t, o) {
return n
? void (n.transaction("s").objectStore("s").get(t).onsuccess = function (e) {
var t = (e.target.result && e.target.result.v) || null;
o(t);
})
: void setTimeout(function () {
e(t, o);
}, 100);
}
var t = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
if (!t) return void ERROR && console.error("indexedDB not supported");
var n,
o = {k: "", v: ""},
r = t.open("d2", 1);
(r.onsuccess = function (e) {
n = this.result;
}),
(r.onerror = function (e) {
ERROR && console.error("indexedDB request error"), INFO && console.log(e);
}),
(r.onupgradeneeded = function (e) {
n = null;
var t = e.target.result.createObjectStore("s", {keyPath: "k"});
t.transaction.oncomplete = function (e) {
n = e.target.db;
};
}),
(window.ldb = {
get: e,
set: function (e, t) {
(o.k = e), (o.v = t), n.transaction("s", "readwrite").objectStore("s").put(o);
}
});
})();