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 .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 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View file

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

View file

@ -45,36 +45,36 @@
xmlns:svg="http://www.w3.org/2000/svg"> xmlns:svg="http://www.w3.org/2000/svg">
<defs> <defs>
<g id="filters"> <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"/> <feGaussianBlur in="SourceGraphic" stdDeviation="0.2"/>
</filter> </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"/> <feGaussianBlur in="SourceGraphic" stdDeviation="1"/>
</filter> </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"/> <feGaussianBlur in="SourceGraphic" stdDeviation="3"/>
</filter> </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"/> <feGaussianBlur in="SourceGraphic" stdDeviation="5"/>
</filter> </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"/> <feGaussianBlur in="SourceGraphic" stdDeviation="7"/>
</filter> </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"/> <feGaussianBlur in="SourceGraphic" stdDeviation="10"/>
</filter> </filter>
<filter id="splotch"> <filter id="splotch" name="Splotch">
<feTurbulence type="fractalNoise" baseFrequency=".01" numOctaves="4"/> <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"/> <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"/> <feComposite in="SourceGraphic" in2="texture" operator="in"/>
</filter> </filter>
<filter id="bluredSplotch"> <filter id="bluredSplotch" name="Blurred Splotch">
<feTurbulence type="fractalNoise" baseFrequency=".01" numOctaves="4"/> <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"/> <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"/> <feComposite in="SourceGraphic" in2="texture" operator="in"/>
<feGaussianBlur stdDeviation="4"/> <feGaussianBlur stdDeviation="4"/>
</filter> </filter>
<filter id="dropShadow"> <filter id="dropShadow" name="Shadow 2">
<feGaussianBlur in="SourceAlpha" stdDeviation="2"/> <feGaussianBlur in="SourceAlpha" stdDeviation="2"/>
<feOffset dx="1" dy="2"/> <feOffset dx="1" dy="2"/>
<feMerge> <feMerge>
@ -82,7 +82,7 @@
<feMergeNode in="SourceGraphic"/> <feMergeNode in="SourceGraphic"/>
</feMerge> </feMerge>
</filter> </filter>
<filter id="dropShadow01"> <filter id="dropShadow01" name="Shadow 0.1">
<feGaussianBlur in="SourceAlpha" stdDeviation=".1"/> <feGaussianBlur in="SourceAlpha" stdDeviation=".1"/>
<feOffset dx=".2" dy=".3"/> <feOffset dx=".2" dy=".3"/>
<feMerge> <feMerge>
@ -90,7 +90,7 @@
<feMergeNode in="SourceGraphic"/> <feMergeNode in="SourceGraphic"/>
</feMerge> </feMerge>
</filter> </filter>
<filter id="dropShadow05"> <filter id="dropShadow05" name="Shadow 0.5">
<feGaussianBlur in="SourceAlpha" stdDeviation=".5"/> <feGaussianBlur in="SourceAlpha" stdDeviation=".5"/>
<feOffset dx=".5" dy=".7"/> <feOffset dx=".5" dy=".7"/>
<feMerge> <feMerge>
@ -98,23 +98,23 @@
<feMergeNode in="SourceGraphic"/> <feMergeNode in="SourceGraphic"/>
</feMerge> </feMerge>
</filter> </filter>
<filter id="outline"> <filter id="outline" name="Outline">
<feGaussianBlur in="SourceAlpha" stdDeviation="1"/> <feGaussianBlur in="SourceAlpha" stdDeviation="1"/>
<feMerge> <feMerge>
<feMergeNode/> <feMergeNode/>
<feMergeNode in="SourceGraphic"/> <feMergeNode in="SourceGraphic"/>
</feMerge> </feMerge>
</filter> </filter>
<filter id="pencil"> <filter id="pencil" name="Pencil">
<feTurbulence baseFrequency="0.03" numOctaves="6" type="fractalNoise"/> <feTurbulence baseFrequency="0.03" numOctaves="6" type="fractalNoise"/>
<feDisplacementMap scale="3" in="SourceGraphic" xChannelSelector="R" yChannelSelector="G"/> <feDisplacementMap scale="3" in="SourceGraphic" xChannelSelector="R" yChannelSelector="G"/>
</filter> </filter>
<filter id="turbulence"> <filter id="turbulence" name="Turbulence">
<feTurbulence baseFrequency="0.1" numOctaves="3" type="fractalNoise"/> <feTurbulence baseFrequency="0.1" numOctaves="3" type="fractalNoise"/>
<feDisplacementMap scale="10" in="SourceGraphic" xChannelSelector="R" yChannelSelector="G"/> <feDisplacementMap scale="10" in="SourceGraphic" xChannelSelector="R" yChannelSelector="G"/>
</filter> </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"/> <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"/> <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"> <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"/> <feComposite in="composite" in2="SourceGraphic" operator="in" x="0%" y="0%" width="100%" height="100%" result="composite1"/>
</filter> </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"/> <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"/> <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"> <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"/> <feComposite in="composite" in2="SourceGraphic" operator="in" x="0%" y="0%" width="100%" height="100%" result="composite1"/>
</filter> </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"/> <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>
<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"/> <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>
<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> <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>
<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> <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> </filter>
</g> </g>
@ -235,7 +235,7 @@
<div id="loading"> <div id="loading">
<div id="titleName"><t data-t="titleName">Azgaar's</t></div> <div id="titleName"><t data-t="titleName">Azgaar's</t></div>
<div id="title"><t data-t="title">Fantasy Map Generator</t></div> <div id="title"><t data-t="title">Fantasy Map Generator</t></div>
<div id="version"><t data-t="version">v. </t>1.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> <p id="loading-text"><t data-t="loading">LOADING</t><span>.</span><span>.</span><span>.</span></p>
</div> </div>
@ -319,13 +319,15 @@
</div> </div>
<div id="styleContent" class="tabcontent"> <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%"> <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="styleDefault" data-system=1 selected>Default</option>
<option value="styleAncient" data-system=1>Ancient</option> <option value="styleAncient" data-system=1>Ancient</option>
<option value="styleGloom" data-system=1>Gloom</option> <option value="styleGloom" data-system=1>Gloom</option>
<option value="styleClean" data-system=1>Clean</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> </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="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> <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="compass">Wind Rose</option>
<option value="zones">Zones</option> <option value="zones">Zones</option>
</select> </select>
<!-- <button id="restoreStyle" data-tip="Click to restore default style for all elements" class="icon-ccw styleButton" onclick="askToRestoreDefaultStyle()"></button> -->
<table id="styleElements"> <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"> <tbody id="styleGroup">
<tr data-tip="Select element group"> <tr data-tip="Select element group">
<td><b>Group</b></td> <td><b>Group</b></td>
<td> <td>
<select id="styleGroupSelect"><option value="regions">regions</option></select> <select id="styleGroupSelect"></select>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -391,24 +392,6 @@
</tr> </tr>
</tbody> </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"> <tbody id="styleLegend">
<tr data-tip="Set maximum number of items in one column"> <tr data-tip="Set maximum number of items in one column">
<td>Column items</td> <td>Column items</td>
@ -600,7 +583,7 @@
</tbody> </tbody>
<tbody id="styleRelief"> <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>Style</td>
<td> <td>
<select id="styleReliefSet"> <select id="styleReliefSet">
@ -611,15 +594,15 @@
</td> </td>
</tr> </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>Size</td>
<td> <td>
<input id="styleReliefSizeInput" data-stored="reliefSize" type="range" min=.2 max=3 step=.01 value=1> <input id="styleReliefSizeInput" data-stored="reliefSize" type="range" min=.2 max=4 step=.01>
<output id="styleReliefSizeOutput">1</output> <output id="styleReliefSizeOutput"></output>
</td> </td>
</tr> </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>Density</td>
<td> <td>
<input id="styleReliefDensityInput" data-stored="reliefDensity" type="range" min=.3 max=.8 step=.01 value=.4> <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"> <tr data-tip="Set font size">
<td>Font size</td> <td>Font size</td>
<td> <td>
<button id="styleFontPlus" data-tip="Multiply font size by 1.1" class="whiteButton">+</button> <button id="styleFontPlus" data-tip="Increase font" class="whiteButton">+</button>
<button id="styleFontMinus" data-tip="Multiply font size by 0.9" 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> <input id="styleFontSize" type="number" min=.5 max=100 step=.1 value=14>
</td> </td>
</tr> </tr>
@ -761,6 +744,45 @@
</tr> </tr>
</tbody> </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"> <tbody id="styleHeightmap">
<tr data-tip="Select color scheme for the element"> <tr data-tip="Select color scheme for the element">
<td>Color scheme</td> <td>Color scheme</td>
@ -830,21 +852,21 @@
<tr data-tip="Set state emblems size multiplier"> <tr data-tip="Set state emblems size multiplier">
<td>State Size</td> <td>State Size</td>
<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> </td>
</tr> </tr>
<tr data-tip="Set province emblems size multiplier"> <tr data-tip="Set province emblems size multiplier">
<td>Province Size</td> <td>Province Size</td>
<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> </td>
</tr> </tr>
<tr data-tip="Set burg emblems size multiplier"> <tr data-tip="Set burg emblems size multiplier">
<td>Burg Size</td> <td>Burg Size</td>
<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> </td>
</tr> </tr>
@ -859,29 +881,7 @@
<tbody id="styleFilter" style="display: block"> <tbody id="styleFilter" style="display: block">
<tr data-tip="Select filter for element. Please note filters may cause performance issues!"> <tr data-tip="Select filter for element. Please note filters may cause performance issues!">
<td>Filter</td> <td>Filter</td>
<td> <td><select id="styleFilterInput"/></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>
</tr> </tr>
</tbody> </tbody>
@ -923,6 +923,13 @@
<label for="hideLabels" class="checkbox-label">Toggle visibility automatically</label> <label for="hideLabels" class="checkbox-label">Toggle visibility automatically</label>
</td> </td>
</tr> </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> </tbody>
</table> </table>
@ -1013,17 +1020,18 @@
<td>Map template</td> <td>Map template</td>
<td> <td>
<select id="templateInput" data-stored="template"> <select id="templateInput" data-stored="template">
<option value="Volcano">Volcano</option> <option value="volcano">Volcano</option>
<option value="High Island">High Island</option> <option value="highIsland">High Island</option>
<option value="Low Island">Low Island</option> <option value="lowIsland">Low Island</option>
<option value="Continents">Two Continents</option> <option value="continents">Two Continents</option>
<option value="Archipelago">Archipelago</option> <option value="archipelago">Archipelago</option>
<option value="Atoll">Atoll</option> <option value="atoll">Atoll</option>
<option value="Mediterranean">Mediterranean</option> <option value="mediterranean">Mediterranean</option>
<option value="Peninsula">Peninsula</option> <option value="peninsula">Peninsula</option>
<option value="Pangea">Pangea</option> <option value="pangea">Pangea</option>
<option value="Isthmus">Isthmus</option> <option value="isthmus">Isthmus</option>
<option value="Shattered">Shattered</option> <option value="shattered">Shattered</option>
<option value="taklamakan">Taklamakan</option>
</select> </select>
</td> </td>
<td></td> <td></td>
@ -1291,6 +1299,18 @@
</td> </td>
</tr> </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"> <!-- <tr data-tip="Select language (not all languages are fully supported). Reload the page to apply">
<td></td> <td></td>
<td>Language</td> <td>Language</td>
@ -1357,7 +1377,7 @@
<div id="addFeature"> <div id="addFeature">
<p>Click to add:</p> <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="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="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> <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/> <input id="riverWidth" disabled/>
</div> </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> <div class="label">Source width:</div>
<input id="riverSourceWidth" type="number" min=0 max=3 step=.1 /> <input id="riverSourceWidth" type="number" min=0 max=3 step=.1 />
</div> </div>
<div data-tip="River width multiplier"> <div data-tip="River width multiplier. Default value is 1">
<div class="label">Width modifier:</div> <div class="label">Width modifier:</div>
<input id="riverWidthFactor" type="number" min=.1 max=4 step=.1 /> <input id="riverWidthFactor" type="number" min=.1 max=4 step=.1 />
</div> </div>
</div> </div>
<div id="riverBottom"> <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="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="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> <button id="riverLegend" data-tip="Edit free text notes (legend) for the river" class="icon-edit"></button>
@ -1667,6 +1687,14 @@
</div> </div>
</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="lakeEditor" class="dialog" style="display: none">
<div id="lakeBody" style="padding-bottom: .3em"> <div id="lakeBody" style="padding-bottom: .3em">
<div> <div>
@ -1927,7 +1955,7 @@
<button id="reliefCopy" data-tip="Copy selected relief icon" class="icon-clone"></button> <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="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="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>
</div> </div>
@ -2327,18 +2355,19 @@
<div id="templateEditor" class="dialog stable" style="display: none"> <div id="templateEditor" class="dialog stable" style="display: none">
<div id="templateTop"> <div id="templateTop">
<i>Select template: </i><select id="templateSelect" style="width:16em" data-prev="templateCustom" data-tip="Select base template"> <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="custom" selected>Custom</option>
<option value="templateVolcano">Volcano</option> <option value="volcano">Volcano</option>
<option value="templateHighIsland">High Island</option> <option value="highIsland">High Island</option>
<option value="templateLowIsland">Low Island</option> <option value="lowIsland">Low Island</option>
<option value="templateContinents">Two Continents</option> <option value="continents">Two Continents</option>
<option value="templateArchipelago">Archipelago</option> <option value="archipelago">Archipelago</option>
<option value="templateAtoll">Atoll</option> <option value="atoll">Atoll</option>
<option value="templateMediterranean">Mediterranean</option> <option value="mediterranean">Mediterranean</option>
<option value="templatePeninsula">Peninsula</option> <option value="peninsula">Peninsula</option>
<option value="templatePangea">Pangea</option> <option value="pangea">Pangea</option>
<option value="templateIsthmus">Isthmus</option> <option value="isthmus">Isthmus</option>
<option value="templateShattered">Shattered</option> <option value="shattered">Shattered</option>
<option value="taklamakan">Taklamakan</option>
</select> </select>
</div> </div>
<div id="templateTools"> <div id="templateTools">
@ -2553,6 +2582,7 @@
<option value="Diarchy">Diarchy</option> <option value="Diarchy">Diarchy</option>
<option value="Federation">Federation</option> <option value="Federation">Federation</option>
<option value="Free City">Free City</option> <option value="Free City">Free City</option>
<option value="Most Serene Republic">Most Serene Republic</option>
<option value="Oligarchy">Oligarchy</option> <option value="Oligarchy">Oligarchy</option>
<option value="Protectorate">Protectorate</option> <option value="Protectorate">Protectorate</option>
<option value="Republic">Republic</option> <option value="Republic">Republic</option>
@ -2575,10 +2605,17 @@
<option value="Tribes">United Tribes</option> <option value="Tribes">United Tribes</option>
</optgroup> </optgroup>
<optgroup label="Theocracy"> <optgroup label="Theocracy">
<option value="Bishopric">Bishopric</option>
<option value="Brotherhood">Brotherhood</option> <option value="Brotherhood">Brotherhood</option>
<option value="Caliphate">Caliphate</option> <option value="Caliphate">Caliphate</option>
<option value="Diocese">Diocese</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="Eparchy">Eparchy</option>
<option value="Holy State">Holy State</option>
<option value="Imamah">Imamah</option> <option value="Imamah">Imamah</option>
<option value="Theocracy">Theocracy</option> <option value="Theocracy">Theocracy</option>
</optgroup> </optgroup>
@ -3275,7 +3312,8 @@
<div id="riversBottom"> <div id="riversBottom">
<button id="riversOverviewRefresh" data-tip="Refresh the Editor" class="icon-cw"></button> <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="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="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> <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"> <input id="options3dSunZ" type="number" min=-1500 max=1500 step=100 style="width:4.7em">
</div> </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"> <div data-tip="Toggle sky mode" style="margin: .6em 0 .3em -.2em">
<input id="options3dMeshSkyMode" class="checkbox" type="checkbox"> <input id="options3dMeshSkyMode" class="checkbox" type="checkbox">
<label for="options3dMeshSkyMode" class="checkbox-label"><i>Show sky and extend water</i></label> <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> <path d="m20,58 h70 m-62,3 h50" stroke="#5c5c70" stroke-dasharray="7, 11" stroke-width="1"></path>
</symbol> </symbol>
<symbol id="relief-deciduous-1" viewBox="0 0 100 100"> <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="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="m50,52 q-12,-7 0,-16 q-3.5,10 0,15.5" fill="#999999"></path> <path d="M 50,51.5 C 44,49 40,43 50,36.5" fill="#999999"></path>
</symbol> </symbol>
<symbol id="relief-conifer-1" viewBox="0 0 100 100"> <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="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="m46,55 l4,-8 -4,0 5,-9 -2.5,9 l1.5,0 -2,8" fill="#999999"></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>
<symbol id="relief-acacia-1" viewBox="0 0 100 100"> <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> <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> <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>
<symbol id="relief-swamp-1" viewBox="0 0 100 100"> <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>
<symbol id="relief-dune-1" viewBox="0 0 100 100"> <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> <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="libs/delaunator.min.js"></script>
<script src="modules/utils.js"></script> <script src="modules/utils.js"></script>
<script src="modules/voronoi.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/heightmap-generator.js"></script>
<script src="modules/ocean-layers.js"></script> <script src="modules/ocean-layers.js"></script>
<script src="modules/river-generator.js"></script> <script src="modules/river-generator.js"></script>
@ -4448,6 +4492,7 @@
<script src="libs/lineclip.min.js"></script> <script src="libs/lineclip.min.js"></script>
<script src="libs/jquery-ui.min.js"></script> <script src="libs/jquery-ui.min.js"></script>
<script src="libs/alea.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/layers.js"></script>
<script src="modules/ui/measurers.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/coastline-editor.js"></script>
<script defer src="modules/ui/labels-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-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/relief-editor.js"></script>
<script defer src="modules/ui/religions-editor.js"></script> <script defer src="modules/ui/religions-editor.js"></script>
<script defer src="modules/ui/markers-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) { "use strict";
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.Pell = factory());
}(this, (function () { 'use strict';
const defaultParagraphSeparatorString = 'defaultParagraphSeparator' window.Pell = (function () {
const formatBlock = 'formatBlock' const defaultParagraphSeparatorString = "defaultParagraphSeparator";
const addEventListener = (parent, type, listener) => parent.addEventListener(type, listener) const formatBlock = "formatBlock";
const appendChild = (parent, child) => parent.appendChild(child) const addEventListener = (parent, type, listener) => parent.addEventListener(type, listener);
const createElement = tag => document.createElement(tag) const appendChild = (parent, child) => parent.appendChild(child);
const queryCommandState = command => document.queryCommandState(command) const createElement = tag => document.createElement(tag);
const queryCommandValue = command => document.queryCommandValue(command) const queryCommandState = command => document.queryCommandState(command);
const exec = (command, value = null) => document.execCommand(command, false, value) const queryCommandValue = command => document.queryCommandValue(command);
const exec = (command, value = null) => document.execCommand(command, false, value);
const defaultActions = { const defaultActions = {
bold: { bold: {
icon: '<b>B</b>', icon: "<b>B</b>",
title: 'Bold', title: "Bold",
state: () => queryCommandState('bold'), state: () => queryCommandState("bold"),
result: () => exec('bold') result: () => exec("bold")
}, },
italic: { italic: {
icon: '<i>I</i>', icon: "<i>I</i>",
title: 'Italic', title: "Italic",
state: () => queryCommandState('italic'), state: () => queryCommandState("italic"),
result: () => exec('italic') result: () => exec("italic")
}, },
underline: { underline: {
icon: '<u>U</u>', icon: "<u>U</u>",
title: 'Underline', title: "Underline",
state: () => queryCommandState('underline'), state: () => queryCommandState("underline"),
result: () => exec('underline') result: () => exec("underline")
}, },
strikethrough: { strikethrough: {
icon: '<strike>S</strike>', icon: "<strike>S</strike>",
title: 'Strike-through', title: "Strike-through",
state: () => queryCommandState('strikeThrough'), state: () => queryCommandState("strikeThrough"),
result: () => exec('strikeThrough') result: () => exec("strikeThrough")
}, },
heading1: { heading1: {
icon: '<b>H<sub>1</sub></b>', icon: "<b>H<sub>1</sub></b>",
title: 'Heading 1', title: "Heading 1",
result: () => exec(formatBlock, '<h1>') result: () => exec(formatBlock, "<h1>")
}, },
heading2: { heading2: {
icon: '<b>H<sub>2</sub></b>', icon: "<b>H<sub>2</sub></b>",
title: 'Heading 2', title: "Heading 2",
result: () => exec(formatBlock, '<h2>') result: () => exec(formatBlock, "<h2>")
}, },
paragraph: { paragraph: {
icon: '&#182;', icon: "&#182;",
title: 'Paragraph', title: "Paragraph",
result: () => exec(formatBlock, '<p>') result: () => exec(formatBlock, "<p>")
}, },
quote: { quote: {
icon: '&#8220; &#8221;', icon: "&#8220; &#8221;",
title: 'Quote', title: "Quote",
result: () => exec(formatBlock, '<blockquote>') result: () => exec(formatBlock, "<blockquote>")
}, },
olist: { olist: {
icon: '&#35;', icon: "&#35;",
title: 'Ordered List', title: "Ordered List",
result: () => exec('insertOrderedList') result: () => exec("insertOrderedList")
}, },
ulist: { ulist: {
icon: '&#8226;', icon: "&#8226;",
title: 'Unordered List', title: "Unordered List",
result: () => exec('insertUnorderedList') result: () => exec("insertUnorderedList")
}, },
code: { code: {
icon: '&lt;/&gt;', icon: "&lt;/&gt;",
title: 'Code', title: "Code",
result: () => exec(formatBlock, '<pre>') result: () => exec(formatBlock, "<pre>")
}, },
line: { line: {
icon: '&#8213;', icon: "&#8213;",
title: 'Horizontal Line', title: "Horizontal Line",
result: () => exec('insertHorizontalRule') result: () => exec("insertHorizontalRule")
}, },
link: { link: {
icon: '&#128279;', icon: "&#128279;",
title: 'Link', title: "Link",
result: () => navigator.clipboard.readText().then(url => exec('createLink', url)) result: () => navigator.clipboard.readText().then(url => exec("createLink", url))
}, },
image: { image: {
icon: '&#128247;', icon: "&#128247;",
title: 'Image', title: "Image",
result: () => { result: () => {
navigator.clipboard.readText().then(url => exec('insertImage', url)) navigator.clipboard.readText().then(url => exec("insertImage", url));
exec('enableObjectResizing') exec("enableObjectResizing");
}
} }
} }
};
const defaultClasses = { const defaultClasses = {
actionbar: 'pell-actionbar', actionbar: "pell-actionbar",
button: 'pell-button', button: "pell-button",
content: 'pell-content', content: "pell-content",
selected: 'pell-button-selected' selected: "pell-button-selected"
} };
const init = settings => { const init = settings => {
const actions = settings.actions const actions = settings.actions
? ( ? settings.actions.map(action => {
settings.actions.map(action => { if (typeof action === "string") return defaultActions[action];
if (typeof action === 'string') return defaultActions[action] else if (defaultActions[action.name]) return {...defaultActions[action.name], ...action};
else if (defaultActions[action.name]) return { ...defaultActions[action.name], ...action } return action;
return action
}) })
) : Object.keys(defaultActions).map(action => defaultActions[action]);
: Object.keys(defaultActions).map(action => defaultActions[action])
const classes = { ...defaultClasses, ...settings.classes } const classes = {...defaultClasses, ...settings.classes};
const defaultParagraphSeparator = settings[defaultParagraphSeparatorString] || 'div' const defaultParagraphSeparator = settings[defaultParagraphSeparatorString] || "div";
const actionbar = createElement('div') const actionbar = createElement("div");
actionbar.className = classes.actionbar actionbar.className = classes.actionbar;
appendChild(settings.element, actionbar) appendChild(settings.element, actionbar);
const content = settings.element.content = createElement('div') const content = (settings.element.content = createElement("div"));
content.contentEditable = true content.contentEditable = true;
content.className = classes.content content.className = classes.content;
content.oninput = ({ target: { firstChild } }) => { content.oninput = ({target: {firstChild}}) => {
if (firstChild && firstChild.nodeType === 3) exec(formatBlock, `<${defaultParagraphSeparator}>`) if (firstChild && firstChild.nodeType === 3) exec(formatBlock, `<${defaultParagraphSeparator}>`);
else if (content.innerHTML === '<br>') content.innerHTML = '' else if (content.innerHTML === "<br>") content.innerHTML = "";
settings.onChange(content.innerHTML) settings.onChange(content.innerHTML);
} };
content.onkeydown = event => { content.onkeydown = event => {
if (event.key === 'Enter' && queryCommandValue(formatBlock) === 'blockquote') { if (event.key === "Enter" && queryCommandValue(formatBlock) === "blockquote") {
setTimeout(() => exec(formatBlock, `<${defaultParagraphSeparator}>`), 0) setTimeout(() => exec(formatBlock, `<${defaultParagraphSeparator}>`), 0);
} }
} };
appendChild(settings.element, content) appendChild(settings.element, content);
actions.forEach(action => { actions.forEach(action => {
const button = createElement('button') const button = createElement("button");
button.className = classes.button button.className = classes.button;
button.innerHTML = action.icon button.innerHTML = action.icon;
button.title = action.title button.title = action.title;
button.setAttribute('type', 'button') button.setAttribute("type", "button");
button.onclick = () => action.result() && content.focus() button.onclick = () => action.result() && content.focus();
if (action.state) { if (action.state) {
const handler = () => button.classList[action.state() ? 'add' : 'remove'](classes.selected) const handler = () => button.classList[action.state() ? "add" : "remove"](classes.selected);
addEventListener(content, 'keyup', handler) addEventListener(content, "keyup", handler);
addEventListener(content, 'mouseup', handler) addEventListener(content, "mouseup", handler);
addEventListener(button, 'click', handler) addEventListener(button, "click", handler);
} }
appendChild(actionbar, button) appendChild(actionbar, button);
}) });
if (settings.styleWithCSS) exec('styleWithCSS') if (settings.styleWithCSS) exec("styleWithCSS");
exec(defaultParagraphSeparatorString, defaultParagraphSeparator) exec(defaultParagraphSeparatorString, defaultParagraphSeparator);
return settings.element return settings.element;
} };
return {exec, init} return {exec, init};
})();
})));

127
main.js
View file

@ -1,9 +1,9 @@
// Azgaar (azgaar.fmg@yandex.com). Minsk, 2017-2021. MIT License // Azgaar (azgaar.fmg@yandex.com). Minsk, 2017-2021. MIT License
// https://github.com/Azgaar/Fantasy-Map-Generator // https://github.com/Azgaar/Fantasy-Map-Generator
'use strict'; "use strict";
const version = '1.63'; // generator version const version = "1.652"; // generator version
document.title += ' v' + version; document.title += " v" + version;
// Logging constants // Logging constants
const PRODUCTION = window.location.host; 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 biomesData = applyDefaultBiomesSystem();
let nameBases = Names.getNameBases(); // cultures-related data 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 let color = d3.scaleSequential(d3.interpolateSpectral); // default color scheme
const lineGen = d3.line().curve(d3.curveBasis); // d3 line generator with default curve interpolation const lineGen = d3.line().curve(d3.curveBasis); // d3 line generator with default curve interpolation
// d3 zoom behavior // d3 zoom behavior
let scale = 1, let scale = 1;
viewX = 0, let viewX = 0;
viewY = 0; let viewY = 0;
const zoom = d3.zoom().scaleExtent([1, 20]).on('zoom', zoomed);
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 // default options
let options = {pinNotes: false}; // options object let options = {pinNotes: false}; // options object
@ -399,26 +413,17 @@ function applyDefaultBiomesSystem() {
} }
function showWelcomeMessage() { 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 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 reddit = link('https://www.reddit.com/r/FantasyMapGenerator', 'Reddit community'); const discord = link("https://discordapp.com/invite/X7E84HU", "Discord server");
const discord = link('https://discordapp.com/invite/X7E84HU', 'Discord server'); const patreon = link("https://www.patreon.com/azgaar", "Patreon");
const patreon = link('https://www.patreon.com/azgaar', 'Patreon');
alertMessage.innerHTML = `The Fantasy Map Generator is updated up to version <b>${version}</b>. 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. This version is compatible with ${changelog}, loaded <i>.map</i> files will be auto-updated.
<ul>${post} <ul>Main changes:
<li>River overview and River editor rework</li> <li>Ability to add river selecting its cells</li>
<li>River generation code refactored and optimized</li> <li>Keep river course on edit</li>
<li>Rivers discharge (flux) and mouth width calculated</li> <li>Refactor river rendering code</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> </ul>
<p>Join our ${discord} and ${reddit} to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.</p> <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() { function doWorkOnZoom(isScaleChanged, isPositionChanged) {
const transform = d3.event.transform; viewbox.attr("transform", `translate(${viewX} ${viewY}) scale(${scale})`);
const scaleDiff = scale - transform.k;
const positionDiff = (viewX - transform.x) | (viewY - transform.y);
if (!positionDiff && !scaleDiff) return;
scale = transform.k; if (isPositionChanged) drawCoordinates();
viewX = transform.x;
viewY = transform.y;
viewbox.attr('transform', transform);
// update grid only if view position if (isScaleChanged) {
if (positionDiff) drawCoordinates();
// rescale only if zoom is changed
if (scaleDiff) {
invokeActiveZooming(); invokeActiveZooming();
drawScaleBar(); drawScaleBar();
} }
// zoom image converter overlay // zoom image converter overlay
const canvas = document.getElementById('canvas'); if (customization === 1) {
if (canvas && +canvas.style.opacity) { const canvas = document.getElementById("canvas");
const img = document.getElementById('image'); if (!canvas || canvas.style.opacity === "0") return;
const ctx = canvas.getContext('2d');
const img = document.getElementById("imageToConvert");
if (!img) return;
const ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.setTransform(scale, 0, 0, scale, viewX, viewY); ctx.setTransform(scale, 0, 0, scale, viewX, viewY);
ctx.drawImage(img, 0, 0, canvas.width, canvas.height); ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
@ -499,15 +498,16 @@ function invokeActiveZooming() {
} }
// rescale lables on zoom // rescale lables on zoom
if (labels.style('display') !== 'none') { if (labels.style("display") !== "none") {
labels.selectAll('g').each(function (d) { labels.selectAll("g").each(function () {
if (this.id === 'burgLabels') return; if (this.id === "burgLabels") return;
const desired = +this.dataset.size; const desired = +this.dataset.size;
const relative = Math.max(rn((desired + desired / scale) / 2, 2), 1); const relative = Math.max(rn((desired + desired / scale) / 2, 2), 1);
this.getAttribute('font-size', relative); if (rescaleLabels.checked) this.setAttribute("font-size", relative);
const hidden = hideLabels.checked && (relative * scale < 6 || relative * scale > 50);
if (hidden) this.classList.add('hidden'); const hidden = hideLabels.checked && (relative * scale < 6 || relative * scale > 60);
else this.classList.remove('hidden'); if (hidden) this.classList.add("hidden");
else this.classList.remove("hidden");
}); });
} }
@ -530,13 +530,14 @@ function invokeActiveZooming() {
// change states halo width // change states halo width
if (!customization) { if (!customization) {
const haloSize = rn(statesHalo.attr('data-width') / scale, 1); const desired = +statesHalo.attr("data-width");
statesHalo.attr('stroke-width', haloSize).style('display', haloSize > 3 ? 'block' : 'none'); const haloSize = rn(desired / scale ** 0.8, 2);
statesHalo.attr("stroke-width", haloSize).style("display", haloSize > 0.1 ? "block" : "none");
} }
// rescale map markers // rescale map markers
if (+markers.attr('rescale') && markers.style('display') !== 'none') { if (+markers.attr("rescale") && markers.style("display") !== "none") {
markers.selectAll('use').each(function (d) { markers.selectAll("use").each(function () {
const x = +this.dataset.x, const x = +this.dataset.x,
y = +this.dataset.y, y = +this.dataset.y,
desired = +this.dataset.size; desired = +this.dataset.size;
@ -639,6 +640,7 @@ function generate() {
drawCoastline(); drawCoastline();
Rivers.generate(); Rivers.generate();
drawRivers();
Lakes.defineGroup(); Lakes.defineGroup();
defineBiomes(); 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.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); 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++) { for (let i = 1, queue = [0]; queue[0] !== -1; i++) {
const start = queue[0]; // first cell const start = queue[0]; // first cell
cells.f[start] = i; // assign feature number cells.f[start] = i; // assign feature number
@ -1331,8 +1342,7 @@ function reMarkFeatures() {
if (land && !eLand) { if (land && !eLand) {
cells.t[q] = 1; cells.t[q] = 1;
cells.t[e] = -1; cells.t[e] = -1;
cells.harbor[q]++; if (!cells.haven[q]) defineHaven(q);
if (!cells.haven[q]) cells.haven[q] = e;
} else if (land && eLand) { } else if (land && eLand) {
if (!cells.t[e] && cells.t[q] === 1) cells.t[e] = 2; 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; else if (!cells.t[q] && cells.t[e] === 1) cells.t[q] = 2;
@ -2146,8 +2156,13 @@ const regenerateMap = debounce(function () {
generate(); generate();
restoreLayers(); restoreLayers();
if (ThreeD.options.isOn) ThreeD.redraw(); if (ThreeD.options.isOn) ThreeD.redraw();
<<<<<<< HEAD
if ($('#worldConfigurator').is(':visible')) editWorld(); if ($('#worldConfigurator').is(':visible')) editWorld();
}, 500); }, 500);
=======
if ($("#worldConfigurator").is(":visible")) editWorld();
}, 1000);
>>>>>>> 597f9ae038fbcc149315df9b1618e64744fb929d
// clear the map // clear the map
function undraw() { function undraw() {

View file

@ -1,12 +1,9 @@
(function (global, factory) { 'use strict';
typeof exports === 'object' && typeof module !== 'undefined' ? (module.exports = factory()) : typeof define === 'function' && define.amd ? define(factory) : (global.BurgsAndStates = factory());
})(this, function () {
'use strict';
window.BurgsAndStates = (function () {
const generate = function () { const generate = function () {
const cells = pack.cells, const {cells, cultures} = pack;
cultures = pack.cultures, const n = cells.i.length;
n = cells.i.length;
cells.burg = new Uint16Array(n); // cell burg cells.burg = new Uint16Array(n); // cell burg
cells.road = new Uint16Array(n); // cell road power cells.road = new Uint16Array(n); // cell road power
@ -80,6 +77,7 @@
TIME && console.time('createStates'); TIME && console.time('createStates');
const states = [{i: 0, name: 'Neutrals'}]; const states = [{i: 0, name: 'Neutrals'}];
const colors = getColors(burgs.length - 1); const colors = getColors(burgs.length - 1);
const each5th = each(5);
burgs.forEach(function (b, i) { burgs.forEach(function (b, i) {
if (!i) return; // skip first element if (!i) return; // skip first element
@ -93,7 +91,7 @@
// states data // states data
const expansionism = rn(Math.random() * powerInput.value + 1, 1); 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 name = Names.getState(basename, b.culture);
const type = cultures[b.culture].type; const type = cultures[b.culture].type;
@ -110,10 +108,10 @@
// place secondary settlements based on geo and economical evaluation // place secondary settlements based on geo and economical evaluation
function placeTowns() { function placeTowns() {
TIME && console.time('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 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) => score[i] > 0 && cells.culture[i] && !cells.burg[i]).sort((a, b) => score[b] - score[a]); // filtered and sorted array of indexes 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 const burgsNumber = Math.min(desiredNumber, sorted.length); // towns to generate
let burgsAdded = 0; let burgsAdded = 0;
@ -175,9 +173,9 @@
if (b.port) { if (b.port) {
b.population = b.population * 1.3; // increase port population b.population = b.population * 1.3; // increase port population
const e = cells.v[i].filter((v) => vertices.c[v].some((c) => c === cells.haven[i])); // vertices of common edge const [x, y] = getMiddlePoint(i, haven);
b.x = rn((vertices.p[e[0]][0] + vertices.p[e[1]][0]) / 2, 2); b.x = x;
b.y = rn((vertices.p[e[0]][1] + vertices.p[e[1]][1]) / 2, 2); b.y = y;
} }
// add random factor // add random factor
@ -477,9 +475,7 @@
// calculate and draw curved state labels for a list of states // calculate and draw curved state labels for a list of states
const drawStateLabels = function (list) { const drawStateLabels = function (list) {
TIME && console.time('drawStateLabels'); TIME && console.time('drawStateLabels');
const cells = pack.cells, const {cells, features, states} = pack;
features = pack.features,
states = pack.states;
const paths = []; // text paths const paths = []; // text paths
lineGen.curve(d3.curveBundle.beta(1)); lineGen.curve(d3.curveBundle.beta(1));
@ -572,8 +568,8 @@
} }
void (function drawLabels() { void (function drawLabels() {
const g = labels.select('#states'), const g = labels.select('#states');
t = defs.select('#textPaths'); const t = defs.select('#textPaths');
const displayed = layerIsOn('toggleLabels'); const displayed = layerIsOn('toggleLabels');
if (!displayed) toggleLabels(); if (!displayed) toggleLabels();
@ -602,8 +598,8 @@
.attr('id', 'textPath_stateLabel' + id); .attr('id', 'textPath_stateLabel' + id);
const pathLength = p[1].length > 1 ? textPath.node().getTotalLength() / letterLength : 0; // path length in letters const pathLength = p[1].length > 1 ? textPath.node().getTotalLength() / letterLength : 0; // path length in letters
let lines = [], let lines = [];
ratio = 100; let ratio = 100;
if (pathLength < s.name.length) { if (pathLength < s.name.length) {
// only short name will fit // only short name will fit
@ -622,10 +618,9 @@
// prolongate path if it's too short // prolongate path if it's too short
if (pathLength && pathLength < lines[0].length) { if (pathLength && pathLength < lines[0].length) {
const points = p[1]; const points = p[1];
const f = points[0], const f = points[0];
l = points[points.length - 1]; const l = points[points.length - 1];
const dx = l[0] - f[0], const [dx, dy] = [l[0] - f[0], l[1] - f[1]];
dy = l[1] - f[1];
const mod = Math.abs((letterLength * lines[0].length) / dx) / 2; const mod = Math.abs((letterLength * lines[0].length) / dx) / 2;
points[0] = [rn(f[0] - dx * mod), rn(f[1] - dy * mod)]; 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)]; points[points.length - 1] = [rn(l[0] + dx * mod), rn(l[1] + dy * mod)];
@ -653,8 +648,8 @@
if (lines.length < 2) return; if (lines.length < 2) return;
// check whether multilined label is generally inside the state. If no, replace with short name label // check whether multilined label is generally inside the state. If no, replace with short name label
const cs = pack.cells.state, const cs = pack.cells.state;
b = el.parentNode.getBBox(); const b = el.parentNode.getBBox();
const c1 = () => +cs[findCell(b.x, b.y)] === id; const c1 = () => +cs[findCell(b.x, b.y)] === id;
const c2 = () => +cs[findCell(b.x + b.width / 2, 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; 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 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 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}; const anarchy = {'Free Territory': 2, Council: 3, Commune: 1, Community: 1};
for (const s of states) { for (const s of states) {
if (list && !list.includes(s.i)) continue; if (list && !list.includes(s.i)) continue;
const tier = expTiers[s.i];
const religion = pack.cells.religion[s.center]; 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 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'; if (isTheocracy) s.form = 'Theocracy';
else if (isAnarchy) s.form = 'Anarchy'; else if (isAnarchy) s.form = 'Anarchy';
else s.form = s.type === 'Naval' ? rw(naval) : rw(generic); else s.form = s.type === 'Naval' ? rw(naval) : rw(generic);
s.formName = selectForm(s); s.formName = selectForm(s, tier);
s.fullName = getFullName(s); s.fullName = getFullName(s);
} }
function selectForm(s) { function selectForm(s, tier) {
const base = pack.cultures[s.culture].base; const base = pack.cultures[s.culture].base;
if (s.form === 'Monarchy') { 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 // Default name depends on exponent tier, some culture bases have special names for tiers
if (s.diplomacy) { 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 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') { if (s.form === 'Republic') {
// Default name is from weighted array, special case for small states with only 1 burg // 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)) { if (trimVowels(s.name) === trimVowels(pack.burgs[s.capital].name)) {
s.name = pack.burgs[s.capital].name; s.name = pack.burgs[s.capital].name;
return 'Free City'; return 'Free City';
@ -1009,10 +1005,15 @@
if (s.form === 'Anarchy') return rw(anarchy); if (s.form === 'Anarchy') return rw(anarchy);
if (s.form === 'Theocracy') { if (s.form === 'Theocracy') {
if (P(0.5) && [0, 1, 2, 3, 4, 6, 8, 9, 13, 15, 20].includes(base)) return 'Diocese'; // Euporean // European
if (P(0.9) && [7, 5].includes(base)) return 'Eparchy'; // Greek, Ruthenian 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.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); return rw(theocracy);
} }
} }
@ -1255,4 +1256,4 @@
generateProvinces, generateProvinces,
updateCultures updateCultures
}; };
}); })();

View file

@ -1,170 +1,364 @@
(function (global, factory) { "use strict";
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.COA = factory());
}(this, (function () {'use strict';
window.COA = (function () {
const tinctures = { const tinctures = {
field: { metals: 3, colours: 4, stains: +P(.03), patterns: 1 }, field: {metals: 3, colours: 4, stains: +P(0.03), patterns: 1},
division: { metals: 5, colours: 8, stains: +P(.03), patterns: 1 }, division: {metals: 5, colours: 8, stains: +P(0.03), patterns: 1},
charge: { metals: 2, colours: 3, stains: +P(.05), patterns: 0 }, charge: {metals: 2, colours: 3, stains: +P(0.05), patterns: 0},
metals: { argent: 3, or: 2 }, metals: {argent: 3, or: 2},
colours: { gules: 5, azure: 4, sable: 3, purpure: 3, vert: 2 }, colours: {gules: 5, azure: 4, sable: 3, purpure: 3, vert: 2},
stains: { murrey: 1, sanguine: 1, tenné: 1 }, stains: {murrey: 1, sanguine: 1, tenné: 1},
patterns: { patterns: {
semy: 8, ermine: 6, semy: 8,
vair: 4, counterVair: 1, vairInPale: 1, vairEnPointe: 2, vairAncien: 2, ermine: 6,
potent: 2, counterPotent: 1, potentInPale: 1, potentEnPointe: 1, vair: 4,
chequy: 8, lozengy: 5, fusily: 2, pally: 8, barry: 10, gemelles: 1, counterVair: 1,
bendy: 8, bendySinister: 4, palyBendy: 2, barryBendy: 1, vairInPale: 1,
pappellony: 2, pappellony2: 3, scaly: 1, plumetty: 1, vairEnPointe: 2,
masoned: 6, fretty: 3, grillage: 1, chainy: 1, maily: 2, honeycombed: 1 } 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 = { const charges = {
// categories selection // 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 }, 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 }, 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 }, semy: {conventional: 12, crosses: 3, plants: 1},
// generic categories // generic categories
conventional: { conventional: {
lozenge: 2, fusil: 4, mascle: 4, rustre: 2, lozengeFaceted: 3, lozengePloye: 1, roundel: 4, roundel2: 3, annulet: 4, lozenge: 2,
mullet: 5, mulletPierced: 1, mulletFaceted: 1, mullet4: 3, mullet6: 4, mullet6Pierced: 1, mullet6Faceted: 1, mullet7: 1, mullet8: 1, mullet10: 1, fusil: 4,
estoile: 1, compassRose: 1, billet: 5, delf: 0, triangle: 3, trianglePierced: 1, goutte: 4, heart: 4, pique: 2, carreau: 1, trefle: 2, mascle: 4,
fleurDeLis: 6, sun: 3, sunInSplendour: 1, crescent: 5, fountain: 1 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: { crosses: {
crossHummetty: 15, crossVoided: 1, crossPattee: 2, crossPatteeAlisee: 1, crossFormee: 1, crossFormee2: 2, crossPotent: 2, crossJerusalem:1, crossHummetty: 15,
crosslet: 1, crossClechy: 3, crossBottony: 1, crossFleury: 3, crossPatonce: 1, crossPommy: 1, crossGamma: 1, crossArrowed: 1, crossFitchy: 1, crossVoided: 1,
crossCercelee: 1, crossMoline: 2, crossFourchy: 1, crossAvellane: 1, crossErminee: 1, crossBiparted: 1, crossMaltese: 3, crossTemplar: 2, crossPattee: 2,
crossCeltic: 1, crossCeltic2: 1, crossTriquetra: 1, crossCarolingian: 1, crossOccitan: 1, crossSaltire: 3, crossBurgundy: 1, crossPatteeAlisee: 1,
crossLatin: 3, crossPatriarchal: 1, crossOrthodox: 1, crossCalvary: 1, crossDouble: 1, crossTau: 1, crossSantiago: 1, crossAnkh: 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: { animals: {
lionRampant: 5, lionPassant: 2, lionPassantGuardant: 1, wolfRampant: 1, wolfPassant: 1, wolfStatant: 1, greyhoundCourant: 1, boarRampant: 1, lionRampant: 5,
horseRampant: 2, horseSalient: 1, bearRampant: 2, bearPassant: 1, bullPassant: 1, goat: 1, lamb: 1, elephant: 1, camel: 1 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 }, 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 }, 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 }, 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 }, plants: {tree: 1, oak: 1, cinquefoil: 1, rose: 1},
agriculture: { garb: 1, rake: 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 }, 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 }, bodyparts: {hand: 4, head: 1, headWreathed: 1},
people: { cavalier: 3, monk: 1, angel: 2 }, people: {cavalier: 3, monk: 1, angel: 2},
architecture: { tower: 1, castle: 1 }, architecture: {tower: 1, castle: 1},
miscellaneous: { miscellaneous: {
crown: 3, orb: 1, chalice: 1, key: 1, buckle: 1, bugleHorn: 1, bugleHorn2: 1, bell: 2, pot: 1, bucket: 1, horseshoe: 3, crown: 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 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: // selection based on culture type:
Naval: { anchor: 3, boat: 1, lymphad: 2, armillarySphere: 1, escallop: 1, dolphin: 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 }, 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 }, 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 }, 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 }, 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 }, 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 // selection based on type
City: { key: 3, bell: 2, lute: 1, tower: 1, castle: 1, mallet: 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 }, 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 }, Сathedra: {chalice: 1, orb: 1, crosier: 2, lamb: 1, monk: 2, angel: 3, crossLatin: 2, crossPatriarchal: 1, crossOrthodox: 1, crossCalvary: 1},
// specific cases // specific cases
natural: { fountain: "azure", garb: "or", raven: "sable" }, // charges to mainly use predefined colours natural: {fountain: "azure", garb: "or", raven: "sable"}, // charges to mainly use predefined colours
sinister: [ // charges that can be sinister sinister: [
"crossGamma", "lionRampant", "lionPassant", "wolfRampant", "wolfPassant", "wolfStatant", "wolfHeadErased", "greyhoundСourant", "boarRampant", // charges that can be sinister
"horseRampant", "horseSalient", "bullPassant", "bearRampant", "bearPassant", "goat", "lamb", "elephant", "eagle", "raven", "cock", "parrot", "crossGamma",
"swan", "swanErased", "heron", "pike", "dragonPassant", "dragonRampant", "wyvern", "wyvernWithWingsDisplayed", "griffinPassant", "griffinRampant", "lionRampant",
"unicornRampant", "pegasus", "serpent", "hatchet", "lochaberAxe", "hand", "wing", "wingSword", "lute", "harp", "bow", "head", "headWreathed", "lionPassant",
"knight", "lymphad", "log", "crosier", "dolphin", "sabre", "monk", "owl", "axe", "camel", "fasces", "lionPassantGuardant", "helmet"], "wolfRampant",
reversed: [ // charges that can be reversed "wolfPassant",
"goutte", "mullet", "mullet7", "crescent", "crossTau", "cancer", "sword", "sabresCrossed", "hand", "wolfStatant",
"horseshoe", "bowWithArrow", "arrow", "arrowsSheaf", "rake", "crossTriquetra", "crossLatin", "crossTau" "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 = { 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 }, 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 }, complex: {e: 40, beh: 1, kn: 1, jeo: 1, abc: 2, jln: 7, jlh: 2, def: 1, abcpqh: 1},
divisions: { divisions: {
perPale: { e: 15, pq: 5, jo: 2, jl: 2, ABCDEFGHIJKL: 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 }, perFess: {e: 12, kn: 4, jkl: 2, gizgiz: 1, jlh: 3, kmo: 1, ABCDEFGHIJKL: 1},
perBend: { e: 5, lm: 5, bcfdgh: 1 }, perBend: {e: 5, lm: 5, bcfdgh: 1},
perBendSinister: { e: 1, jo: 1 }, perBendSinister: {e: 1, jo: 1},
perCross: { e: 4, jlmo: 1, j: 1, jo: 2, jl: 1 }, perCross: {e: 4, jlmo: 1, j: 1, jo: 2, jl: 1},
perChevron: { e: 1, jlh: 1, dfk: 1, dfbh: 2, bdefh: 1 }, perChevron: {e: 1, jlh: 1, dfk: 1, dfbh: 2, bdefh: 1},
perChevronReversed: { e: 1, mok: 2, dfh: 2, dfbh: 1, 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 }, perSaltire: {bhdf: 8, e: 3, abcdefgzi: 1, bh: 1, df: 1, ABCDEFGHIJKL: 1},
perPile: { ee: 3, be: 2, abceh: 1, abcabc: 1, jleh: 1 } perPile: {ee: 3, be: 2, abceh: 1, abcabc: 1, jleh: 1}
}, },
ordinariesOn: { ordinariesOn: {
pale: { ee: 12, beh: 10, kn: 3, bb: 1 }, pale: {ee: 12, beh: 10, kn: 3, bb: 1},
fess: { ee: 1, def: 3 }, fess: {ee: 1, def: 3},
bar: { defdefdef: 1 }, bar: {defdefdef: 1},
fessCotissed: { ee: 1, def: 3 }, fessCotissed: {ee: 1, def: 3},
fessDoubleCotissed: { ee: 1, defdef: 3 }, fessDoubleCotissed: {ee: 1, defdef: 3},
bend: { ee: 2, jo: 1, joe: 1 }, bend: {ee: 2, jo: 1, joe: 1},
bendSinister: { ee: 1, lm: 1, lem: 4 }, bendSinister: {ee: 1, lm: 1, lem: 4},
bendlet: { joejoejoe: 1 }, bendlet: {joejoejoe: 1},
bendletSinister: { lemlemlem: 1 }, bendletSinister: {lemlemlem: 1},
bordure: { ABCDEFGHIJKL: 1 }, bordure: {ABCDEFGHIJKL: 1},
chief: { abc: 5, bbb: 1 }, chief: {abc: 5, bbb: 1},
quarter: { jjj: 1 }, quarter: {jjj: 1},
canton: { yyyy: 1 }, canton: {yyyy: 1},
cross: { eeee: 1, behdfbehdf: 3, behbehbeh: 2 }, cross: {eeee: 1, behdfbehdf: 3, behbehbeh: 2},
crossParted: { e: 5, ee: 1 }, crossParted: {e: 5, ee: 1},
saltire: { ee: 5, jlemo: 1 }, saltire: {ee: 5, jlemo: 1},
saltireParted: { e: 5, ee: 1 }, saltireParted: {e: 5, ee: 1},
pall: { ee: 1, jleh: 5, jlhh: 3 }, pall: {ee: 1, jleh: 5, jlhh: 3},
pallReversed: { ee: 1, bemo: 5 }, pallReversed: {ee: 1, bemo: 5},
pile: { bbb: 1 }, pile: {bbb: 1},
pileInBend: { eeee: 1, eeoo: 1 }, pileInBend: {eeee: 1, eeoo: 1},
pileInBendSinister: { eeee: 1, eemm: 1 } pileInBendSinister: {eeee: 1, eemm: 1}
}, },
ordinariesOff: { ordinariesOff: {
pale: { yyy: 1 }, pale: {yyy: 1},
fess: { abc: 3, abcz: 1 }, fess: {abc: 3, abcz: 1},
bar: { abc: 2, abcgzi: 1, jlh: 5, bgi: 2, ach: 1 }, bar: {abc: 2, abcgzi: 1, jlh: 5, bgi: 2, ach: 1},
gemelle: { abc: 1 }, gemelle: {abc: 1},
bend: { ccg: 2, ccc: 1 }, bend: {ccg: 2, ccc: 1},
bendSinister: { aai: 2, aaa: 1 }, bendSinister: {aai: 2, aaa: 1},
bendlet: { ccg: 2, ccc: 1 }, bendlet: {ccg: 2, ccc: 1},
bendletSinister: { aai: 2, aaa: 1 }, bendletSinister: {aai: 2, aaa: 1},
bordure: { e: 4, jleh:2, kenken: 1, peqpeq: 1 }, bordure: {e: 4, jleh: 2, kenken: 1, peqpeq: 1},
orle: { e: 4, jleh: 1, kenken: 1, peqpeq: 1 }, orle: {e: 4, jleh: 1, kenken: 1, peqpeq: 1},
chief: { emo: 2, emoz: 1, ez: 2 }, chief: {emo: 2, emoz: 1, ez: 2},
terrace: { e: 5, def: 1, bdf: 3 }, terrace: {e: 5, def: 1, bdf: 3},
mount: { e: 5, def: 1, bdf: 3 }, mount: {e: 5, def: 1, bdf: 3},
point: { e: 2, def: 1, bdf: 3, acbdef: 1 }, point: {e: 2, def: 1, bdf: 3, acbdef: 1},
flaunches: { e: 3, kn: 1, beh: 3 }, flaunches: {e: 3, kn: 1, beh: 3},
gyron: { bh: 1 }, gyron: {bh: 1},
quarter: { e: 1 }, quarter: {e: 1},
canton: { e: 5, beh: 1, def: 1, bdefh: 1, kn: 1 }, canton: {e: 5, beh: 1, def: 1, bdefh: 1, kn: 1},
cross: { acgi: 1 }, cross: {acgi: 1},
pall: { BCKFEILGJbdmfo: 1 }, pall: {BCKFEILGJbdmfo: 1},
pallReversed: { aczac: 1 }, pallReversed: {aczac: 1},
chevron: { ach: 3, hhh: 1 }, chevron: {ach: 3, hhh: 1},
chevronReversed: { bbb: 1 }, chevronReversed: {bbb: 1},
pile: { acdfgi: 1, acac: 1 }, pile: {acdfgi: 1, acac: 1},
pileInBend: { cg: 1 }, pileInBend: {cg: 1},
pileInBendSinister: { ai: 1 }, pileInBendSinister: {ai: 1},
label: { defgzi: 2, eh: 3, defdefhmo: 1, egiegi: 1, pqn: 5 } label: {defgzi: 2, eh: 3, defdefhmo: 1, egiegi: 1, pqn: 5}
}, },
// charges // charges
inescutcheon: { e: 4, jln: 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 }, 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 }, 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 }, 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 }, 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 }, 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 }, 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 }, griffinPassant: {e: 10, def: 1, abc: 1, bdefh: 1, jlh: 1, abcpqh: 1},
boarRampant: { e: 12, beh: 1, kn: 1, jln: 2 }, boarRampant: {e: 12, beh: 1, kn: 1, jln: 2},
eagle: { e: 15, beh: 1, kn: 1, abc: 1, jlh: 2, def: 2, pq: 1 }, 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 }, raven: {e: 15, beh: 1, kn: 1, jeo: 1, abc: 3, jln: 3, def: 1},
wyvern: { e: 10, jln: 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 }, 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 }, 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 }, hand: {e: 10, jln: 2, kn: 1, jeo: 1, abc: 2, pqe: 1},
armillarySphere: {e: 1}, armillarySphere: {e: 1},
tree: {e: 1}, tree: {e: 1},
lymphad: {e: 1}, lymphad: {e: 1},
@ -175,33 +369,92 @@
}; };
const lines = { const lines = {
straight: 50, wavy: 8, engrailed: 4, invecked: 3, rayonne: 3, embattled: 1, raguly: 1, urdy: 1, dancetty: 1, indented: 2, straight: 50,
dentilly: 1, bevilled: 1, angled: 1, flechy: 1, barby: 1, enclavy: 1, escartely: 1, arched: 2, archedReversed: 1, nowy: 1, nowyReversed: 1, wavy: 8,
embattledGhibellin: 1, embattledNotched: 1, embattledGrady: 1, dovetailedIndented: 1, dovetailed: 1, engrailed: 4,
potenty: 1, potentyDexter: 1, potentySinister: 1, nebuly: 2, seaWaves: 1, dragonTeeth: 1, firTrees: 1 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 = { 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, perPale: lines,
perFess: lines, perFess: lines,
perBend: lines, perBend: lines,
perBendSinister: lines, perBendSinister: lines,
perChevron: lines, perChevron: lines,
perChevronReversed: 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 perPile: lines
}; };
const ordinaries = { const ordinaries = {
lined: { lined: {
pale: 7, fess: 5, bend: 3, bendSinister: 2, chief: 5, bar: 2, gemelle: 1, fessCotissed: 1, fessDoubleCotissed: 1, pale: 7,
bendlet: 2, bendletSinister: 1, terrace: 3, cross: 6, crossParted: 1, saltire: 2, saltireParted: 1 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: { straight: {
bordure: 8, orle: 4, mount: 1, point: 2, flaunches: 1, gore: 1, bordure: 8,
gyron: 1, quarter: 1, canton: 2, pall: 3, pallReversed: 2, chevron: 4, chevronReversed: 3, orle: 4,
pile: 2, pileInBend: 2, pileInBendSinister: 1, piles: 1, pilesInPoint: 2, label: 1 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}, simple: {round: 12, oval: 6, vesicaPiscis: 1, square: 1, diamond: 2, no: 0},
fantasy: {fantasy1: 2, fantasy2: 2, fantasy3: 1, fantasy4: 1, fantasy5: 3}, 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} 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") { if (!parent || parent === "custom") {
parent = null; parent = null;
kinship = 0; kinship = 0;
dominion = 0; dominion = 0;
} }
let usedPattern = null, usedTinctures = []; let usedPattern = null,
usedTinctures = [];
const t1 = P(kinship) ? parent.t1 : getTincture("field"); const t1 = P(kinship) ? parent.t1 : getTincture("field");
if (t1.includes("-")) usedPattern = t1; if (t1.includes("-")) usedPattern = t1;
const coa = {t1}; const coa = {t1};
let charge = P(usedPattern ? .5 : .93) ? true : false; // 80% for charge let charge = P(usedPattern ? 0.5 : 0.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 linedOrdinary = (charge && P(0.3)) || P(0.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 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 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 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 - .1) ? parent.division.division : rw(divisions.variants) : null; const division = divisioned ? (parent?.division && P(kinship - 0.1) ? parent.division.division : rw(divisions.variants)) : null;
if (charge) charge = if (charge) charge = parent?.charges && P(kinship - 0.1) ? parent.charges[0].charge : type && type !== "Generic" && P(0.2) ? rw(charges[type]) : selectCharge();
parent?.charges && P(kinship - .1) ? parent.charges[0].charge :
type && type !== "Generic" && P(.2) ? rw(charges[type]) :
selectCharge();
if (division) { 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}; 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) { if (ordinary) {
coa.ordinaries = [{ordinary, t: getTincture("charge", usedTinctures, coa.t1)}]; coa.ordinaries = [{ordinary, t: getTincture("charge", usedTinctures, coa.t1)}];
if (linedOrdinary) coa.ordinaries[0].line = usedPattern || (division && P(.7)) ? "straight" : rw(lines); if (linedOrdinary) coa.ordinaries[0].line = usedPattern || (division && P(0.7)) ? "straight" : rw(lines);
if (division && !charge && !usedPattern && P(.5) && ordinary !== "bordure" && ordinary !== "orle") { if (division && !charge && !usedPattern && P(0.5) && ordinary !== "bordure" && ordinary !== "orle") {
if (P(.8)) coa.ordinaries[0].divided = "counter"; // 40% if (P(0.8)) coa.ordinaries[0].divided = "counter";
else if (P(.6)) coa.ordinaries[0].divided = "field"; // 6% // 40%
else if (P(0.6)) coa.ordinaries[0].divided = "field";
// 6%
else coa.ordinaries[0].divided = "division"; // 4% else coa.ordinaries[0].divided = "division"; // 4%
} }
} }
if (charge) { if (charge) {
let p = "e", t = "gules"; let p = "e",
t = "gules";
const ordinaryT = coa.ordinaries ? coa.ordinaries[0].t : null; 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) // place charge over ordinary (use tincture of field type)
p = rw(positions.ordinariesOn[ordinary]); p = rw(positions.ordinariesOn[ordinary]);
while (charges.natural[charge] === ordinaryT) charge = selectCharge(); while (charges.natural[charge] === ordinaryT) charge = selectCharge();
t = !usedPattern && P(.3) ? coa.t1 : getTincture("charge", [], ordinaryT); t = !usedPattern && P(0.3) ? coa.t1 : getTincture("charge", [], ordinaryT);
} else if (positions.ordinariesOff[ordinary] && P(.95)) { } else if (positions.ordinariesOff[ordinary] && P(0.95)) {
// place charge out of ordinary (use tincture of ordinary type) // place charge out of ordinary (use tincture of ordinary type)
p = rw(positions.ordinariesOff[ordinary]); p = rw(positions.ordinariesOff[ordinary]);
while (charges.natural[charge] === coa.t1) charge = selectCharge(); 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]) { } else if (positions.divisions[division]) {
// place charge in fields made by division // place charge in fields made by division
p = rw(positions.divisions[division]); p = rw(positions.divisions[division]);
@ -289,43 +543,41 @@
if (charges.natural[charge]) t = charges.natural[charge]; // natural tincture if (charges.natural[charge]) t = charges.natural[charge]; // natural tincture
coa.charges = [{charge, t, p}]; coa.charges = [{charge, t, p}];
if (p === "ABCDEFGHIKL" && P(.95)) { if (p === "ABCDEFGHIKL" && P(0.95)) {
// add central charge if charge is in bordure // add central charge if charge is in bordure
coa.charges[0].charge = rw(charges.conventional); coa.charges[0].charge = rw(charges.conventional);
const charge = selectCharge(charges.single); const charge = selectCharge(charges.single);
const t = getTincture("charge", usedTinctures, coa.t1); const t = getTincture("charge", usedTinctures, coa.t1);
coa.charges.push({charge, t, p: "e"}); coa.charges.push({charge, t, p: "e"});
} else if (P(.8) && charge === "inescutcheon") { } else if (P(0.8) && charge === "inescutcheon") {
// add charge to inescutcheon // add charge to inescutcheon
const charge = selectCharge(charges.types); const charge = selectCharge(charges.types);
const t2 = getTincture("charge", [], t); 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) { } else if (division && !ordinary) {
const allowCounter = !usedPattern && (!coa.line || coa.line === "straight"); const allowCounter = !usedPattern && (!coa.line || coa.line === "straight");
// dimidiation: second charge at division basic positons // 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"; coa.charges[0].divided = "field";
if (P(.95)) { if (P(0.95)) {
const p2 = p === "e" || P(.5) ? "e" : rw(positions.divisions[division]); const p2 = p === "e" || P(0.5) ? "e" : rw(positions.divisions[division]);
const charge = selectCharge(charges.single); const charge = selectCharge(charges.single);
const t = getTincture("charge", usedTinctures, coa.division.t); const t = getTincture("charge", usedTinctures, coa.division.t);
coa.charges.push({charge, t, p: p2, divided: "division"}); coa.charges.push({charge, t, p: p2, divided: "division"});
} }
} } else if (allowCounter && P(0.4)) coa.charges[0].divided = "counter";
else if (allowCounter && P(.4)) coa.charges[0].divided = "counter"; // counterchanged, 40% // counterchanged, 40%
else if (["perPale", "perFess", "perBend", "perBendSinister"].includes(division) && P(.8)) { // place 2 charges in division standard positions else if (["perPale", "perFess", "perBend", "perBendSinister"].includes(division) && P(0.8)) {
const [p1, p2] = division === "perPale" ? ["p", "q"] : // place 2 charges in division standard positions
division === "perFess" ? ["k", "n"] : const [p1, p2] = division === "perPale" ? ["p", "q"] : division === "perFess" ? ["k", "n"] : division === "perBend" ? ["l", "m"] : ["j", "o"]; // perBendSinister
division === "perBend" ? ["l", "m"] :
["j", "o"]; // perBendSinister
coa.charges[0].p = p1; coa.charges[0].p = p1;
const charge = selectCharge(charges.single); const charge = selectCharge(charges.single);
const t = getTincture("charge", usedTinctures, coa.division.t); const t = getTincture("charge", usedTinctures, coa.division.t);
coa.charges.push({charge, t, p: p2}); coa.charges.push({charge, t, p: p2});
} } else if (["perCross", "perSaltire"].includes(division) && P(0.5)) {
else if (["perCross", "perSaltire"].includes(division) && P(.5)) { // place 4 charges in division standard positions // place 4 charges in division standard positions
const [p1, p2, p3, p4] = division === "perCross" ? ["j", "l", "m", "o"] : ["b", "d", "f", "h"]; const [p1, p2, p3, p4] = division === "perCross" ? ["j", "l", "m", "o"] : ["b", "d", "f", "h"];
coa.charges[0].p = p1; coa.charges[0].p = p1;
@ -338,8 +590,7 @@
const c4 = selectCharge(charges.single); const c4 = selectCharge(charges.single);
const t4 = getTincture("charge", [], coa.t1); 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}); 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)); coa.charges.forEach(c => defineChargeAttributes(c));
@ -351,8 +602,8 @@
c.p = [...new Set(c.p)].join(""); c.p = [...new Set(c.p)].join("");
// define orientation // define orientation
if (P(.02) && charges.sinister.includes(c.charge)) c.sinister = 1; if (P(0.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.reversed.includes(c.charge)) c.reversed = 1;
} }
} }
@ -380,24 +631,26 @@
if (!coa.charges) coa.charges = []; if (!coa.charges) coa.charges = [];
coa.charges.push({charge, t: t2, p: "y", size: 0.5}); 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) { 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]); return type === "inescutcheon" ? "inescutcheon" : rw(charges[type]);
} }
// select tincture: element type (field, division, charge), used field tinctures, field type to follow RoT // select tincture: element type (field, division, charge), used field tinctures, field type to follow RoT
function getTincture(element, fields = [], 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 let type = rw(tinctures[element]); // metals, colours, stains, patterns
if (RoT && type !== "patterns") type = getType(base) === "metals" ? "colours" : "metals"; // follow RoT 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 if (type === "metals" && fields.includes("or") && fields.includes("argent")) type = "colours"; // exclude metals overuse
let tincture = rw(tinctures[type]); 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 if (type !== "patterns" && element !== "charge") usedTinctures.push(tincture); // add field tincture
@ -425,39 +678,60 @@
if (Object.keys(tinctures.stains).includes(tincture)) return "stains"; if (Object.keys(tinctures.stains).includes(tincture)) return "stains";
else return "pattern"; else return "pattern";
} }
} }
function definePattern(pattern, element, size = "") { function definePattern(pattern, element, size = "") {
let t1 = null, t2 = null; let t1 = null,
if (P(.1)) size = "-small"; t2 = null;
else if (P(.1)) size = "-smaller"; if (P(0.1)) size = "-small";
else if (P(.01)) size = "-big"; else if (P(0.1)) size = "-smaller";
else if (P(.005)) size = "-smallest"; else if (P(0.01)) size = "-big";
else if (P(0.005)) size = "-smallest";
// apply standard tinctures // apply standard tinctures
if (P(.5) && ["vair", "vairInPale", "vairEnPointe"].includes(pattern)) {t1 = "azure"; t2 = "argent";} if (P(0.5) && ["vair", "vairInPale", "vairEnPointe"].includes(pattern)) {
else if (P(.8) && pattern === "ermine") {t1 = "argent"; t2 = "sable";} t1 = "azure";
else if (pattern === "pappellony") { t2 = "argent";
if (P(.2)) {t1 = "gules"; t2 = "or";} } else if (P(0.8) && pattern === "ermine") {
else if (P(.2)) {t1 = "argent"; t2 = "sable";} t1 = "argent";
else if (P(.2)) {t1 = "azure"; t2 = "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") { } else if (pattern === "masoned") {
if (P(.3)) {t1 = "gules"; t2 = "argent";} if (P(0.3)) {
else if (P(.3)) {t1 = "argent"; t2 = "sable";} t1 = "gules";
else if (P(.1)) {t1 = "or"; t2 = "sable";} t2 = "argent";
} else if (P(0.3)) {
t1 = "argent";
t2 = "sable";
} else if (P(0.1)) {
t1 = "or";
t2 = "sable";
} }
else if (pattern === "fretty") { } else if (pattern === "fretty") {
if (t2 === "sable" || P(.35)) {t1 = "argent"; t2 = "gules";} if (t2 === "sable" || P(0.35)) {
else if (P(.25)) {t1 = "sable"; t2 = "or";} t1 = "argent";
else if (P(.15)) {t1 = "gules"; t2 = "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); } else if (pattern === "semy") pattern += "_of_" + selectCharge(charges.semy);
if (!t1 || !t2) { if (!t1 || !t2) {
const startWithMetal = P(.7); const startWithMetal = P(0.7);
t1 = startWithMetal ? rw(tinctures.metals) : rw(tinctures.colours); t1 = startWithMetal ? rw(tinctures.metals) : rw(tinctures.colours);
t2 = startWithMetal ? rw(tinctures.colours) : rw(tinctures.metals); t2 = startWithMetal ? rw(tinctures.colours) : rw(tinctures.metals);
} }
@ -474,28 +748,30 @@
function replaceTincture(t, n) { function replaceTincture(t, n) {
const type = getType(t); const type = getType(t);
while (!n || n === t) {n = rw(tinctures[type]);} while (!n || n === t) {
n = rw(tinctures[type]);
}
return n; return n;
} }
function getSize(p, o = null, d = null) { function getSize(p, o = null, d = null) {
if (p === "e" && (o === "bordure" || o === "orle")) return 1.1; if (p === "e" && (o === "bordure" || o === "orle")) return 1.1;
if (p === "e") return 1.5; if (p === "e") return 1.5;
if (p === "jln" || p === "jlh") return .7; if (p === "jln" || p === "jlh") return 0.7;
if (p === "abcpqh" || p === "ez" || p === "be") return .5; if (p === "abcpqh" || p === "ez" || p === "be") return 0.5;
if (["a", "b", "c", "d", "f", "g", "h", "i", "bh", "df"].includes(p)) return .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 .6; if (["j", "l", "m", "o", "jlmo"].includes(p) && d === "perCross") return 0.6;
if (p.length > 10) return .18; // >10 (bordure) if (p.length > 10) return 0.18; // >10 (bordure)
if (p.length > 7) return .3; // 8, 9, 10 if (p.length > 7) return 0.3; // 8, 9, 10
if (p.length > 4) return .4; // 5, 6, 7 if (p.length > 4) return 0.4; // 5, 6, 7
if (p.length > 2) return .5; // 3, 4 if (p.length > 2) return 0.5; // 3, 4
return .7; // 1, 2 return 0.7; // 1, 2
} }
return coa; return coa;
} };
const getShield = function(culture, state) { const getShield = function (culture, state) {
const emblemShape = document.getElementById("emblemShape"); const emblemShape = document.getElementById("emblemShape");
const shapeGroup = emblemShape.selectedOptions[0]?.parentNode.label || "Diversiform"; const shapeGroup = emblemShape.selectedOptions[0]?.parentNode.label || "Diversiform";
if (shapeGroup !== "Diversiform") return emblemShape.value; if (shapeGroup !== "Diversiform") return emblemShape.value;
@ -504,11 +780,10 @@
if (pack.cultures[culture].shield) return pack.cultures[culture].shield; if (pack.cultures[culture].shield) return pack.cultures[culture].shield;
console.error("Shield shape is not defined on culture level", pack.cultures[culture]); console.error("Shield shape is not defined on culture level", pack.cultures[culture]);
return "heater"; return "heater";
} };
const toString = coa => JSON.stringify(coa).replaceAll("#", "%23"); const toString = coa => JSON.stringify(coa).replaceAll("#", "%23");
const copy = coa => JSON.parse(JSON.stringify(coa)); const copy = coa => JSON.parse(JSON.stringify(coa));
return {generate, toString, copy, getShield, shields}; 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) { "use strict";
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.Cultures = factory());
}(this, (function () {'use strict';
window.Cultures = (function () {
let cells; let cells;
const generate = function() { const generate = function () {
TIME && console.time('generateCultures'); TIME && console.time("generateCultures");
cells = pack.cells; cells = pack.cells;
cells.culture = new Uint16Array(cells.i.length); // cell cultures cells.culture = new Uint16Array(cells.i.length); // cell cultures
let count = Math.min(+culturesInput.value, +culturesSet.selectedOptions[0].dataset.max); let count = Math.min(+culturesInput.value, +culturesSet.selectedOptions[0].dataset.max);
@ -17,13 +14,19 @@
count = Math.floor(populated.length / 50); count = Math.floor(populated.length / 50);
if (!count) { if (!count) {
WARN && console.warn(`There are no populated cells. Cannot generate cultures`); 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 = ` alertMessage.innerHTML = `
The climate is harsh and people cannot live in this world.<br> The climate is harsh and people cannot live in this world.<br>
No cultures, states and burgs will be created.<br> No cultures, states and burgs will be created.<br>
Please consider changing climate settings in the World Configurator`; Please consider changing climate settings in the World Configurator`;
$("#alert").dialog({resizable: false, title: "Extreme climate warning", $("#alert").dialog({
buttons: {Ok: function() {$(this).dialog("close");}} resizable: false,
title: "Extreme climate warning",
buttons: {
Ok: function () {
$(this).dialog("close");
}
}
}); });
return; return;
} else { } else {
@ -32,22 +35,28 @@
There are only ${populated.length} populated cells and it's insufficient livable area.<br> 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> Only ${count} out of ${culturesInput.value} requested cultures will be generated.<br>
Please consider changing climate settings in the World Configurator`; Please consider changing climate settings in the World Configurator`;
$("#alert").dialog({resizable: false, title: "Extreme climate warning", $("#alert").dialog({
buttons: {Ok: function() {$(this).dialog("close");}} 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 centers = d3.quadtree();
const colors = getColors(count); const colors = getColors(count);
const emblemShape = document.getElementById("emblemShape").value; const emblemShape = document.getElementById("emblemShape").value;
const codes = []; const codes = [];
cultures.forEach(function(c, i) { cultures.forEach(function (c, i) {
const cell = c.center = placeCenter(c.sort ? c.sort : (i) => cells.s[i]); const cell = (c.center = placeCenter(c.sort ? c.sort : i => cells.s[i]));
centers.add(cells.p[cell]); centers.add(cells.p[cell]);
c.i = i+1; c.i = i + 1;
delete c.odd; delete c.odd;
delete c.sort; delete c.sort;
c.color = colors[i]; c.color = colors[i];
@ -56,20 +65,24 @@
c.origin = 0; c.origin = 0;
c.code = abbreviate(c.name, codes); c.code = abbreviate(c.name, codes);
codes.push(c.code); codes.push(c.code);
cells.culture[cell] = i+1; cells.culture[cell] = i + 1;
if (emblemShape === "random") c.shield = getRandomShield(); if (emblemShape === "random") c.shield = getRandomShield();
}); });
function placeCenter(v) { function placeCenter(v) {
let c, spacing = (graphWidth + graphHeight) / 2 / count; let c,
const sorted = [...populated].sort((a, b) => v(b) - v(a)), max = Math.floor(sorted.length / 2); spacing = (graphWidth + graphHeight) / 2 / count;
do {c = sorted[biased(0, max, 5)]; spacing *= .9;} const sorted = [...populated].sort((a, b) => v(b) - v(a)),
while (centers.find(cells.p[c][0], cells.p[c][1], spacing) !== undefined); 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; return c;
} }
// the first culture with id 0 is for wildlands // 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 // make sure all bases exist in nameBases
if (!nameBases.length) { if (!nameBases.length) {
@ -77,7 +90,7 @@
nameBases = Names.getNameBases(); nameBases = Names.getNameBases();
} }
cultures.forEach(c => c.base = c.base % nameBases.length); cultures.forEach(c => (c.base = c.base % nameBases.length));
function selectCultures(c) { function selectCultures(c) {
let def = getDefault(c); let def = getDefault(c);
@ -87,11 +100,11 @@
const count = Math.min(c, def.length); const count = Math.min(c, def.length);
const cultures = []; 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 { do {
rnd = rand(def.length-1); rnd = rand(def.length - 1);
culture = def[rnd]; culture = def[rnd];
} while (!P(culture.odd)) } while (!P(culture.odd));
cultures.push(culture); cultures.push(culture);
def.splice(rnd, 1); def.splice(rnd, 1);
} }
@ -100,31 +113,31 @@
// set culture type based on culture center position // set culture type based on culture center position
function defineCultureType(i) { 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 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 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 (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 ((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.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"; return "Generic";
} }
function defineCultureExpansionism(type) { function defineCultureExpansionism(type) {
let base = 1; // Generic let base = 1; // Generic
if (type === "Lake") base = .8; else if (type === "Lake") base = 0.8;
if (type === "Naval") base = 1.5; else else if (type === "Naval") base = 1.5;
if (type === "River") base = .9; else else if (type === "River") base = 0.9;
if (type === "Nomadic") base = 1.5; else else if (type === "Nomadic") base = 1.5;
if (type === "Hunting") base = .7; else else if (type === "Hunting") base = 0.7;
if (type === "Highland") base = 1.2; else if (type === "Highland") base = 1.2;
return rn((Math.random() * powerInput.value / 2 + 1) * base, 1); 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(); const defaultCultures = getDefault();
let culture, base, name; let culture, base, name;
@ -139,7 +152,10 @@
name = Names.getCulture(culture, 5, 8, ""); name = Names.getCulture(culture, 5, 8, "");
base = pack.cultures[culture].base; 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 i = pack.cultures.length;
const color = d3.color(d3.scaleSequential(d3.interpolateRainbow)(Math.random())).hex(); const color = d3.color(d3.scaleSequential(d3.interpolateRainbow)(Math.random())).hex();
@ -148,220 +164,231 @@
const emblemShape = document.getElementById("emblemShape").value; const emblemShape = document.getElementById("emblemShape").value;
if (emblemShape === "random") shield = getRandomShield(); 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 // 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 cells = pack.cells,
const n = cell => Math.ceil(s[cell] / sMax * 3) // normalized cell score s = cells.s,
const td = (cell, goal) => {const d = Math.abs(temp[cells.g[cell]] - goal); return d ? d+1 : 1;} // temperature difference fee sMax = d3.max(s),
const bd = (cell, biomes, fee = 4) => biomes.includes(cells.biome[cell]) ? 1 : fee; // biome difference fee t = cells.t,
const sf = (cell, fee = 4) => cells.haven[cell] && pack.features[cells.f[cells.haven[cell]]].type !== "lake" ? 1 : fee; // not on sea coast fee 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") { if (culturesSet.value === "european") {
return [ return [
{name:"Shwazen", base:0, odd:1, sort: i => n(i) / td(i, 10) / bd(i, [6, 8]), shield:"swiss"}, {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: "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: "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: "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: "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: "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: "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: "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: "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: "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: "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: "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: "Turchian", base: 16, odd: 0.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: "Euskati", base: 20, odd: 0.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: "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") { if (culturesSet.value === "oriental") {
return [ return [
{name:"Koryo", base:10, odd:1, sort: i => n(i) / td(i, 12) / t[i], shield:"round"}, {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: "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: "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: "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: "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: "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: "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: "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: "Maui", base: 25, odd: 0.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: "Carnatic", base: 26, odd: 0.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: "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:.5, sort: i => n(i) / td(i, 17), 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"} {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") { if (culturesSet.value === "english") {
const getName = () => Names.getBase(1, 5, 9, "", 0); const getName = () => Names.getBase(1, 5, 9, "", 0);
return [ return [
{name:getName(), base:1, odd:1, shield:"heater"}, {name: getName(), base: 1, odd: 1, shield: "heater"},
{name:getName(), base:1, odd:1, shield:"wedged"}, {name: getName(), base: 1, odd: 1, shield: "wedged"},
{name:getName(), base:1, odd:1, shield:"swiss"}, {name: getName(), base: 1, odd: 1, shield: "swiss"},
{name:getName(), base:1, odd:1, shield:"oldFrench"}, {name: getName(), base: 1, odd: 1, shield: "oldFrench"},
{name:getName(), base:1, odd:1, shield:"swiss"}, {name: getName(), base: 1, odd: 1, shield: "swiss"},
{name:getName(), base:1, odd:1, shield:"spanish"}, {name: getName(), base: 1, odd: 1, shield: "spanish"},
{name:getName(), base:1, odd:1, shield:"hessen"}, {name: getName(), base: 1, odd: 1, shield: "hessen"},
{name:getName(), base:1, odd:1, shield:"fantasy5"}, {name: getName(), base: 1, odd: 1, shield: "fantasy5"},
{name:getName(), base:1, odd:1, shield:"fantasy4"}, {name: getName(), base: 1, odd: 1, shield: "fantasy4"},
{name:getName(), base:1, odd:1, shield:"fantasy1"} {name: getName(), base: 1, odd: 1, shield: "fantasy1"}
]; ];
} }
if (culturesSet.value === "antique") { if (culturesSet.value === "antique") {
return [ 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, 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, 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, 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: "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, 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: "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: "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) ** .5 / bd(i, [6, 8]), shield:"round"}, {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) ** .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:.8, sort: i => n(i) / td(i, 18) * h[i], shield:"oval"}, // Iranian {name: "Persian", base: 24, odd: 0.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: "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:.5, sort: i => n(i) / td(i, 16) * h[i], shield:"oval"}, // Basque {name: "Cantabrian", base: 20, odd: 0.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: "Estian", base: 9, odd: 0.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: "Carthaginian", base: 17, odd: 0.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: "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") { if (culturesSet.value === "highFantasy") {
return [ return [
// fantasy races // 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: "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: "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: "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:.3, sort: i => n(i) / bd(i, [7,8,9,12], 10) * t[i], shield:"wedged"}, // 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: "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: "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: "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: "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: "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:.7, sort: i => td(i, -10), shield:"pavise"}, // Giant {name: "Yotunn (Giants)", base: 38, odd: 0.7, sort: i => td(i, -10), shield: "pavise"}, // Giant
{name:"Rake (Drakonic)", base: 39, odd:.7, sort: i => -s[i], shield:"fantasy2"}, // Draconic {name: "Rake (Drakonic)", base: 39, odd: 0.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: "Arago (Arachnid)", base: 40, odd: 0.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: "Aj'Snaga (Serpents)", base: 41, odd: 0.7, sort: i => n(i) / bd(i, [12], 10), shield: "fantasy1"}, // Serpents
// fantasy human // fantasy human
{name:"Anor (Human)", base:32, odd:1, sort: i => n(i) / td(i, 10), shield:"fantasy5"}, {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: "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: "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: "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") { if (culturesSet.value === "darkFantasy") {
return [ return [
// common real-world English // common real-world English
{name:"Angshire", base:1, odd:1, sort: i => n(i) / td(i, 10) / sf(i), 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: "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: "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: "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: "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: "Kentian", base: 1, odd: 1, sort: i => n(i) / td(i, 12), shield: "heater"},
// rare real-world western // rare real-world western
{name:"Norse", base:6, odd:.7, sort: i => n(i) / td(i, 5) / sf(i), shield:"oldFrench"}, {name: "Norse", base: 6, odd: 0.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: "Schwarzen", base: 0, odd: 0.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: "Luarian", base: 2, odd: 0.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: "Hetallian", base: 3, odd: 0.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: "Astellian", base: 4, odd: 0.3, sort: i => n(i) / td(i, 16), shield: "spanish"},
// rare real-world exotic // 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: "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:.05, sort: i => n(i) / td(i, 15) / bd(i, [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:.05, sort: i => n(i) / td(i, 12) / t[i], shield:"round"}, {name: "Koryo", base: 10, odd: 0.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: "Hantzu", base: 11, odd: 0.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: "Yamoto", base: 12, odd: 0.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: "Guantzu", base: 30, odd: 0.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: "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:.05, sort: i => n(i) / td(i, 12), shield:"round"}, {name: "Turan", base: 16, odd: 0.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: "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:.05, sort: i => n(i) / td(i, 26) / bd(i, [1, 2], 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:.05, sort: i => n(i) / td(i, 6) * 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:.1, sort: i => n(i) / td(i, 11) ** .5 / bd(i, [6, 8]), shield:"vesicaPiscis"}, {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:.2, sort: i => n(i) / td(i, 18) / sf(i) * h[i], shield:"boeotian"}, {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:.2, sort: i => n(i) / td(i, 14) / t[i], shield:"roman"}, {name: "Romian", base: 8, odd: 0.2, sort: i => n(i) / td(i, 14) / t[i], shield: "roman"},
// fantasy races // 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: "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:.8, sort: i => n(i) / bd(i, [7,8,9,12], 10) * t[i], shield:"hessen"}, // Dark 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:.8, sort: i => n(i) + h[i], shield:"erebor"}, // Dwarven {name: "Durinn", base: 35, odd: 0.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: "Kobblin", base: 36, odd: 0.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: "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:.8, sort: i => td(i, -10), shield:"pavise"}, // Giant {name: "Yotunn", base: 38, odd: 0.8, sort: i => td(i, -10), shield: "pavise"}, // Giant
{name:"Drake", base: 39, odd:.9, sort: i => -s[i], shield:"fantasy2"}, // Draconic {name: "Drake", base: 39, odd: 0.9, sort: i => -s[i], shield: "fantasy2"}, // Draconic
{name:"Rakhnid", base: 40, odd:.9, sort: i => t[i] - s[i], shield:"horsehead2"}, // Arachnid {name: "Rakhnid", base: 40, odd: 0.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: "Aj'Snaga", base: 41, odd: 0.9, sort: i => n(i) / bd(i, [12], 10), shield: "fantasy1"} // Serpents
] ];
} }
if (culturesSet.value === "random") { if (culturesSet.value === "random") {
return d3.range(count).map(function() { return d3.range(count).map(function () {
const rnd = rand(nameBases.length-1); const rnd = rand(nameBases.length - 1);
const name = Names.getBaseShort(rnd); const name = Names.getBaseShort(rnd);
return {name, base:rnd, odd:1, shield:getRandomShield()} return {name, base: rnd, odd: 1, shield: getRandomShield()};
}); });
} }
// all-world // all-world
return [ return [
{name:"Shwazen", base:0, odd:.7, sort: i => n(i) / td(i, 10) / bd(i, [6, 8]), shield:"hessen"}, {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: "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: "Luari", base: 2, odd: 0.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: "Tallian", base: 3, odd: 0.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: "Astellian", base: 4, odd: 0.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: "Slovan", base: 5, odd: 0.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: "Norse", base: 6, odd: 0.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: "Elladan", base: 7, odd: 0.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: "Romian", base: 8, odd: 0.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: "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:.1, sort: i => n(i) / td(i, 12) / t[i], shield:"round"}, {name: "Koryo", base: 10, odd: 0.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: "Hantzu", base: 11, odd: 0.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: "Yamoto", base: 12, odd: 0.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: "Portuzian", base: 13, odd: 0.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: "Nawatli", base: 14, odd: 0.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: "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:.2, sort: i => n(i) / td(i, 13), shield:"round"}, {name: "Turchian", base: 16, odd: 0.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: "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:.2, sort: i => n(i) / td(i, 26) / bd(i, [1, 2], 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:.05, sort: i => td(i, -1) / bd(i, [10, 11]) / sf(i), shield:"square"}, {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:.05, sort: i => n(i) / td(i, 15) * h[i], shield:"spanish"}, {name: "Euskati", base: 20, odd: 0.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: "Yoruba", base: 21, odd: 0.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: "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:.05, sort: i => n(i) / td(i, 22) * t[i], shield:"diamond"}, {name: "Efratic", base: 23, odd: 0.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: "Tehrani", base: 24, odd: 0.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: "Maui", base: 25, odd: 0.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: "Carnatic", base: 26, odd: 0.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: "Inqan", base: 27, odd: 0.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: "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:.1, sort: i => n(i) / td(i, 25) / bd(i, [7], 7) / t[i], shield:"banner"}, {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:.1, sort: i => n(i) / td(i, 17), shield:"banner"}, {name: "Guantzu", base: 30, odd: 0.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: "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) // expand cultures across the map (Dijkstra-like algorithm)
const expand = function() { const expand = function () {
TIME && console.time('expandCultures'); TIME && console.time("expandCultures");
cells = pack.cells; cells = pack.cells;
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
pack.cultures.forEach(function(c) { pack.cultures.forEach(function (c) {
if (!c.i || c.removed) return; 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 = []; const cost = [];
while (queue.length) { 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; const type = pack.cultures[c].type;
cells.c[n].forEach(function(e) { cells.c[n].forEach(function (e) {
const biome = cells.biome[e]; const biome = cells.biome[e];
const biomeCost = getBiomeCost(c, biome, type); const biomeCost = getBiomeCost(c, biome, type);
const biomeChangeCost = biome === cells.biome[n] ? 0 : 20; // penalty on biome change const biomeChangeCost = biome === cells.biome[n] ? 0 : 20; // penalty on biome change
@ -375,13 +402,13 @@
if (!cost[e] || totalCost < cost[e]) { if (!cost[e] || totalCost < cost[e]) {
if (cells.s[e] > 0) cells.culture[e] = c; // assign culture to populated cell if (cells.s[e] > 0) cells.culture[e] = c; // assign culture to populated cell
cost[e] = totalCost; 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) { function getBiomeCost(c, biome, type) {
if (cells.biome[pack.cultures[c].center] === biome) return 10; // tiny penalty for native biome if (cells.biome[pack.cultures[c].center] === biome) return 10; // tiny penalty for native biome
@ -391,7 +418,8 @@
} }
function getHeightCost(i, h, type) { 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 === "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 === "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 if (type === "Nomadic" && h < 20) return a * 50; // giant sea/lake crossing penalty for Nomads
@ -407,7 +435,7 @@
function getRiverCost(r, i, type) { function getRiverCost(r, i, type) {
if (type === "River") return r ? 0 : 100; // penalty for river cultures if (type === "River") return r ? 0 : 100; // penalty for river cultures
if (!r) return 0; // no penalty for others if there is no river 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) { function getTypeCost(t, type) {
@ -417,11 +445,10 @@
return 0; return 0;
} }
const getRandomShield = function() { const getRandomShield = function () {
const type = rw(COA.shields.types); const type = rw(COA.shields.types);
return rw(COA.shields[type]); return rw(COA.shields[type]);
} };
return {generate, add, expand, getDefault, getRandomShield}; 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) { "use strict";
typeof exports === "object" && typeof module !== "undefined" ? (module.exports = factory()) : typeof define === "function" && define.amd ? define(factory) : (global.HeightmapGenerator = factory());
})(this, function () {
"use strict";
window.HeightmapGenerator = (function () {
let cells, p; let cells, p;
const generate = function () { const generate = function () {
@ -12,262 +10,63 @@
cells.h = new Uint8Array(grid.points.length); cells.h = new Uint8Array(grid.points.length);
const template = document.getElementById("templateInput").value; const template = document.getElementById("templateInput").value;
switch (template) { const templateString = HeightmapTemplates[template];
case "Volcano": const steps = templateString.split("\n");
templateVolcano();
break; if (!steps.length) throw new Error(`Heightmap template: no steps. Template: ${template}. Steps: ${steps}`);
case "High Island":
templateHighIsland(); for (const step of steps) {
break; const elements = step.trim().split(" ");
case "Low Island": if (elements.length < 2) throw new Error(`Heightmap template: steps < 2. Template: ${template}. Step: ${elements}`);
templateLowIsland(); addStep(...elements);
break;
case "Continents":
templateContinents();
break;
case "Archipelago":
templateArchipelago();
break;
case "Atoll":
templateAtoll();
break;
case "Mediterranean":
templateMediterranean();
break;
case "Peninsula":
templatePeninsula();
break;
case "Pangea":
templatePangea();
break;
case "Isthmus":
templateIsthmus();
break;
case "Shattered":
templateShattered();
break;
} }
TIME && console.timeEnd("generateHeightmap"); TIME && console.timeEnd("generateHeightmap");
}; };
// parse template step
function addStep(a1, a2, a3, a4, a5) { function addStep(a1, a2, a3, a4, a5) {
if (a1 === "Hill") return addHill(a2, a3, a4, a5); if (a1 === "Hill") return addHill(a2, a3, a4, a5);
if (a1 === "Pit") return addPit(a2, a3, a4, a5); if (a1 === "Pit") return addPit(a2, a3, a4, a5);
if (a1 === "Range") return addRange(a2, a3, a4, a5); if (a1 === "Range") return addRange(a2, a3, a4, a5);
if (a1 === "Trough") return addTrough(a2, a3, a4, a5); if (a1 === "Trough") return addTrough(a2, a3, a4, a5);
if (a1 === "Strait") return addStrait(a2, a3); if (a1 === "Strait") return addStrait(a2, a3);
if (a1 === "Add") return modify(a3, a2, 1); if (a1 === "Add") return modify(a3, +a2, 1);
if (a1 === "Multiply") return modify(a3, 0, a2); if (a1 === "Multiply") return modify(a3, 0, +a2);
if (a1 === "Smooth") return smooth(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() { function getBlobPower() {
switch (+pointsInput.dataset.cells) { const cells = +pointsInput.dataset.cells;
case 1000: if (cells === 1000) return 0.93;
return 0.93; if (cells === 2000) return 0.95;
case 2000: if (cells === 5000) return 0.96;
return 0.95; if (cells === 10000) return 0.98;
case 5000: if (cells === 20000) return 0.985;
return 0.96; if (cells === 30000) return 0.987;
case 10000: if (cells === 40000) return 0.9892;
return 0.98; if (cells === 50000) return 0.9911;
case 20000: if (cells === 60000) return 0.9921;
return 0.985; if (cells === 70000) return 0.9934;
case 30000: if (cells === 80000) return 0.9942;
return 0.987; if (cells === 90000) return 0.9946;
case 40000: if (cells === 100000) return 0.995;
return 0.9892;
case 50000:
return 0.9911;
case 60000:
return 0.9921;
case 70000:
return 0.9934;
case 80000:
return 0.9942;
case 90000:
return 0.9946;
case 100000:
return 0.995;
}
} }
function getLinePower() { function getLinePower() {
switch (+pointsInput.dataset.cells) { const cells = +pointsInput.dataset.cells;
case 1000: if (cells === 1000) return 0.74;
return 0.74; if (cells === 2000) return 0.75;
case 2000: if (cells === 5000) return 0.78;
return 0.75; if (cells === 10000) return 0.81;
case 5000: if (cells === 20000) return 0.82;
return 0.78; if (cells === 30000) return 0.83;
case 10000: if (cells === 40000) return 0.84;
return 0.81; if (cells === 50000) return 0.855;
case 20000: if (cells === 60000) return 0.87;
return 0.82; if (cells === 70000) return 0.885;
case 30000: if (cells === 80000) return 0.91;
return 0.83; if (cells === 90000) return 0.92;
case 40000: if (cells === 100000) return 0.93;
return 0.84;
case 50000:
return 0.855;
case 60000:
return 0.87;
case 70000:
return 0.885;
case 80000:
return 0.91;
case 90000:
return 0.92;
case 100000:
return 0.93;
}
} }
const addHill = function (count, height, rangeX, rangeY) { const addHill = function (count, height, rangeX, rangeY) {
@ -422,7 +221,6 @@
if (d % 6 !== 0) return; if (d % 6 !== 0) return;
for (const l of d3.range(i)) { 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 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; cells.h[min] = (cells.h[cur] * 2 + cells.h[min]) / 3;
cur = min; cur = min;
} }
@ -611,4 +409,4 @@
} }
return {generate, addHill, addRange, addTrough, addStrait, addPit, smooth, modify}; 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) { "use strict";
typeof exports === "object" && typeof module !== "undefined" ? (module.exports = factory()) : typeof define === "function" && define.amd ? define(factory) : (global.Lakes = factory());
})(this, function () {
"use strict";
window.Lakes = (function () {
const setClimateData = function (h) { const setClimateData = function (h) {
const cells = pack.cells; const cells = pack.cells;
const lakeOutCells = new Uint16Array(cells.i.length); const lakeOutCells = new Uint16Array(cells.i.length);
@ -10,8 +8,8 @@
pack.features.forEach(f => { pack.features.forEach(f => {
if (f.type !== "lake") return; if (f.type !== "lake") return;
// default flux: sum of precipition around lake first cell // default flux: sum of precipitation around lake
f.flux = rn(d3.sum(f.shoreline.map(c => grid.cells.prec[cells.g[c]])) / 2); f.flux = f.shoreline.reduce((acc, c) => acc + grid.cells.prec[cells.g[c]], 0);
// temperature and evaporation to detect closed lakes // temperature and evaporation to detect closed lakes
f.temp = f.cells < 6 ? grid.cells.temp[cells.g[f.firstCell]] : rn(d3.mean(f.shoreline.map(c => grid.cells.temp[cells.g[c]])), 1); f.temp = f.cells < 6 ? grid.cells.temp[cells.g[f.firstCell]] : rn(d3.mean(f.shoreline.map(c => grid.cells.temp[cells.g[c]])), 1);
@ -96,7 +94,6 @@
if (feature.type !== "lake") continue; if (feature.type !== "lake") continue;
delete feature.river; delete feature.river;
delete feature.enteringFlux; delete feature.enteringFlux;
delete feature.shoreline;
delete feature.outCell; delete feature.outCell;
delete feature.closed; delete feature.closed;
feature.height = rn(feature.height, 3); 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.height > 60 && feature.cells < 10 && feature.firstCell % 10 === 0) return "lava";
if (!feature.inlets && !feature.outlet) { if (!feature.inlets && !feature.outlet) {
if (feature.evaporation / 2 > feature.flux) return "dry"; if (feature.evaporation > feature.flux * 4) return "dry";
if (feature.cells < 3 && feature.firstCell % 10 === 0) return "sinkhole"; if (feature.cells < 3 && feature.firstCell % 10 === 0) return "sinkhole";
} }
@ -150,4 +147,4 @@
} }
return {setClimateData, cleanupLakeData, prepareLakeData, defineGroup, generateName, getName, getShoreline}; 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[19]) options = JSON.parse(settings[19]);
if (settings[20]) mapName.value = settings[20]; if (settings[20]) mapName.value = settings[20];
if (settings[21]) hideLabels.checked = +settings[21]; if (settings[21]) hideLabels.checked = +settings[21];
if (settings[22]) stylePreset.value = settings[22];
if (settings[23]) rescaleLabels.checked = settings[23];
})(); })();
void (function parseConfiguration() { void (function parseConfiguration() {
@ -220,6 +222,8 @@ function parseLoadedData(data) {
burgLabels = labels.select('#burgLabels'); burgLabels = labels.select('#burgLabels');
})(); })();
loadUsedFonts();
void (function parseGridData() { void (function parseGridData() {
grid = JSON.parse(data[6]); grid = JSON.parse(data[6]);
calculateVoronoi(grid, grid.points); calculateVoronoi(grid, grid.points);
@ -270,12 +274,13 @@ function parseLoadedData(data) {
} }
})(); })();
void (function restoreLayersState() {
// helper functions
const notHidden = (selection) => selection.node() && selection.style('display') !== 'none'; const notHidden = (selection) => selection.node() && selection.style('display') !== 'none';
const hasChildren = (selection) => selection.node()?.hasChildNodes(); const hasChildren = (selection) => selection.node()?.hasChildNodes();
const hasChild = (selection, selector) => selection.node()?.querySelector(selector); const hasChild = (selection, selector) => selection.node()?.querySelector(selector);
const turnOn = (el) => document.getElementById(el).classList.remove('buttonoff'); const turnOn = (el) => document.getElementById(el).classList.remove('buttonoff');
void (function restoreLayersState() {
// turn all layers off // turn all layers off
document document
.getElementById('mapLayers') .getElementById('mapLayers')
@ -290,7 +295,7 @@ function parseLoadedData(data) {
if (hasChildren(gridOverlay)) turnOn('toggleGrid'); if (hasChildren(gridOverlay)) turnOn('toggleGrid');
if (hasChildren(coordinates)) turnOn('toggleCoordinates'); if (hasChildren(coordinates)) turnOn('toggleCoordinates');
if (notHidden(compass) && hasChild(compass, 'use')) turnOn('toggleCompass'); 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 (notHidden(terrain) && hasChildren(terrain)) turnOn('toggleRelief');
if (hasChildren(relig)) turnOn('toggleReligions'); if (hasChildren(relig)) turnOn('toggleReligions');
if (hasChildren(cults)) turnOn('toggleCultures'); if (hasChildren(cults)) turnOn('toggleCultures');
@ -303,7 +308,6 @@ function parseLoadedData(data) {
if (hasChild(population, 'line')) turnOn('togglePopulation'); if (hasChild(population, 'line')) turnOn('togglePopulation');
if (hasChildren(ice)) turnOn('toggleIce'); if (hasChildren(ice)) turnOn('toggleIce');
if (hasChild(prec, 'circle')) turnOn('togglePrec'); if (hasChild(prec, 'circle')) turnOn('togglePrec');
if (hasChildren(goods)) turnOn('toggleResources');
if (notHidden(emblems) && hasChild(emblems, 'use')) turnOn('toggleEmblems'); if (notHidden(emblems) && hasChild(emblems, 'use')) turnOn('toggleEmblems');
if (notHidden(labels)) turnOn('toggleLabels'); if (notHidden(labels)) turnOn('toggleLabels');
if (notHidden(icons)) turnOn('toggleIcons'); if (notHidden(icons)) turnOn('toggleIcons');
@ -694,7 +698,7 @@ function parseLoadedData(data) {
} }
if (version < 1.63) { 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'); const oceanPattern = document.getElementById('oceanPattern');
if (oceanPattern) oceanPattern.removeAttribute('opacity'); if (oceanPattern) oceanPattern.removeAttribute('opacity');
const oceanicPattern = document.getElementById('oceanicPattern'); const oceanicPattern = document.getElementById('oceanicPattern');
@ -713,6 +717,50 @@ function parseLoadedData(data) {
defs.append('g').attr('id', 'defs-icons'); defs.append('g').attr('id', 'defs-icons');
Resources.generate(); 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() { void (function checkDataIntegrity() {
@ -812,6 +860,7 @@ function parseLoadedData(data) {
// set options // set options
yearInput.value = options.year; yearInput.value = options.year;
eraInput.value = options.era; eraInput.value = options.era;
shapeRendering.value = viewbox.attr('shape-rendering') || 'geometricPrecision';
if (window.restoreDefaultEvents) restoreDefaultEvents(); if (window.restoreDefaultEvents) restoreDefaultEvents();
focusOn(); // based on searchParams focus on point, cell or burg focusOn(); // based on searchParams focus on point, cell or burg

View file

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

View file

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

View file

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

View file

@ -1,41 +1,41 @@
(function (global, factory) { "use strict";
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.ReliefIcons = factory());
}(this, (function () {'use strict';
const ReliefIcons = function() { window.ReliefIcons = (function () {
TIME && console.time('drawRelief'); const ReliefIcons = function () {
TIME && console.time("drawRelief");
terrain.selectAll("*").remove(); 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 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) { for (const i of cells.i) {
const height = cells.h[i]; const height = cells.h[i];
if (height < 20) continue; // no icons on water if (height < 20) continue; // no icons on water
if (cells.r[i]) continue; // no icons on rivers if (cells.r[i]) continue; // no icons on rivers
const b = cells.biome[i]; const biome = cells.biome[i];
if (height < 50 && biomesData.iconsDensity[b] === 0) continue; // no icons for this biome if (height < 50 && biomesData.iconsDensity[biome] === 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
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() { function placeBiomeIcons() {
const iconsDensity = biomesData.iconsDensity[b] / 100; const iconsDensity = biomesData.iconsDensity[biome] / 100;
const radius = 2 / iconsDensity / density; const radius = 2 / iconsDensity / density;
if (Math.random() > iconsDensity * 10) return; 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; if (!d3.polygonContains(polygon, [cx, cy])) continue;
let h = rn((4 + Math.random()) * size, 2); let h = (4 + Math.random()) * size;
const icon = getBiomeIcon(i, biomesData.icons[b]); const icon = getBiomeIcon(i, biomesData.icons[biome]);
if (icon === "#relief-grass-1") h *= 1.3; 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)}); 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 radius = 2 / density;
const [icon, h] = getReliefIcon(i, height); 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; 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 // 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 = ""; let reliefHTML = "";
for (const r of relief) { for (const r of relief) {
reliefHTML += `<use href="${r.i}" x="${r.x}" y="${r.y}" width="${r.s}" height="${r.s}"/>`; reliefHTML += `<use href="${r.i}" x="${r.x}" y="${r.y}" width="${r.s}" height="${r.s}"/>`;
} }
terrain.html(reliefHTML); terrain.html(reliefHTML);
TIME && console.timeEnd('drawRelief'); TIME && console.timeEnd("drawRelief");
} };
function getBiomeIcon(i, b) { function getBiomeIcon(i, b) {
let type = b[Math.floor(Math.random() * b.length)]; let type = b[Math.floor(Math.random() * b.length)];
@ -78,27 +77,42 @@
} }
function getVariant(type) { function getVariant(type) {
switch(type) { switch (type) {
case "mount": return rand(2,7); case "mount":
case "mountSnow": return rand(1,6); return rand(2, 7);
case "hill": return rand(2,5); case "mountSnow":
case "conifer": return 2; return rand(1, 6);
case "coniferSnow": return 1; case "hill":
case "swamp": return rand(2,3); return rand(2, 5);
case "cactus": return rand(1,3); case "conifer":
case "deadTree": return rand(1,2); return 2;
default: 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) { function getOldIcon(type) {
switch(type) { switch (type) {
case "mountSnow": return "mount"; case "mountSnow":
case "vulcan": return "mount"; return "mount";
case "coniferSnow": return "conifer"; case "vulcan":
case "cactus": return "dune"; return "mount";
case "deadTree": return "dune"; case "coniferSnow":
default: return type; return "conifer";
case "cactus":
return "dune";
case "deadTree":
return "dune";
default:
return type;
} }
} }
@ -111,5 +125,4 @@
} }
return ReliefIcons; return ReliefIcons;
})();
})));

View file

@ -1,19 +1,13 @@
(function (global, factory) { "use strict";
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.Religions = factory());
}(this, (function () {'use strict';
window.Religions = (function () {
// name generation approach and relative chance to be selected // name generation approach and relative chance to be selected
const approach = {"Number":1, "Being":3, "Adjective":5, "Color + Animal":5, 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};
"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 // turn weighted array into simple array
const approaches = []; const approaches = [];
for (const a in approach) { for (const a in approach) {
for (let j=0; j < approach[a]; j++) { for (let j = 0; j < approach[a]; j++) {
approaches.push(a); approaches.push(a);
} }
} }
@ -29,64 +23,70 @@
}; };
const forms = { const forms = {
Folk:{"Shamanism":2, "Animism":2, "Ancestor worship":1, "Polytheism":2}, Folk: {Shamanism: 2, Animism: 2, "Ancestor worship": 1, Polytheism: 2},
Organized:{"Polytheism":5, "Dualism":1, "Monotheism":4, "Non-theism":1}, Organized: {Polytheism: 5, Dualism: 1, Monotheism: 4, "Non-theism": 1},
Cult:{"Cult":1, "Dark Cult":1}, Cult: {Cult: 1, "Dark Cult": 1},
Heresy:{"Heresy":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 = { const types = {
"Shamanism":{"Beliefs":3, "Shamanism":2, "Spirits":1}, Shamanism: {Beliefs: 3, Shamanism: 2, Spirits: 1},
"Animism":{"Spirits":1, "Beliefs":1}, Animism: {Spirits: 1, Beliefs: 1},
"Ancestor worship":{"Beliefs":1, "Forefathers":2, "Ancestors":2}, "Ancestor worship": {Beliefs: 1, Forefathers: 2, Ancestors: 2},
"Polytheism":{"Deities":3, "Faith":1, "Gods":1, "Pantheon":1}, Polytheism: {Deities: 3, Faith: 1, Gods: 1, Pantheon: 1},
"Dualism":{"Religion":3, "Faith":1, "Cult":1}, Dualism: {Religion: 3, Faith: 1, Cult: 1},
"Monotheism":{"Religion":1, "Church":1}, Monotheism: {Religion: 1, Church: 1},
"Non-theism":{"Beliefs":3, "Spirits":1}, "Non-theism": {Beliefs: 3, Spirits: 1},
"Cult":{"Cult":4, "Sect":4, "Worship":1, "Orden":1, "Coterie":1, "Arcanum":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}, "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() { const generate = function () {
TIME && console.time('generateReligions'); TIME && console.time("generateReligions");
const cells = pack.cells, states = pack.states, cultures = pack.cultures; const cells = pack.cells,
const religions = pack.religions = []; states = pack.states,
cultures = pack.cultures;
const religions = (pack.religions = []);
cells.religion = new Uint16Array(cells.culture); // cell religion; initially based on culture cells.religion = new Uint16Array(cells.culture); // cell religion; initially based on culture
// add folk religions // add folk religions
pack.cultures.forEach(c => { pack.cultures.forEach(c => {
if (!c.i) {religions.push({i: 0, name: "No religion"}); return;} if (!c.i) {
if (c.removed) {religions.push({i: c.i, name: "Extinct religion for "+c.name, color:getMixedColor(c.color, .1, 0), removed:true}); return;} 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 form = rw(forms.Folk);
const name = c.name + " " + rw(types[form]); const name = c.name + " " + rw(types[form]);
const deity = form === "Animism" ? null : getDeityName(c.i); const deity = form === "Animism" ? null : getDeityName(c.i);
const color = getMixedColor(c.color, .1, 0); // `url(#hatch${rand(8,13)})`; 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}); 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) { 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; return;
} }
const burgs = pack.burgs.filter(b => b.i && !b.removed); const burgs = pack.burgs.filter(b => b.i && !b.removed);
const sorted = burgs.length > +religionsInput.value 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]);
? 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 religionsTree = d3.quadtree();
const spacing = (graphWidth + graphHeight) / 6 / religionsInput.value; // base min distance between towns 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; const count = +religionsInput.value - cultsCount + religions.length;
// generate organized religions // generate organized religions
for (let i=0; religions.length < count && i < 1000; i++) { for (let i = 0; religions.length < count && i < 1000; i++) {
let center = sorted[biased(0, sorted.length-1, 5)]; // religion center let center = sorted[biased(0, sorted.length - 1, 5)]; // religion center
const form = rw(forms.Organized); const form = rw(forms.Organized);
const state = cells.state[center]; const state = cells.state[center];
const culture = cells.culture[center]; const culture = cells.culture[center];
@ -96,34 +96,36 @@
if (expansion === "state" && !state) expansion = "global"; if (expansion === "state" && !state) expansion = "global";
if (expansion === "culture" && !culture) expansion = "global"; if (expansion === "culture" && !culture) expansion = "global";
if (expansion === "state" && Math.random() > .5) center = states[state].center; if (expansion === "state" && Math.random() > 0.5) center = states[state].center;
if (expansion === "culture" && Math.random() > .5) center = cultures[culture].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]); 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 if (religionsTree.find(x, y, s) !== undefined) continue; // to close to existing religion
// add "Old" to name of the folk religion on this culture // add "Old" to name of the folk religion on this culture
const folk = religions.find(r => r.culture === culture && r.type === "Folk"); 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 origin = folk ? folk.i : 0;
const expansionism = rand(3, 8); const expansionism = rand(3, 8);
const color = getMixedColor(religions[origin].color, .3, 0); // `url(#hatch${rand(0,5)})`; 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}); religions.push({i: religions.length, name, color, culture, type: "Organized", form, deity, expansion, expansionism, center, origin});
religionsTree.add([x, y]); religionsTree.add([x, y]);
} }
// generate cults // 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); 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]); 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 if (religionsTree.find(x, y, s) !== undefined) continue; // to close to existing religion
const culture = cells.culture[center]; const culture = cells.culture[center];
@ -131,9 +133,9 @@
const origin = folk ? folk.i : 0; const origin = folk ? folk.i : 0;
const deity = getDeityName(culture); const deity = getDeityName(culture);
const name = getCultName(form, center); const name = getCultName(form, center);
const expansionism = gauss(1.1, .5, 0, 5); const expansionism = gauss(1.1, 0.5, 0, 5);
const color = getMixedColor(cultures[culture].color, .5, 0); // "url(#hatch7)"; 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}); religions.push({i: religions.length, name, color, culture, type: "Cult", form, deity, expansion: "global", expansionism, center, origin});
religionsTree.add([x, y]); religionsTree.add([x, y]);
//debug.append("circle").attr("cx", x).attr("cy", y).attr("r", 2).attr("fill", "red"); //debug.append("circle").attr("cx", x).attr("cy", y).attr("r", 2).attr("fill", "red");
} }
@ -141,21 +143,24 @@
expandReligions(); expandReligions();
// generate heresies // generate heresies
religions.filter(r => r.type === "Organized").forEach(r => { religions
.filter(r => r.type === "Organized")
.forEach(r => {
if (r.expansionism < 3) return; if (r.expansionism < 3) return;
const count = gauss(0, 1, 0, 3); const count = gauss(0, 1, 0, 3);
for (let i=0; i < count; i++) { 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))); 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 (!center) continue;
if (!cells.burg[center] && cells.c[center].some(c => cells.burg[c])) center = cells.c[center].find(c => cells.burg[c]); 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];
if (religionsTree.find(x, y, spacing / 10) !== undefined) continue; // to close to other if (religionsTree.find(x, y, spacing / 10) !== undefined) continue; // to close to other
const culture = cells.culture[center]; const culture = cells.culture[center];
const name = getCultName("Heresy", center); const name = getCultName("Heresy", center);
const expansionism = gauss(1.2, .5, 0, 5); const expansionism = gauss(1.2, 0.5, 0, 5);
const color = getMixedColor(r.color, .4, .2); // "url(#hatch6)"; 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}); 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]); religionsTree.add([x, y]);
//debug.append("circle").attr("cx", x).attr("cy", y).attr("r", 2).attr("fill", "green"); //debug.append("circle").attr("cx", x).attr("cy", y).attr("r", 2).attr("fill", "green");
} }
@ -164,49 +169,64 @@
expandHeresies(); expandHeresies();
checkCenters(); checkCenters();
TIME && console.timeEnd('generateReligions'); TIME && console.timeEnd("generateReligions");
} };
const add = function(center) { const add = function (center) {
const cells = pack.cells, religions = pack.religions; const cells = pack.cells,
religions = pack.religions;
const r = cells.religion[center]; const r = cells.religion[center];
const i = religions.length; const i = religions.length;
const culture = cells.culture[center]; 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 form = rw(forms[type]);
const deity = type === "Heresy" ? religions[r].deity : form === "Non-theism" ? null : getDeityName(culture); const deity = type === "Heresy" ? religions[r].deity : form === "Non-theism" ? null : getDeityName(culture);
let name, expansion; let name, expansion;
if (type === "Organized") [name, expansion] = getReligionName(form, deity, center) if (type === "Organized") [name, expansion] = getReligionName(form, deity, center);
else {name = getCultName(form, center); expansion = "global";} else {
const formName = type === "Heresy" ? religions[r].form : form; name = getCultName(form, center);
const code = abbreviate(name, religions.map(r => r.code)); expansion = "global";
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;
} }
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});
cells.religion[center] = i;
};
// growth algorithm to assign cells to religions // growth algorithm to assign cells to religions
const expandReligions = function() { const expandReligions = function () {
const cells = pack.cells, religions = pack.religions; const cells = pack.cells,
religions = pack.religions;
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const cost = []; const cost = [];
religions.filter(r => r.type === "Organized" || r.type === "Cult").forEach(r => { religions
.filter(r => r.type === "Organized" || r.type === "Cult")
.forEach(r => {
cells.religion[r.center] = r.i; cells.religion[r.center] = r.i;
queue.queue({e:r.center, p:0, r:r.i, s: cells.state[r.center], c:r.culture}); queue.queue({e: r.center, p: 0, r: r.i, s: cells.state[r.center], c: r.culture});
cost[r.center] = 1; 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 const popCost = d3.max(cells.pop) / 3; // enougth population to spered religion without penalty
while (queue.length) { 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; 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 === "culture" && c !== cells.culture[e]) return;
if (expansion === "state" && s !== cells.state[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 biomeCost = cells.road[e] ? 1 : biomesData.cost[cells.biome[e]];
const populationCost = Math.max(rn(popCost - cells.pop[e]), 0); const populationCost = Math.max(rn(popCost - cells.pop[e]), 0);
const heightCost = Math.max(cells.h[e], 20) - 20; 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; const totalCost = p + (cultureCost + stateCost + biomeCost + populationCost + heightCost + waterCost) / religions[r].expansionism;
if (totalCost > neutral) return; if (totalCost > neutral) return;
if (!cost[e] || totalCost < cost[e]) { if (!cost[e] || totalCost < cost[e]) {
if (cells.h[e] >= 20 && cells.culture[e]) cells.religion[e] = r; // assign religion to cell if (cells.h[e] >= 20 && cells.culture[e]) cells.religion[e] = r; // assign religion to cell
cost[e] = totalCost; 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 // growth algorithm to assign cells to heresies
const expandHeresies = function() { const expandHeresies = function () {
const cells = pack.cells, religions = pack.religions; const cells = pack.cells,
religions = pack.religions;
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const cost = []; const cost = [];
religions.filter(r => r.type === "Heresy").forEach(r => { religions
.filter(r => r.type === "Heresy")
.forEach(r => {
const b = cells.religion[r.center]; // "base" religion id const b = cells.religion[r.center]; // "base" religion id
cells.religion[r.center] = r.i; // heresy id cells.religion[r.center] = r.i; // heresy id
queue.queue({e:r.center, p:0, r:r.i, b}); queue.queue({e: r.center, p: 0, r: r.i, b});
cost[r.center] = 1; 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) { 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 religionCost = cells.religion[e] === b ? 0 : 2000;
const biomeCost = cells.road[e] ? 0 : biomesData.cost[cells.biome[e]]; const biomeCost = cells.road[e] ? 0 : biomesData.cost[cells.biome[e]];
const heightCost = Math.max(cells.h[e], 20) - 20; 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 + (religionCost + biomeCost + heightCost + waterCost) / Math.max(religions[r].expansionism, .1); const totalCost = p + (religionCost + biomeCost + heightCost + waterCost) / Math.max(religions[r].expansionism, 0.1);
if (totalCost > neutral) return; if (totalCost > neutral) return;
if (!cost[e] || totalCost < cost[e]) { if (!cost[e] || totalCost < cost[e]) {
if (cells.h[e] >= 20 && cells.culture[e]) cells.religion[e] = r; // assign religion to cell if (cells.h[e] >= 20 && cells.culture[e]) cells.religion[e] = r; // assign religion to cell
cost[e] = totalCost; cost[e] = totalCost;
queue.queue({e, p:totalCost, r}); queue.queue({e, p: totalCost, r});
} }
}); });
} }
} };
function checkCenters() { function checkCenters() {
const cells = pack.cells, religions = pack.religions; const cells = pack.cells,
religions = pack.religions;
const codes = religions.map(r => r.code); const codes = religions.map(r => r.code);
religions.filter(r => r.i).forEach(r => { religions
.filter(r => r.i)
.forEach(r => {
r.code = abbreviate(r.name, codes); r.code = abbreviate(r.name, codes);
// move religion center if it's not within religion area after expansion // move religion center if it's not within religion area after expansion
if (cells.religion[r.center] === r.i) return; // in area if (cells.religion[r.center] === r.i) return; // in area
const religCells = cells.i.filter(i => cells.religion[i] === r.i); const religCells = cells.i.filter(i => cells.religion[i] === r.i);
if (!religCells.length) return; // extinct religion if (!religCells.length) return; // extinct religion
r.center = religCells.sort((a,b) => b.pop - a.pop)[0]; r.center = religCells.sort((a, b) => b.pop - a.pop)[0];
}); });
} }
function updateCultures() { function updateCultures() {
TIME && console.time('updateCulturesForReligions'); TIME && console.time("updateCulturesForReligions");
pack.religions = pack.religions.map( (religion, index) => { pack.religions = pack.religions.map((religion, index) => {
if(index === 0) { if (index === 0) {
return religion; return religion;
} }
return {...religion, culture: pack.cells.culture[religion.center]}; return {...religion, culture: pack.cells.culture[religion.center]};
}); });
TIME && console.timeEnd('updateCulturesForReligions'); TIME && console.timeEnd("updateCulturesForReligions");
} }
// get supreme deity name // get supreme deity name
const getDeityName = function(culture) { const getDeityName = function (culture) {
if (culture === undefined) {ERROR && console.error("Please define a culture"); return;} if (culture === undefined) {
const meaning = generateMeaning(); ERROR && console.error("Please define a culture");
const cultureName = Names.getCulture(culture, null, null, "", .8); return;
return cultureName + ", The " + meaning;
} }
const meaning = generateMeaning();
const cultureName = Names.getCulture(culture, null, null, "", 0.8);
return cultureName + ", The " + meaning;
};
function generateMeaning() { function generateMeaning() {
const a = ra(approaches); // select generation approach const a = ra(approaches); // select generation approach
@ -318,21 +351,29 @@
function getReligionName(form, deity, center) { function getReligionName(form, deity, center) {
const cells = pack.cells; const cells = pack.cells;
const random = function() {return Names.getCulture(cells.culture[center], null, null, "", 0);} const random = function () {
const type = function() {return rw(types[form]);} return Names.getCulture(cells.culture[center], null, null, "", 0);
const supreme = function() {return deity.split(/[ ,]+/)[0];} };
const place = function(adj) { 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; const base = cells.burg[center] ? pack.burgs[cells.burg[center]].name : pack.states[cells.state[center]].name;
let name = trimVowels(base.split(/[ ,]+/)[0]); let name = trimVowels(base.split(/[ ,]+/)[0]);
return adj ? getAdjective(name) : name; 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); const m = rw(methods);
if (m === "Random + type") return [random() + " " + type(), "global"]; if (m === "Random + type") return [random() + " " + type(), "global"];
if (m === "Random + ism") return [trimVowels(random()) + "ism", "global"]; if (m === "Random + ism") return [trimVowels(random()) + "ism", "global"];
if (m === "Supreme + ism" && deity) return [trimVowels(supreme()) + "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 === "Place + ism") return [place() + "ism", "state"];
if (m === "Culture + ism") return [trimVowels(culture()) + "ism", "culture"]; if (m === "Culture + ism") return [trimVowels(culture()) + "ism", "culture"];
if (m === "Place + ian + type") return [place("adj") + " " + type(), "state"]; if (m === "Place + ian + type") return [place("adj") + " " + type(), "state"];
@ -342,14 +383,19 @@
function getCultName(form, center) { function getCultName(form, center) {
const cells = pack.cells; const cells = pack.cells;
const type = function() {return rw(types[form]);} const type = function () {
const random = function() {return trimVowels(Names.getCulture(cells.culture[center], null, null, "", 0).split(/[ ,]+/)[0]);} return rw(types[form]);
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();
return type() + " of the " + generateMeaning();
}; };
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() > 0.5) return random() + "ian " + type();
return type() + " of the " + generateMeaning();
}
return {generate, add, getDeityName, expandReligions, updateCultures}; return {generate, add, getDeityName, expandReligions, updateCultures};
})();
})));

View file

@ -1,15 +1,18 @@
(function (global, factory) { "use strict";
typeof exports === "object" && typeof module !== "undefined" ? (module.exports = factory()) : typeof define === "function" && define.amd ? define(factory) : (global.Rivers = factory());
})(this, function () {
"use strict";
window.Rivers = (function () {
const generate = function (allowErosion = true) { const generate = function (allowErosion = true) {
TIME && console.time("generateRivers"); TIME && console.time("generateRivers");
Math.random = aleaPRNG(seed); Math.random = aleaPRNG(seed);
const {cells, features} = pack; 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.fl = new Uint16Array(cells.i.length); // water flux array
cells.r = new Uint16Array(cells.i.length); // rivers array cells.r = new Uint16Array(cells.i.length); // rivers array
cells.conf = new Uint8Array(cells.i.length); // confluences array cells.conf = new Uint8Array(cells.i.length); // confluences array
@ -20,6 +23,7 @@
resolveDepressions(h); resolveDepressions(h);
drainWater(); drainWater();
defineRivers(); defineRivers();
calculateConfluenceFlux();
Lakes.cleanupLakeData(); Lakes.cleanupLakeData();
if (allowErosion) cells.h = Uint8Array.from(h); // apply changed heights as basic one if (allowErosion) cells.h = Uint8Array.from(h); // apply changed heights as basic one
@ -28,99 +32,98 @@
function drainWater() { function drainWater() {
const MIN_FLUX_TO_FORM_RIVER = 30; 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 land = cells.i.filter(i => h[i] >= 20).sort((a, b) => h[b] - h[a]);
const lakeOutCells = Lakes.setClimateData(h); const lakeOutCells = Lakes.setClimateData(h);
// const flow = cells.i.length < 65535 ? new Uint16Array(cells.i.length) : new Uint32Array(cells.i.length);
// flow[i] = min;
// debug.append("path").attr("class", "arrow").attr("d", `M${cells.p[i][0]},${cells.p[i][1]}L${cells.p[min][0]},${cells.p[min][1]}`);
land.forEach(function (i) { land.forEach(function (i) {
cells.fl[i] += grid.cells.prec[cells.g[i]]; // flux from precipitation cells.fl[i] += prec[cells.g[i]]; // add flux from precipitation
const [x, y] = p[i];
// create lake outlet if lake is not in deep depression and flux > evaporation // create lake outlet if lake is not in deep depression and flux > evaporation
const lakes = lakeOutCells[i] ? features.filter(feature => i === feature.outCell && feature.flux > feature.evaporation) : []; const lakes = lakeOutCells[i] ? features.filter(feature => i === feature.outCell && feature.flux > feature.evaporation) : [];
for (const lake of lakes) { for (const lake of lakes) {
const lakeCell = cells.c[i].find(c => h[c] < 20 && cells.f[c] === lake.i); const lakeCell = cells.c[i].find(c => h[c] < 20 && cells.f[c] === lake.i);
cells.fl[lakeCell] += Math.max(lake.flux - lake.evaporation, 0); // not evaporated lake water drains to outlet cells.fl[lakeCell] += Math.max(lake.flux - lake.evaporation, 0); // not evaporated lake water drains to outlet
// allow chain lakes to retain identity // allow chain lakes to retain identity
if (cells.r[lakeCell] !== lake.river) { if (cells.r[lakeCell] !== lake.river) {
const sameRiver = cells.c[lakeCell].some(c => cells.r[c] === lake.river); const sameRiver = cells.c[lakeCell].some(c => cells.r[c] === lake.river);
if (sameRiver) { if (sameRiver) {
cells.r[lakeCell] = lake.river; 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 { } else {
cells.r[lakeCell] = riverNext; 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++; riverNext++;
} }
} }
lake.outlet = cells.r[lakeCell]; 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 // assign all tributary rivers to outlet basin
for (let outlet = lakes[0]?.outlet, l = 0; l < lakes.length; l++) { const outlet = lakes[0]?.outlet;
lakes[l].inlets?.forEach(fork => (riversData.find(r => r.river === fork).parent = 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 // near-border cell: pour water out of the screen
if (cells.b[i] && cells.r[i]) { if (cells.b[i] && cells.r[i]) return addCellToRiver(-1, 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;
}
// downhill cell (make sure it's not in the source lake) // 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]; let min = null;
const min = filtered.sort((a, b) => h[a] - h[b])[0]; 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 // cells is depressed
if (h[i] <= h[min]) return; if (h[i] <= h[min]) return;
if (cells.fl[i] < MIN_FLUX_TO_FORM_RIVER) { 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]; if (h[min] >= 20) cells.fl[min] += cells.fl[i];
return; // flux is too small to operate as river return;
} }
// proclaim a new river // proclaim a new river
if (!cells.r[i]) { if (!cells.r[i]) {
cells.r[i] = riverNext; cells.r[i] = riverNext;
riversData.push({river: riverNext, cell: i, x, y, flux: cells.fl[i]}); addCellToRiver(i, riverNext);
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) { function flowDown(toCell, fromFlux, river) {
if (cells.r[toCell]) { const toFlux = cells.fl[toCell] - cells.conf[toCell];
const toRiver = cells.r[toCell];
if (toRiver) {
// downhill cell already has river assigned // downhill cell already has river assigned
if (toFlux < fromFlux) { if (fromFlux > toFlux) {
cells.conf[toCell] = cells.fl[toCell]; // mark confluence 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 (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 cells.r[toCell] = river; // re-assign river if downhill part has less flux
} else { } else {
cells.conf[toCell] += fromFlux; // mark confluence 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 } else cells.r[toCell] = river; // assign the river to the downhill cell
if (h[toCell] < 20) { if (h[toCell] < 20) {
// pour water to the water body // 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]]; const waterBody = features[cells.f[toCell]];
if (waterBody.type === "lake") { if (waterBody.type === "lake") {
if (!waterBody.river || fromFlux > waterBody.enteringFlux) { if (!waterBody.river || fromFlux > waterBody.enteringFlux) {
@ -128,58 +131,69 @@
waterBody.enteringFlux = fromFlux; waterBody.enteringFlux = fromFlux;
} }
waterBody.flux = waterBody.flux + fromFlux; waterBody.flux = waterBody.flux + fromFlux;
waterBody.inlets ? waterBody.inlets.push(river) : (waterBody.inlets = [river]); if (!waterBody.inlets) waterBody.inlets = [river];
else waterBody.inlets.push(river);
} }
} else { } else {
// propagate flux and add next river segment // propagate flux and add next river segment
cells.fl[toCell] += fromFlux; cells.fl[toCell] += fromFlux;
riversData.push({river, cell: toCell, x: p[toCell][0], y: p[toCell][1], flux: fromFlux});
} }
addCellToRiver(toCell, river);
} }
function defineRivers() { function defineRivers() {
cells.r = new Uint16Array(cells.i.length); // re-initiate rivers array // re-initialize rivers and confluence arrays
pack.rivers = []; // rivers data cells.r = new Uint16Array(cells.i.length);
const riverPaths = []; cells.conf = new Uint16Array(cells.i.length);
pack.rivers = [];
for (let r = 1; r <= riverNext; r++) { for (const key in riversData) {
const riverSegments = riversData.filter(d => d.river === r); const riverCells = riversData[key];
if (riverSegments.length < 3) continue; if (riverCells.length < 3) continue; // exclude tiny rivers
for (const segment of riverSegments) { const riverId = +key;
const i = segment.cell; for (const cell of riverCells) {
if (cells.r[i]) continue; if (cell < 0 || cells.h[cell] < 20) continue;
if (cells.h[i] < 20) continue;
cells.r[i] = r; // 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 source = riverCells[0];
const mouth = riverSegments[riverSegments.length - 2].cell; 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 widthFactor = !parent || parent === riverId ? 1.2 : 1;
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 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); pack.rivers.push({i: riverId, source, mouth, discharge, length, width, widthFactor, sourceWidth: 0, parent, cells: riverCells});
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});
} }
// draw rivers function calculateConfluenceFlux() {
rivers.html(riverPaths.map(d => `<path id="river${d[1]}" d="${d[0]}"/>`).join("")); 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 // add distance to water value to land cells to make map less depressed
const alterHeights = () => { const alterHeights = () => {
const cells = pack.cells; const {h, c, t} = pack.cells;
return Array.from(cells.h).map((h, i) => { return Array.from(h).map((h, i) => {
if (h < 20 || cells.t[i] < 1) return h; if (h < 20 || t[i] < 1) return h;
return h + cells.t[i] / 100 + d3.mean(cells.c[i].map(c => cells.t[c])) / 10000; 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`); depressions && WARN && console.warn(`Unresolved depressions: ${depressions}. Edit heightmap to fix`);
}; };
// add more river points on 1/3 and 2/3 of length // add points at 1/3 and 2/3 of a line between adjacents river cells
const addMeandering = function (segments, width = 1, meandering = 0.5) { const addMeandering = function (riverCells, riverPoints = null, meandering = 0.5) {
const riverMeandered = []; // to store enhanced segments 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++) { let fluxPrev = 0;
const sX = segments[s].x, const getFlux = (step, flux) => (step === lastStep ? fluxPrev : flux);
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]);
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, const [x1, y1] = points[i];
eY = segments[s + 1].y; // segment end coordinates const flux1 = getFlux(i, fl[cell]);
const angle = Math.atan2(eY - sY, eX - sX); fluxPrev = flux1;
const sin = Math.sin(angle),
cos = Math.cos(angle);
const meander = meandering + 1 / width + Math.random() * Math.max(meandering - width / 100, 0); meandered.push([x1, y1, flux1]);
const dist2 = (eX - sX) ** 2 + (eY - sY) ** 2; // square distance between segment start and end 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 // 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 p1x = (x1 * 2 + x2) / 3 + -sinMeander;
const p1y = (sY * 2 + eY) / 3 + cos * meander; const p1y = (y1 * 2 + y2) / 3 + cosMeander;
const p2x = (sX + eX * 2) / 3 + sin * meander; const p2x = (x1 + x2 * 2) / 3 + sinMeander / 2;
const p2y = (sY + eY * 2) / 3 + cos * meander; const p2y = (y1 + y2 * 2) / 3 - cosMeander / 2;
riverMeandered.push([p1x, p1y], [p2x, p2y]); const [p1fl, p2fl] = keepInitialFlux ? [flux1, flux1] : [(flux1 * 2 + flux2) / 3, (flux1 + flux2 * 2) / 3];
} else if (dist2 > 25 || segments.length < 6) { 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 // if dist is medium or river is small add 1 extra middlepoint
const p1x = (sX + eX) / 2 + -sin * meander; const p1x = (x1 + x2) / 2 + -sinMeander;
const p1y = (sY + eY) / 2 + cos * meander; const p1y = (y1 + y2) / 2 + cosMeander;
riverMeandered.push([p1x, p1y]); 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) { const getRiverPoints = (riverCells, riverPoints) => {
let offset, const {p} = pack.cells;
extraOffset = sourceWidth; // starting river width (to make river source visible) return riverCells.map((cell, i) => {
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 if (riverPoints && riverPoints[i]) return riverPoints[i];
const widening = 1000 + riverLength * 30; if (cell === -1) return getBorderPoint(riverCells[i - 1]);
const riverPointsLeft = [], return p[cell];
riverPointsRight = []; // store points on both sides to build a valid polygon });
const last = points.length - 1; };
const factor = riverLength / points.length;
// first point const getBorderPoint = i => {
let x = points[0][0], const [x, y] = pack.cells.p[i];
y = points[0][1], const min = Math.min(y, graphHeight - y, x, graphWidth - x);
c; if (min === y) return [x, 0];
let angle = Math.atan2(y - points[1][1], x - points[1][0]); else if (min === graphHeight - y) return [x, graphHeight];
let sin = Math.sin(angle), else if (min === x) return [0, y];
cos = Math.cos(angle); return [graphWidth, y];
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]);
// middle points const FLUX_FACTOR = 500;
for (let p = 1; p < last; p++) { const MAX_FLUX_WIDTH = 2;
(x = points[p][0]), (y = points[p][1]), (c = points[p][2] || 0); const LENGTH_FACTOR = 200;
const xPrev = points[p - 1][0], const STEP_WIDTH = 1 / LENGTH_FACTOR;
yPrev = points[p - 1][1]; const LENGTH_PROGRESSION = [1, 1, 2, 3, 5, 8, 13, 21, 34].map(n => n / LENGTH_FACTOR);
const xNext = points[p + 1][0], const MAX_PROGRESSION = last(LENGTH_PROGRESSION);
yNext = points[p + 1][1];
angle = Math.atan2(yPrev - yNext, xPrev - xNext); const getOffset = (flux, pointNumber, widthFactor = 1, startingWidth = 0) => {
(sin = Math.sin(angle)), (cos = Math.cos(angle)); const fluxWidth = Math.min(flux ** 0.9 / FLUX_FACTOR, MAX_FLUX_WIDTH);
offset = (Math.atan(Math.pow(p * factor, 2) / widening) / 2) * widthFactor + extraOffset; const lengthWidth = pointNumber * STEP_WIDTH + (LENGTH_PROGRESSION[pointNumber] || MAX_PROGRESSION);
const confOffset = Math.atan((c * 5) / widening); return widthFactor * (lengthWidth + fluxWidth) + startingWidth;
extraOffset += confOffset; };
(xLeft = x + -sin * offset), (yLeft = y + cos * (offset + confOffset));
riverPointsLeft.push([xLeft, yLeft]); // build polygon from a list of points and calculated offset (width)
(xRight = x + sin * offset), (yRight = y + -cos * offset); const getRiverPath = function (points, widthFactor = 1, startingWidth = 0) {
riverPointsRight.unshift([xRight, yRight]); 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 const right = lineGen(riverPointsRight.reverse());
(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);
let left = lineGen(riverPointsLeft); let left = lineGen(riverPointsLeft);
left = left.substring(left.indexOf("C")); left = left.substring(left.indexOf("C"));
return [round(right + left, 2), rn(riverLength, 2), offset];
return round(right + left, 1);
}; };
const specify = function () { const specify = function () {
const rivers = pack.rivers; const rivers = pack.rivers;
if (!rivers.length) return; if (!rivers.length) return;
Math.random = aleaPRNG(seed);
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) { for (const river of rivers) {
r.basin = getBasin(r.i); river.basin = getBasin(river.i);
r.name = getName(r.mouth); river.name = getName(river.mouth);
const small = r.length < smallLength; river.type = getType(river);
r.type = r.parent && !(r.i % 6) ? (small ? "Branch" : "Fork") : small ? rw(smallType) : "River";
} }
}; };
@ -361,6 +389,36 @@
return Names.getCulture(pack.cells.culture[cell]); 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 // remove river and all its tributaries
const remove = function (id) { const remove = function (id) {
const cells = pack.cells; const cells = pack.cells;
@ -381,5 +439,5 @@
return getBasin(parent); 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,25 +1,21 @@
(function (global, factory) { window.Routes = (function () {
typeof exports === "object" && typeof module !== "undefined" ? (module.exports = factory()) : typeof define === "function" && define.amd ? define(factory) : (global.Routes = factory());
})(this, function () {
"use strict";
const getRoads = function () { const getRoads = function () {
TIME && console.time("generateMainRoads"); TIME && console.time("generateMainRoads");
const cells = pack.cells; const cells = pack.cells;
const burgs = pack.burgs.filter(b => b.i && !b.removed); 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 if (capitals.length < 2) return []; // not enough capitals to build main roads
const paths = []; // array to store path segments const paths = []; // array to store path segments
for (const b of capitals) { for (const b of capitals) {
const connect = capitals.filter(c => c.i > b.i && c.feature === b.feature); const connect = capitals.filter(c => c.feature === b.feature && c !== b);
if (!connect.length) continue; for (const t of connect) {
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, t.cell, true);
const [from, exit] = findLandPath(b.cell, connect[farthest].cell, null);
const segments = restorePath(b.cell, exit, "main", from); const segments = restorePath(b.cell, exit, "main", from);
segments.forEach(s => paths.push(s)); segments.forEach(s => paths.push(s));
} }
}
cells.i.forEach(i => (cells.s[i] += cells.road[i] / 2)); // add roads to suitability score cells.i.forEach(i => (cells.s[i] += cells.road[i] / 2)); // add roads to suitability score
TIME && console.timeEnd("generateMainRoads"); TIME && console.timeEnd("generateMainRoads");
@ -41,11 +37,12 @@
isle.forEach(function (b, i) { isle.forEach(function (b, i) {
let path = []; let path = [];
if (!i) { if (!i) {
// build trail from the first burg on island to the farthest one on the same island // build trail from the first burg on island
// to the farthest one on the same island 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 farthest = d3.scan(isle, (a, c) => (c.y - b.y) ** 2 + (c.x - b.x) ** 2 - ((a.y - b.y) ** 2 + (a.x - b.x) ** 2));
const to = isle[farthest].cell; const to = isle[farthest].cell;
if (cells.road[to]) return; if (cells.road[to]) return;
const [from, exit] = findLandPath(b.cell, to, null); const [from, exit] = findLandPath(b.cell, to, true);
path = restorePath(b.cell, exit, "small", from); path = restorePath(b.cell, exit, "small", from);
} else { } else {
// build trail from all other burgs to the closest road on the same island // 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 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 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]]; 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 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 heightChangeCost = Math.abs(cells.h[c] - cells.h[n]) * 10; // routes tend to avoid elevation changes
const heightCost = cells.h[c] > 80 ? cells.h[c] : 0; // routes tend to avoid mountainous areas const heightCost = cells.h[c] > 80 ? cells.h[c] : 0; // routes tend to avoid mountainous areas
@ -198,7 +196,7 @@
let segment = [], let segment = [],
current = end, current = end,
prev = 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 (type === "ocean" || !cells.road[prev]) segment.push(end);
if (!cells.road[prev]) cells.road[prev] = score; if (!cells.road[prev]) cells.road[prev] = score;
@ -268,4 +266,4 @@
} }
return [from, exit, false]; return [from, exit, false];
} }
}); })();

View file

@ -74,7 +74,7 @@ async function saveJPEG() {
async function saveTiles() { async function saveTiles() {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
// download schema // download schema
const urlSchema = await getMapURL('tiles', 'schema'); const urlSchema = await getMapURL('tiles', {debug: true});
const zip = new JSZip(); const zip = new JSZip();
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
@ -138,20 +138,26 @@ async function saveTiles() {
} }
// parse map svg to object url // 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 const cloneEl = document.getElementById('map').cloneNode(true); // clone svg
cloneEl.id = 'fantasyMap'; cloneEl.id = 'fantasyMap';
document.body.appendChild(cloneEl); document.body.appendChild(cloneEl);
const clone = d3.select(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 cloneDefs = cloneEl.getElementsByTagName('defs')[0];
const svgDefs = document.getElementById('defElements'); const svgDefs = document.getElementById('defElements');
const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
if (isFirefox && type === 'mesh') clone.select('#oceanPattern').remove(); if (isFirefox && type === 'mesh') clone.select('#oceanPattern').remove();
if (subtype === 'globe') clone.select('#scaleBar').remove(); if (globe) clone.select('#scaleBar').remove();
if (subtype === 'noWater') { 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('#oceanBase').attr('opacity', 0);
clone.select('#oceanPattern').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 // load non-standard fonts
if (fontStyle) clone.select('defs').append('style').text(fontStyle.join('\n')); // add font to style 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(); clone.remove();
const serialized = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>` + new XMLSerializer().serializeToString(cloneEl); const serialized = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>` + new XMLSerializer().serializeToString(cloneEl);
@ -357,63 +371,6 @@ function inlineStyle(clone) {
emptyG.remove(); 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 // prepare map data for saving
function getMapData() { function getMapData() {
TIME && console.time('createMapDataBlob'); TIME && console.time('createMapDataBlob');
@ -445,7 +402,9 @@ function getMapData() {
precOutput.value, precOutput.value,
JSON.stringify(options), JSON.stringify(options),
mapName.value, mapName.value,
+hideLabels.checked +hideLabels.checked,
stylePreset.value,
+rescaleLabels.checked
].join('|'); ].join('|');
const coords = JSON.stringify(mapCoordinates); const coords = JSON.stringify(mapCoordinates);
const biomes = [biomesData.color, biomesData.habitability, biomesData.name].join('|'); const biomes = [biomesData.color, biomesData.habitability, biomesData.name].join('|');
@ -667,10 +626,7 @@ function getRiverPoints(node) {
} }
async function quickSave() { async function quickSave() {
if (customization) { if (customization) return tip('Map cannot be saved when edit mode is active, please exit the mode and retry', false, 'error');
tip('Map cannot be saved when edit mode is active, please exit the mode and retry', false, 'error');
return;
}
const blob = await getMapData(); const blob = await getMapData();
if (blob) ldb.set('lastMap', blob); // auto-save map 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); 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() { function clicked() {
const el = d3.event.target; const el = d3.event.target;
if (!el || !el.parentElement || !el.parentElement.parentElement) return; if (!el || !el.parentElement || !el.parentElement.parentElement) return;
const parent = el.parentElement, const parent = el.parentElement;
grand = parent.parentElement, const grand = parent.parentElement;
great = grand.parentElement; const great = grand.parentElement;
const p = d3.mouse(this); const p = d3.mouse(this);
const i = findCell(p[0], p[1]); const i = findCell(p[0], p[1]);
if (grand.id === 'emblems') editEmblem(); 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 (grand.id === 'routes') editRoute();
else if (el.tagName === 'tspan' && grand.parentNode.parentNode.id === 'labels') editLabel(); else if (el.tagName === 'tspan' && grand.parentNode.parentNode.id === 'labels') editLabel();
else if (grand.id === 'burgLabels') editBurg(); else if (grand.id === 'burgLabels') editBurg();
@ -118,33 +118,6 @@ function applySorting(headers) {
.forEach((line) => list.appendChild(line)); .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) { function addBurg(point) {
const cells = pack.cells; const cells = pack.cells;
const x = rn(point[0], 2), const x = rn(point[0], 2),
@ -405,15 +378,14 @@ function clearLegend() {
} }
// draw color (fill) picker // draw color (fill) picker
function createPicker(hatching) { function createPicker() {
const COLORS_IN_ROW = 14;
const pos = () => tip('Drag to change the picker position'); const pos = () => tip('Drag to change the picker position');
const cl = () => tip('Click to close the picker'); 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%'); const contaiter = 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); 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 = container const picker = contaiter
.append('g') .append('g')
.attr('id', 'picker') .attr('id', 'picker')
.call( .call(
@ -469,7 +441,11 @@ function createPicker(hatching) {
spaces.selectAll('input').on('change', changePickerSpace); spaces.selectAll('input').on('change', changePickerSpace);
const colors = picker.append('g').attr('id', 'pickerColors').attr('stroke', '#333333'); 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) { clr.forEach(function (d, i) {
colors colors
.append('rect') .append('rect')
@ -481,14 +457,8 @@ function createPicker(hatching) {
.attr('width', 16) .attr('width', 16)
.attr('height', 16); .attr('height', 16);
}); });
colors
.selectAll('rect')
.on('click', pickerFillClicked)
.on('mousemove', () => tip('Click to fill with the color'));
if (hatching) { hatching.each(function (d, i) {
const hatches = picker.append('g').attr('id', 'pickerHatches').attr('stroke', '#333333');
d3.selectAll('g#hatching > pattern').each(function (d, i) {
hatches hatches
.append('rect') .append('rect')
.attr('id', 'picker_' + this.id) .attr('id', 'picker_' + this.id)
@ -498,11 +468,15 @@ function createPicker(hatching) {
.attr('width', 16) .attr('width', 16)
.attr('height', 16); .attr('height', 16);
}); });
colors
.selectAll('rect')
.on('click', pickerFillClicked)
.on('mousemove', () => tip('Click to fill with the color'));
hatches hatches
.selectAll('rect') .selectAll('rect')
.on('click', pickerFillClicked) .on('click', pickerFillClicked)
.on('mousemove', () => tip('Click to fill with the hatching')); .on('mousemove', () => tip('Click to fill with the hatching'));
}
// append box // append box
const bbox = picker.node().getBBox(); const bbox = picker.node().getBBox();
@ -973,6 +947,7 @@ function selectIcon(initial, callback) {
// Calls the refresh for all currently open editors // Calls the refresh for all currently open editors
function refreshAllEditors() { function refreshAllEditors() {
TIME && console.time('refreshAllEditors');
if (document.getElementById('culturesEditorRefresh').offsetParent) culturesEditorRefresh.click(); if (document.getElementById('culturesEditorRefresh').offsetParent) culturesEditorRefresh.click();
if (document.getElementById('biomesEditorRefresh').offsetParent) biomesEditorRefresh.click(); if (document.getElementById('biomesEditorRefresh').offsetParent) biomesEditorRefresh.click();
if (document.getElementById('diplomacyEditorRefresh').offsetParent) diplomacyEditorRefresh.click(); if (document.getElementById('diplomacyEditorRefresh').offsetParent) diplomacyEditorRefresh.click();
@ -980,5 +955,5 @@ function refreshAllEditors() {
if (document.getElementById('religionsEditorRefresh').offsetParent) religionsEditorRefresh.click(); if (document.getElementById('religionsEditorRefresh').offsetParent) religionsEditorRefresh.click();
if (document.getElementById('statesEditorRefresh').offsetParent) statesEditorRefresh.click(); if (document.getElementById('statesEditorRefresh').offsetParent) statesEditorRefresh.click();
if (document.getElementById('zonesEditorRefresh').offsetParent) zonesEditorRefresh.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 {string} tip Tooltip text
* @param {boolean} main Show above other tooltips * @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 * @param {number} time Timeout to auto hide, ms
*/ */
function tip(tip = 'Tip is undefined', main, type, time) { function tip(tip = 'Tip is undefined', main, type, time) {
@ -96,10 +96,7 @@ function showMapTooltip(point, e, i, g) {
const land = pack.cells.h[i] >= 20; const land = pack.cells.h[i] >= 20;
// specific elements // specific elements
if (group === 'armies') { if (group === 'armies') return tip(e.target.parentNode.dataset.name + '. Click to edit');
tip(e.target.parentNode.dataset.name + '. Click to edit');
return;
}
if (group === 'emblems' && e.target.tagName === 'use') { if (group === 'emblems' && e.target.tagName === 'use') {
const parent = e.target.parentNode; const parent = e.target.parentNode;
@ -130,14 +127,11 @@ function showMapTooltip(point, e, i, g) {
if (riversOverview.offsetParent) highlightEditorLine(riversOverview, river, 5000); if (riversOverview.offsetParent) highlightEditorLine(riversOverview, river, 5000);
return; return;
} }
if (group === 'routes') {
tip('Click to edit the Route'); if (group === 'routes') return tip('Click to edit the Route');
return;
} if (group === 'terrain') return tip('Click to edit the Relief Icon');
if (group === 'terrain') {
tip('Click to edit the Relief Icon');
return;
}
if (subgroup === 'burgLabels' || subgroup === 'burgIcons') { if (subgroup === 'burgLabels' || subgroup === 'burgIcons') {
const burg = +path[path.length - 10].dataset.id; const burg = +path[path.length - 10].dataset.id;
const b = pack.burgs[burg]; const b = pack.burgs[burg];
@ -146,50 +140,25 @@ function showMapTooltip(point, e, i, g) {
if (burgsOverview.offsetParent) highlightEditorLine(burgsOverview, burg, 5000); if (burgsOverview.offsetParent) highlightEditorLine(burgsOverview, burg, 5000);
return; return;
} }
if (group === 'labels') { if (group === 'labels') return tip('Click to edit the Label');
tip('Click to edit the Label');
return; if (group === 'markers') return tip('Click to edit the Marker');
}
if (group === 'markers') {
tip('Click to edit the Marker');
return;
}
if (group === 'ruler') { if (group === 'ruler') {
const tag = e.target.tagName; const tag = e.target.tagName;
const className = e.target.getAttribute('class'); const className = e.target.getAttribute('class');
if (tag === 'circle' && className === 'edge') { if (tag === 'circle' && className === 'edge') return tip('Drag to adjust. Hold Ctrl and drag to add a point. Click to remove the point');
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');
return; 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 === 'circle' && className === 'control') { if (tag === 'path') return tip('Drag to move the measurer');
tip('Drag to adjust. Hold Shifta and drag to keep axial direction. Click to remove the point'); if (tag === 'text') return tip('Drag to move, click to remove the measurer');
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 (subgroup === 'burgIcons') return tip('Click to edit the Burg');
if (subgroup === 'burgLabels') return tip('Click to edit the Burg');
if (group === 'lakes' && !land) { if (group === 'lakes' && !land) {
const lakeId = +e.target.dataset.f; const lakeId = +e.target.dataset.f;
const name = pack.features[lakeId]?.name; const name = pack.features[lakeId]?.name;
@ -197,20 +166,16 @@ function showMapTooltip(point, e, i, g) {
tip(`${fullName} lake. Click to edit`); tip(`${fullName} lake. Click to edit`);
return; return;
} }
if (group === 'coastline') { if (group === 'coastline') return tip('Click to edit the coastline');
tip('Click to edit the coastline');
return;
}
if (group === 'zones') { if (group === 'zones') {
const zone = path[path.length - 8]; const zone = path[path.length - 8];
tip(zone.dataset.description); tip(zone.dataset.description);
if (zonesEditor.offsetParent) highlightEditorLine(zonesEditor, zone.id, 5000); if (zonesEditor.offsetParent) highlightEditorLine(zonesEditor, zone.id, 5000);
return; return;
} }
if (group === 'ice') {
tip('Click to edit the Ice'); if (group === 'ice') return tip('Click to edit the Ice');
return;
}
// covering elements // covering elements
if (layerIsOn('togglePrec') && land) tip('Annual Precipitation: ' + getFriendlyPrecipitation(i)); 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'; 'use strict';
function editHeightmap() { function editHeightmap() {
@ -134,15 +133,8 @@ function editHeightmap() {
// Exit customization mode // Exit customization mode
function finalizeHeightmap() { function finalizeHeightmap() {
if (viewbox.select('#heights').selectAll('*').size() < 200) { 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');
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');
return;
}
if (document.getElementById('imageConverter').offsetParent) {
tip('Please exit the Image Conversion mode first', null, 'error');
return;
}
delete window.edits; // remove global variable delete window.edits; // remove global variable
redo.disabled = templateRedo.disabled = true; redo.disabled = templateRedo.disabled = true;
@ -207,6 +199,7 @@ function editHeightmap() {
} }
} }
drawRivers();
Lakes.defineGroup(); Lakes.defineGroup();
defineBiomes(); defineBiomes();
@ -586,6 +579,7 @@ function editHeightmap() {
document.getElementById('brushesSliders').style.display = 'none'; document.getElementById('brushesSliders').style.display = 'none';
} }
const dragBrushThrottled = throttle(dragBrush, 100);
function toggleBrushMode(e) { function toggleBrushMode(e) {
if (e.target.classList.contains('pressed')) { if (e.target.classList.contains('pressed')) {
exitBrushMode(); exitBrushMode();
@ -594,7 +588,7 @@ function editHeightmap() {
exitBrushMode(); exitBrushMode();
document.getElementById('brushesSliders').style.display = 'block'; document.getElementById('brushesSliders').style.display = 'block';
e.target.classList.add('pressed'); 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() { function dragBrush() {
@ -842,118 +836,15 @@ function editHeightmap() {
body.setAttribute('data-changed', 0); body.setAttribute('data-changed', 0);
body.innerHTML = ''; body.innerHTML = '';
if (template === 'templateVolcano') { const templateString = HeightmapTemplates[template];
addStep('Hill', '1', '90-100', '44-56', '40-60'); if (!templateString) return;
addStep('Multiply', 0.8, '50-100');
addStep('Range', '1.5', '30-55', '45-55', '40-60'); const steps = templateString.split('\n');
addStep('Smooth', 2); if (!steps.length) return tip(`Heightmap template: no steps defined`, false, 'error');
addStep('Hill', '1.5', '25-35', '25-30', '20-75');
addStep('Hill', '1', '25-35', '75-80', '25-75'); for (const step of steps) {
addStep('Hill', '0.5', '20-25', '10-15', '20-25'); const elements = step.trim().split(' ');
} else if (template === 'templateHighIsland') { addStep(...elements);
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');
} }
} }
@ -1119,6 +1010,10 @@ function editHeightmap() {
const reader = new FileReader(); const reader = new FileReader();
const img = new Image(); const img = new Image();
img.id = 'imageToConvert';
img.style.display = 'none';
document.body.appendChild(img);
img.onload = function () { img.onload = function () {
const ctx = document.getElementById('canvas').getContext('2d'); const ctx = document.getElementById('canvas').getContext('2d');
ctx.drawImage(img, 0, 0, graphWidth, graphHeight); ctx.drawImage(img, 0, 0, graphWidth, graphHeight);
@ -1311,10 +1206,7 @@ function editHeightmap() {
} }
function applyConversion() { function applyConversion() {
if (colorsAssigned.childElementCount < 3) { if (colorsAssigned.childElementCount < 3) return tip('Please do the assignment first', false, 'error');
tip('Please do the assignment first', false, 'error');
return;
}
viewbox viewbox
.select('#heights') .select('#heights')
@ -1340,6 +1232,9 @@ function editHeightmap() {
const canvas = document.getElementById('canvas'); const canvas = document.getElementById('canvas');
if (canvas) canvas.remove(); if (canvas) canvas.remove();
const image = document.getElementById('imageToConvert');
if (image) image.remove();
d3.select('#imageConverter').selectAll('div.color-div').remove(); d3.select('#imageConverter').selectAll('div.color-div').remove();
colorsAssigned.style.display = 'none'; colorsAssigned.style.display = 'none';
colorsUnassigned.style.display = 'none'; colorsUnassigned.style.display = 'none';

View file

@ -123,9 +123,10 @@ function restoreLayers() {
if (layerIsOn('toggleIce')) drawIce(); if (layerIsOn('toggleIce')) drawIce();
if (layerIsOn('toggleEmblems')) drawEmblems(); if (layerIsOn('toggleEmblems')) drawEmblems();
// states are getting rendered each time, if it's not required than layers should be hidden // some layers are rendered each time, remove them if they are not on
if (!layerIsOn('toggleBorders')) $('#borders').fadeOut(); if (!layerIsOn('toggleBorders')) borders.selectAll('path').remove();
if (!layerIsOn('toggleStates')) regions.style('display', 'none').selectAll('path').remove(); if (!layerIsOn('toggleStates')) regions.selectAll('path').remove();
if (!layerIsOn('toggleRivers')) rivers.selectAll('*').remove();
} }
function toggleHeight(event) { function toggleHeight(event) {
@ -876,35 +877,80 @@ function toggleStates(event) {
} }
} }
// draw states
function drawStates() { function drawStates() {
TIME && console.time('drawStates'); TIME && console.time('drawStates');
regions.selectAll('path').remove(); regions.selectAll('path').remove();
const cells = pack.cells, const {cells, vertices, features} = pack;
vertices = pack.vertices, const states = pack.states;
states = pack.states, const n = cells.i.length;
n = cells.i.length;
const used = new Uint8Array(cells.i.length); const used = new Uint8Array(cells.i.length);
const vArray = new Array(states.length); // store vertices array const vArray = new Array(states.length); // store vertices array
const body = new Array(states.length).fill(''); // store path around each state const body = new Array(states.length).fill(''); // path around each state
const gap = new Array(states.length).fill(''); // store path along water for each state to fill the gaps 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) { for (const i of cells.i) {
if (!cells.state[i] || used[i]) continue; if (!cells.state[i] || used[i]) continue;
const s = cells.state[i]; const state = cells.state[i];
const onborder = cells.c[i].some((n) => cells.state[n] !== s);
const onborder = cells.c[i].some((n) => cells.state[n] !== state);
if (!onborder) continue; 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 vertex = cells.v[i].find((v) => vertices.c[v].some((i) => cells.state[i] === borderWith));
const chain = connectVertices(vertex, s, borderWith); const chain = connectVertices(vertex, state);
if (chain.length < 3) continue;
const points = chain.map((v) => vertices.p[v[0]]); const noInnerLakes = chain.filter((v) => v[1] !== 'innerLake');
if (!vArray[s]) vArray[s] = []; if (noInnerLakes.length < 3) continue;
vArray[s].push(points);
body[s] += 'M' + points.join('L'); // get path around the state
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), ''); 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 // find state visual center
@ -913,13 +959,14 @@ function drawStates() {
states[i].pole = polylabel(sorted, 1.0); // pole of inaccessibility 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 bodyData = body.map((p, s) => [p.length > 10 ? p : null, s, states[s].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 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 bodyString = bodyData.map((d) => `<path id="state${d[1]}" d="${d[0]}" fill="${d[2]}" stroke="none"/>`).join('');
const gapString = gapData.map((d) => `<path id="state-gap${d[1]}" d="${d[0]}" fill="none" stroke="${d[2]}"/>`).join(''); const gapString = gapData.map((d) => `<path id="state-gap${d[1]}" d="${d[0]}" fill="none" stroke="${d[2]}"/>`).join('');
const clipString = bodyData.map((d) => `<clipPath id="state-clip${d[1]}"><use href="#state${d[1]}"/></clipPath>`).join(''); const clipString = bodyData.map((d) => `<clipPath id="state-clip${d[1]}"><use href="#state${d[1]}"/></clipPath>`).join('');
const haloString = bodyData 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'}"/>`) .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(''); .join('');
@ -928,57 +975,77 @@ function drawStates() {
statesHalo.html(haloString); statesHalo.html(haloString);
// connect vertices to chain // connect vertices to chain
function connectVertices(start, t, state) { function connectVertices(start, state) {
const chain = []; // vertices chain to form a path const chain = []; // vertices chain to form a path
let land = vertices.c[start].some((c) => cells.h[c] >= 20 && cells.state[c] !== t); const getType = (c) => {
function check(i) { const borderCell = c.find((i) => cells.b[i]);
state = cells.state[i]; if (borderCell) return 'border';
land = cells.h[i] >= 20;
} 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++) { 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 const prev = chain.length ? chain[chain.length - 1][0] : -1; // previous vertex in chain
chain.push([current, state, land]); // add current vertex to sequence
const c = vertices.c[current]; // cells adjacent to vertex const c = vertices.c[current]; // cells adjacent to vertex
c.filter((c) => cells.state[c] === t).forEach((c) => (used[c] = 1)); chain.push([current, getType(c)]); // add current vertex to sequence
const c0 = c[0] >= n || cells.state[c[0]] !== t;
const c1 = c[1] >= n || cells.state[c[1]] !== t; c.filter((c) => cells.state[c] === state).forEach((c) => (used[c] = 1));
const c2 = c[2] >= n || cells.state[c[2]] !== t; 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 const v = vertices.v[current]; // neighboring vertices
if (v[0] !== prev && c0 !== c1) {
current = v[0]; 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];
} else if (v[1] !== prev && c1 !== c2) { else if (v[2] !== prev && c0 !== c2) current = v[2];
current = v[1];
check(c1 ? c[1] : c[2]); if (current === prev) {
} else if (v[2] !== prev && c0 !== c2) {
current = v[2];
check(c2 ? c[2] : c[0]);
}
if (current === chain[chain.length - 1][0]) {
ERROR && console.error('Next vertex is not found'); ERROR && console.error('Next vertex is not found');
break; break;
} }
} }
chain.push([start, state, land]); // add starting vertex to sequence to close the path
if (chain.length) chain.push(chain[0]);
return chain; return chain;
} }
invokeActiveZooming(); invokeActiveZooming();
TIME && console.timeEnd('drawStates'); 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 // draw state and province borders
function drawBorders() { function drawBorders() {
TIME && console.time('drawBorders'); TIME && console.time('drawBorders');
borders.selectAll('path').remove(); borders.selectAll('path').remove();
const cells = pack.cells, const {cells, vertices} = pack;
vertices = pack.vertices, const n = cells.i.length;
n = cells.i.length;
const sPath = [], const sPath = [];
pPath = []; const pPath = [];
const sUsed = new Array(pack.states.length).fill('').map((a) => []);
const pUsed = new Array(pack.provinces.length).fill('').map((a) => []); 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++) { for (let i = 0; i < cells.i.length; i++) {
if (!cells.state[i]) continue; if (!cells.state[i]) continue;
@ -1070,21 +1137,6 @@ function drawBorders() {
TIME && console.timeEnd('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) { function toggleProvinces(event) {
if (!layerIsOn('toggleProvinces')) { if (!layerIsOn('toggleProvinces')) {
turnButtonOn('toggleProvinces'); turnButtonOn('toggleProvinces');
@ -1396,18 +1448,30 @@ function toggleTexture(event) {
function toggleRivers(event) { function toggleRivers(event) {
if (!layerIsOn('toggleRivers')) { if (!layerIsOn('toggleRivers')) {
turnButtonOn('toggleRivers'); turnButtonOn('toggleRivers');
$('#rivers').fadeIn(); drawRivers();
if (event && isCtrlClick(event)) editStyle('rivers'); if (event && isCtrlClick(event)) editStyle('rivers');
} else { } else {
if (event && isCtrlClick(event)) { if (event && isCtrlClick(event)) return editStyle('rivers');
editStyle('rivers'); rivers.selectAll('*').remove();
return;
}
$('#rivers').fadeOut();
turnButtonOff('toggleRivers'); 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) { function toggleRoutes(event) {
if (!layerIsOn('toggleRoutes')) { if (!layerIsOn('toggleRoutes')) {
turnButtonOn('toggleRoutes'); turnButtonOn('toggleRoutes');
@ -1558,21 +1622,21 @@ function drawEmblems() {
const getStateEmblemsSize = () => { const getStateEmblemsSize = () => {
const startSize = Math.min(Math.max((graphHeight + graphWidth) / 40, 10), 100); 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 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 return rn((startSize / statesMod) * sizeMod); // target size ~50px on 1536x754 map with 15 states
}; };
const getProvinceEmblemsSize = () => { const getProvinceEmblemsSize = () => {
const startSize = Math.min(Math.max((graphHeight + graphWidth) / 100, 5), 70); 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 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 return rn((startSize / provincesMod) * sizeMod); // target size ~20px on 1536x754 map with 115 provinces
}; };
const getBurgEmblemSize = () => { const getBurgEmblemSize = () => {
const startSize = Math.min(Math.max((graphHeight + graphWidth) / 185, 2), 50); 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 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 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, 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, 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, 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 const array = supporters
.replace(/(?:\r\n|\r|\n)/g, '') .replace(/(?:\r\n|\r|\n)/g, '')
@ -157,6 +158,7 @@ optionsContent.addEventListener('change', function (event) {
if (id === 'zoomExtentMin' || id === 'zoomExtentMax') changeZoomExtent(value); if (id === 'zoomExtentMin' || id === 'zoomExtentMax') changeZoomExtent(value);
else if (id === 'optionsSeed') generateMapWithSeed(); else if (id === 'optionsSeed') generateMapWithSeed();
else if (id === 'uiSizeInput' || id === 'uiSizeOutput') changeUIsize(value); else if (id === 'uiSizeInput' || id === 'uiSizeOutput') changeUIsize(value);
if (id === 'shapeRendering') viewbox.attr('shape-rendering', value);
else if (id === 'yearInput') changeYear(); else if (id === 'yearInput') changeYear();
else if (id === 'eraInput') changeEra(); else if (id === 'eraInput') changeEra();
}); });
@ -494,6 +496,9 @@ function applyStoredOptions() {
const height = +params.get('height'); const height = +params.get('height');
if (width) mapWidthInput.value = width; if (width) mapWidthInput.value = width;
if (height) mapHeightInput.value = height; 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') // randomize options if randomization is allowed (not locked or options='default')
@ -537,17 +542,18 @@ function randomizeOptions() {
// select heightmap template pseudo-randomly // select heightmap template pseudo-randomly
function randomizeHeightmapTemplate() { function randomizeHeightmapTemplate() {
const templates = { const templates = {
Volcano: 3, volcano: 3,
'High Island': 22, highIsland: 22,
'Low Island': 9, lowIsland: 9,
Continents: 20, continents: 19,
Archipelago: 25, archipelago: 23,
Mediterranean: 3, mediterranean: 5,
Peninsula: 3, peninsula: 3,
Pangea: 5, pangea: 5,
Isthmus: 2, isthmus: 2,
Atoll: 1, atoll: 1,
Shattered: 7 shattered: 7,
taklamakan: 1
}; };
document.getElementById('templateInput').value = rw(templates); document.getElementById('templateInput').value = rw(templates);
} }
@ -773,6 +779,12 @@ document
.forEach((el) => el.addEventListener('input', updateTilesOptions)); .forEach((el) => el.addEventListener('input', updateTilesOptions));
function 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 tileSize = document.getElementById('tileSize');
const tilesX = +document.getElementById('tileColsOutput').value; const tilesX = +document.getElementById('tileColsOutput').value;
const tilesY = +document.getElementById('tileRowsOutput').value; const tilesY = +document.getElementById('tileRowsOutput').value;
@ -908,6 +920,7 @@ function toggle3dOptions() {
document.getElementById('options3dMeshRotationNumber').addEventListener('change', changeRotation); document.getElementById('options3dMeshRotationNumber').addEventListener('change', changeRotation);
document.getElementById('options3dGlobeRotationRange').addEventListener('input', changeRotation); document.getElementById('options3dGlobeRotationRange').addEventListener('input', changeRotation);
document.getElementById('options3dGlobeRotationNumber').addEventListener('change', changeRotation); document.getElementById('options3dGlobeRotationNumber').addEventListener('change', changeRotation);
document.getElementById('options3dMeshLabels3d').addEventListener('change', toggleLabels3d);
document.getElementById('options3dMeshSkyMode').addEventListener('change', toggleSkyMode); document.getElementById('options3dMeshSkyMode').addEventListener('change', toggleSkyMode);
document.getElementById('options3dMeshSky').addEventListener('input', changeColors); document.getElementById('options3dMeshSky').addEventListener('input', changeColors);
document.getElementById('options3dMeshWater').addEventListener('input', changeColors); document.getElementById('options3dMeshWater').addEventListener('input', changeColors);
@ -924,6 +937,7 @@ function toggle3dOptions() {
options3dSunZ.value = ThreeD.options.sun.z; options3dSunZ.value = ThreeD.options.sun.z;
options3dMeshRotationRange.value = options3dMeshRotationNumber.value = ThreeD.options.rotateMesh; options3dMeshRotationRange.value = options3dMeshRotationNumber.value = ThreeD.options.rotateMesh;
options3dGlobeRotationRange.value = options3dGlobeRotationNumber.value = ThreeD.options.rotateGlobe; options3dGlobeRotationRange.value = options3dGlobeRotationNumber.value = ThreeD.options.rotateGlobe;
options3dMeshLabels3d.value = ThreeD.options.labels3d;
options3dMeshSkyMode.value = ThreeD.options.extendedWater; options3dMeshSkyMode.value = ThreeD.options.extendedWater;
options3dColorSection.style.display = ThreeD.options.extendedWater ? 'block' : 'none'; options3dColorSection.style.display = ThreeD.options.extendedWater ? 'block' : 'none';
options3dMeshSky.value = ThreeD.options.skyColor; options3dMeshSky.value = ThreeD.options.skyColor;
@ -954,6 +968,10 @@ function toggle3dOptions() {
ThreeD.setRotation(speed); ThreeD.setRotation(speed);
} }
function toggleLabels3d() {
ThreeD.toggleLabels();
}
function toggleSkyMode() { function toggleSkyMode() {
const hide = ThreeD.options.extendedWater; const hide = ThreeD.options.extendedWater;
options3dColorSection.style.display = hide ? 'none' : 'block'; options3dColorSection.style.display = hide ? 'none' : 'block';

View file

@ -81,6 +81,7 @@ function editReliefIcon() {
reliefSpacingDiv.style.display = 'none'; reliefSpacingDiv.style.display = 'none';
reliefIconsSeletionAny.style.display = 'none'; reliefIconsSeletionAny.style.display = 'none';
removeCircle();
updateReliefSizeInput(); updateReliefSizeInput();
restoreDefaultEvents(); restoreDefaultEvents();
clearMainTip(); clearMainTip();
@ -115,10 +116,7 @@ function editReliefIcon() {
function dragToAdd() { function dragToAdd() {
const pressed = reliefIconsDiv.querySelector('svg.pressed'); const pressed = reliefIconsDiv.querySelector('svg.pressed');
if (!pressed) { if (!pressed) return tip('Please select an icon', false, error);
tip('Please select an icon', false, error);
return;
}
const type = pressed.dataset.type; const type = pressed.dataset.type;
const r = +reliefRadiusNumber.value; const r = +reliefRadiusNumber.value;
@ -188,10 +186,7 @@ function editReliefIcon() {
function dragToRemove() { function dragToRemove() {
const pressed = reliefIconsDiv.querySelector('svg.pressed'); const pressed = reliefIconsDiv.querySelector('svg.pressed');
if (!pressed) { if (!pressed) return tip('Please select an icon', false, error);
tip('Please select an icon', false, error);
return;
}
const r = +reliefRadiusNumber.value; const r = +reliefRadiusNumber.value;
const type = pressed.dataset.type; const type = pressed.dataset.type;
@ -256,12 +251,32 @@ function editReliefIcon() {
} }
function removeIcon() { function removeIcon() {
const message = 'Are you sure you want to remove the relief icon? <br>This action cannot be reverted'; let selection = null;
const onConfirm = () => { const pressed = reliefTools.querySelector('button.pressed');
elSelected.remove(); 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'); $('#reliefEditor').dialog('close');
}; },
confirmationDialog({title: 'Remove relief icon', message, confirm: 'Remove', onConfirm}); Cancel: function () {
$(this).dialog('close');
}
}
});
} }
function closeReliefEditor() { 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'; 'use strict';
function editRiver(id) { function editRiver(id) {
if (customization) return; if (customization) return;
if (elSelected && d3.event && d3.event.target.id === elSelected.attr('id')) return; if (elSelected && id === elSelected.attr('id')) return;
closeDialogs('.stable'); closeDialogs('.stable');
if (!layerIsOn('toggleRivers')) toggleRivers(); if (!layerIsOn('toggleRivers')) toggleRivers();
const node = id ? document.getElementById(id) : d3.event.target; document.getElementById('toggleCells').dataset.forced = +!layerIsOn('toggleCells');
elSelected = d3.select(node).on('click', addInterimControlPoint); if (!layerIsOn('toggleCells')) toggleCells();
viewbox.on('touchmove mousemove', showEditorTips);
debug.append('g').attr('id', 'controlPoints').attr('transform', elSelected.attr('transform')); 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(); updateRiverData();
drawControlPoints(node);
const river = getRiver();
const {cells, points} = river;
const riverPoints = Rivers.getRiverPoints(cells, points);
drawControlPoints(riverPoints, cells);
drawCells(cells, 'current');
$('#riverEditor').dialog({ $('#riverEditor').dialog({
title: 'Edit River', title: 'Edit River',
resizable: false, 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 close: closeRiverEditor
}); });
@ -23,27 +33,19 @@ function editRiver(id) {
modules.editRiver = true; modules.editRiver = true;
// add listeners // 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('riverName').addEventListener('input', changeName);
document.getElementById('riverType').addEventListener('input', changeType); document.getElementById('riverType').addEventListener('input', changeType);
document.getElementById('riverNameCulture').addEventListener('click', generateNameCulture); document.getElementById('riverNameCulture').addEventListener('click', generateNameCulture);
document.getElementById('riverNameRandom').addEventListener('click', generateNameRandom); document.getElementById('riverNameRandom').addEventListener('click', generateNameRandom);
document.getElementById('riverMainstem').addEventListener('change', changeParent); document.getElementById('riverMainstem').addEventListener('change', changeParent);
document.getElementById('riverSourceWidth').addEventListener('input', changeSourceWidth); document.getElementById('riverSourceWidth').addEventListener('input', changeSourceWidth);
document.getElementById('riverWidthFactor').addEventListener('input', changeWidthFactor); 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() { function getRiver() {
const riverId = +elSelected.attr('id').slice(5); const riverId = +elSelected.attr('id').slice(5);
const river = pack.rivers.find((r) => r.i === riverId); 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('riverBasin').value = pack.rivers.find((river) => river.i === r.basin).name;
document.getElementById('riverDischarge').value = r.discharge + ' m³/s'; 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('riverSourceWidth').value = r.sourceWidth;
document.getElementById('riverWidthFactor').value = r.widthFactor; document.getElementById('riverWidthFactor').value = r.widthFactor;
updateRiverLength(r);
updateRiverWidth(r);
} }
function drawControlPoints(node) { function updateRiverLength(river) {
const length = getRiver().length; river.length = rn(elSelected.node().getTotalLength() / 2, 2);
const segments = Math.ceil(length / 4); const lengthUI = `${rn(river.length * distanceScaleInput.value)} ${distanceUnitInput.value}`;
const increment = rn((length / segments) * 1e5); document.getElementById('riverLength').value = lengthUI;
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 addControlPoint(point, before = null) { function updateRiverWidth(river) {
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); 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() { function drawControlPoints(points, cells) {
this.setAttribute('cx', d3.event.x);
this.setAttribute('cy', d3.event.y);
redrawRiver();
}
function redrawRiver() {
const points = [];
debug debug
.select('#controlPoints') .select('#controlPoints')
.selectAll('circle') .selectAll('circle')
.each(function () { .data(points)
points.push([+this.getAttribute('cx'), +this.getAttribute('cy')]); .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));
}
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);
}
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();
}); });
if (points.length < 2) return; d3.event.on('end', () => {
if (points.length === 2) { if (movedToCell) {
const p0 = points[0], this.dataset.cell = movedToCell;
p1 = points[1]; river.cells[index] = movedToCell;
const angle = Math.atan2(p1[1] - p0[1], p1[0] - p0[0]); drawCells(river.cells, 'current');
const sin = Math.sin(angle),
cos = Math.cos(angle); if (!r[movedToCell]) {
elSelected.attr('d', `M${p0[0]},${p0[1]} L${p1[0]},${p1[1]} l${-sin / 2},${cos / 2} Z`); // swap river data
return; r[initCell] = 0;
r[movedToCell] = river.i;
const sourceFlux = fl[initCell];
fl[initCell] = fl[movedToCell];
fl[movedToCell] = sourceFlux;
}
}
});
} }
const widthFactor = +document.getElementById('riverWidthFactor').value; function redrawRiver() {
const sourceWidth = +document.getElementById('riverSourceWidth').value; const river = getRiver();
const [path, length, offset] = Rivers.getPath(points, widthFactor, sourceWidth); 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); elSelected.attr('d', path);
const r = getRiver(); updateRiverLength(river);
if (r) {
r.width = rn(offset ** 2, 2);
r.length = length;
updateRiverData();
}
if (modules.elevation) showEPForRiver(elSelected.node()); 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() { function changeName() {
getRiver().name = this.value; getRiver().name = this.value;
} }
@ -174,12 +196,16 @@ function editRiver(id) {
} }
function changeSourceWidth() { function changeSourceWidth() {
getRiver().sourceWidth = +this.value; const river = getRiver();
river.sourceWidth = +this.value;
updateRiverWidth(river);
redrawRiver(); redrawRiver();
} }
function changeWidthFactor() { function changeWidthFactor() {
getRiver().widthFactor = +this.value; const river = getRiver();
river.widthFactor = +this.value;
updateRiverWidth(river);
redrawRiver(); redrawRiver();
} }
@ -194,81 +220,35 @@ function editRiver(id) {
editNotes(id, river.name + ' ' + river.type); 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() { function removeRiver() {
const message = 'Are you sure you want to remove the river? <br>All tributaries will be auto-removed'; alertMessage.innerHTML = 'Are you sure you want to remove the river and all its tributaries';
const onConfirm = () => { $('#alert').dialog({
resizable: false,
width: '22em',
title: 'Remove river and tributaries',
buttons: {
Remove: function () {
$(this).dialog('close');
const river = +elSelected.attr('id').slice(5); const river = +elSelected.attr('id').slice(5);
Rivers.remove(river); Rivers.remove(river);
elSelected.remove(); // if river if missed in pack.rivers elSelected.remove();
$('#riverEditor').dialog('close'); $('#riverEditor').dialog('close');
}; },
confirmationDialog({title: 'Remove river', message, confirm: 'Remove', onConfirm}); Cancel: function () {
$(this).dialog('close');
}
}
});
} }
function closeRiverEditor() { function closeRiverEditor() {
exitRiverCreationMode();
elSelected.on('click', null);
debug.select('#controlPoints').remove(); debug.select('#controlPoints').remove();
debug.select('#controlCells').remove();
unselect(); 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 // add listeners
document.getElementById('riversOverviewRefresh').addEventListener('click', riversOverviewAddLines); document.getElementById('riversOverviewRefresh').addEventListener('click', riversOverviewAddLines);
document.getElementById('addNewRiver').addEventListener('click', toggleAddRiver); document.getElementById('addNewRiver').addEventListener('click', toggleAddRiver);
document.getElementById('riverCreateNew').addEventListener('click', createRiver);
document.getElementById('riversBasinHighlight').addEventListener('click', toggleBasinsHightlight); document.getElementById('riversBasinHighlight').addEventListener('click', toggleBasinsHightlight);
document.getElementById('riversExport').addEventListener('click', downloadRiversData); document.getElementById('riversExport').addEventListener('click', downloadRiversData);
document.getElementById('riversRemoveAll').addEventListener('click', triggerAllRiversRemove); document.getElementById('riversRemoveAll').addEventListener('click', triggerAllRiversRemove);
@ -129,27 +130,53 @@ function overviewRivers() {
} }
function openRiverEditor() { function openRiverEditor() {
editRiver('river' + this.parentNode.dataset.id); const id = 'river' + this.parentNode.dataset.id;
editRiver(id);
} }
function triggerRiverRemove() { function triggerRiverRemove() {
const river = +this.parentNode.dataset.id; 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'; $('#alert').dialog({
const onConfirm = () => { resizable: false,
width: '22em',
title: 'Remove river',
buttons: {
Remove: function () {
Rivers.remove(river); Rivers.remove(river);
riversOverviewAddLines(); riversOverviewAddLines();
}; $(this).dialog('close');
confirmationDialog({title: 'Remove river', message, confirm: 'Remove', onConfirm}); },
Cancel: function () {
$(this).dialog('close');
}
}
});
} }
function triggerAllRiversRemove() { function triggerAllRiversRemove() {
const message = 'Are you sure you want to remove all rivers? <br>This action cannot be reverted'; alertMessage.innerHTML = `Are you sure you want to remove all rivers?`;
const onConfirm = () => { $('#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.rivers = [];
pack.cells.r = new Uint16Array(pack.cells.i.length);
rivers.selectAll('*').remove(); rivers.selectAll('*').remove();
riversOverviewAddLines(); riversOverviewAddLines();
};
confirmationDialog({title: 'Remove all rivers', message, confirm: 'Remove', onConfirm});
} }
} }

View file

@ -849,18 +849,21 @@ function editStates() {
} }
function adjustProvinces(affectedProvinces) { function adjustProvinces(affectedProvinces) {
const cells = pack.cells, const {cells, provinces, states} = pack;
provinces = pack.provinces,
states = pack.states;
const form = {Zone: 1, Area: 1, Territory: 2, Province: 1}; const form = {Zone: 1, Area: 1, Territory: 2, Province: 1};
<<<<<<< HEAD
affectedProvinces.forEach((p) => { affectedProvinces.forEach((p) => {
// do nothing if neutral lands are captured // do nothing if neutral lands are captured
if (!p) return; 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 // 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 // find states owning at least 1 province cell
const provCells = cells.i.filter((i) => cells.province[i] === p); const provCells = cells.i.filter((i) => cells.province[i] === p);
@ -871,8 +874,13 @@ function editStates() {
if (owner) { if (owner) {
const name = provinces[p].name; const name = provinces[p].name;
<<<<<<< HEAD
// if province is historical part of abouther state province, unite with old province // 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)); 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) { if (part) {
provinces[p].removed = true; provinces[p].removed = true;
provCells.filter((i) => cells.state[i] === owner).forEach((i) => (cells.province[i] = part)); 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,93 +551,119 @@ function toggleAddRiver() {
} }
function addRiverOnClick() { function addRiverOnClick() {
const cells = pack.cells; const {cells, rivers} = pack;
const point = d3.mouse(this); let i = findCell(...d3.mouse(this));
let i = findCell(point[0], point[1]);
if (cells.r[i] || cells.h[i] < 20 || cells.b[i]) return;
const dataRiver = []; // to store river points if (cells.r[i]) return tip('There is already a river here', false, 'error');
let river = +getNextId('river').slice(5); // river id if (cells.h[i] < 20) return tip('Cannot create river in water cell', false, 'error');
cells.fl[i] = grid.cells.prec[cells.g[i]]; // initial flux if (cells.b[i]) return;
const h = Rivers.alterHeights(); const {alterHeights, resolveDepressions, addMeandering, getRiverPath, getBasin, getName, getType, getWidth, getOffset, getApproximateLength} = Rivers;
Lakes.prepareLakeData(h); const riverCells = [];
Rivers.resolveDepressions(h); 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) { while (i) {
cells.r[i] = river; cells.r[i] = riverId;
const [x, y] = cells.p[i]; riverCells.push(i);
dataRiver.push({x, y, cell: i});
const min = cells.c[i].sort((a, b) => h[a] - h[b])[0]; // downhill cell 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'); if (h[i] <= h[min]) return tip(`Cell ${i} is depressed, river cannot flow further`, false, 'error');
const [tx, ty] = cells.p[min];
if (h[min] < 20) {
// pour to water body // pour to water body
dataRiver.push({x: tx, y: ty, cell: i}); if (h[min] < 20) {
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; 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]) { if (!cells.r[min]) {
// continue if next cell has not river
cells.fl[min] += cells.fl[i]; cells.fl[min] += cells.fl[i];
i = min; i = min;
continue; continue;
} }
// handle case when lowest cell already has a river // handle case when lowest cell already has a river
const r = cells.r[min]; const oldRiverId = cells.r[min];
const riverCells = cells.i.filter((i) => cells.r[i] === r); const oldRiver = rivers.find((river) => river.i === oldRiverId);
const riverCellsUpper = riverCells.filter((i) => h[i] > h[min]); 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 // create new river as a tributary
if (dataRiver.length <= riverCellsUpper.length) { if (riverCells.length <= oldRiverCellsUpper.length) {
cells.conf[min] += cells.fl[i]; cells.conf[min] += cells.fl[i];
dataRiver.push({x: tx, y: ty, cell: min}); riverCells.push(min);
dataRiver[0].parent = r; // new river is tributary parent = oldRiverId;
break; break;
} }
// extend old river // continue old river
rivers.select('#river' + r).remove(); document.getElementById('river' + oldRiverId)?.remove();
cells.i.filter((i) => cells.r[i] === river).forEach((i) => (cells.r[i] = r)); riverCells.forEach((i) => (cells.r[i] = oldRiverId));
riverCells.forEach((i) => (cells.r[i] = 0)); oldRiverCells.forEach((cell) => {
river = r; if (h[cell] > h[min]) {
cells.fl[min] = cells.fl[i] + grid.cells.prec[cells.g[min]]; cells.r[cell] = 0;
i = min; cells.fl[cell] = grid.cells.prec[cells.g[cell]];
}
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);
// 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
if (r) {
r.source = dataRiver[0].cell;
r.length = length;
r.discharge = discharge;
} else { } else {
const parent = dataRiver[0].parent || 0; riverCells.push(cell);
const basin = Rivers.getBasin(river); cells.fl[cell] += cells.fl[i];
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';
pack.rivers.push({i: river, source, mouth, discharge, length, width, widthFactor, sourceWidth, parent, basin, name, type});
} }
});
riverId = oldRiverId;
break;
}
const river = rivers.find((r) => r.i === riverId);
const source = riverCells[0];
const mouth = riverCells[riverCells.length - 2];
const widthFactor = river?.widthFactor || (!parent || parent === riverId ? 1.2 : 1);
const meanderedPoints = addMeandering(riverCells);
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 basin = getBasin(parent);
const name = getName(mouth);
const type = getType({i: riverId, length, parent});
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) { if (d3.event.shiftKey === false) {
Lakes.cleanupLakeData(); Lakes.cleanupLakeData();

View file

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

View file

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