mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-16 17:31:24 +01:00
commit
1e60b477fb
45 changed files with 3384 additions and 1072 deletions
BIN
Fantasy Map Generator.lnk
Normal file
BIN
Fantasy Map Generator.lnk
Normal file
Binary file not shown.
5
Readme.txt
Normal file
5
Readme.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
Azgaar's Fantasy Map Generator
|
||||
This is an open-source software available under MIT license
|
||||
https://github.com/Azgaar/Fantasy-Map-Generator
|
||||
|
||||
To run the tool unzip ALL files and open index.html in browser
|
||||
36
icons.css
36
icons.css
|
|
@ -250,7 +250,6 @@
|
|||
.icon-smooth:before {font-weight: bold;content:'∼';}
|
||||
.icon-disrupt:before {font-weight: bold;content:'⥄';}
|
||||
.icon-if:before {font-style: italic; font-weight: bold;content:'if';}
|
||||
/* .icon-coa:before {content: '⚜'; font-size: 1.1em; margin: -2px;} */
|
||||
.icon-coa:before {content:'\f3ed'; font-size: .9em; color: #999;} /* '' */
|
||||
.icon-half:before {font-weight: bold;content:'½';}
|
||||
.icon-curve:before {content: 'C';}
|
||||
|
|
@ -263,4 +262,37 @@
|
|||
margin-left: 1px;
|
||||
width: .6em;
|
||||
font-family: monospace;
|
||||
}
|
||||
}
|
||||
.icon-die:before {content:'🎲';}
|
||||
.icon-button-die:before {content:'🎲'; padding-right: .4em;}
|
||||
.icon-button-power:before {content:'💪'; padding-right: .6em;}
|
||||
|
||||
.icon-button-melee:before {content:'⚔️'; padding-right: .4em;}
|
||||
.icon-button-skirmish:before {content:'🎯'; padding-right: .4em;}
|
||||
.icon-button-pursue:before {content:'🐎'; padding-right: .4em;}
|
||||
.icon-button-retreat:before {content:'🏳️'; padding-right: .4em;}
|
||||
.icon-button-shelling:before {content:'💣'; padding-right: .4em;}
|
||||
.icon-button-boarding:before {content:'⚔️'; padding-right: .4em;}
|
||||
.icon-button-chase:before {content:'⛵'; padding-right: .4em;}
|
||||
.icon-button-withdrawal:before {content:'🏳️'; padding-right: .4em;}
|
||||
.icon-button-bombardment:before {content:'💣'; padding-right: .4em;}
|
||||
.icon-button-blockade:before {content:'⏳'; padding-right: .4em;}
|
||||
.icon-button-sheltering:before {content:'🔒'; padding-right: .4em;}
|
||||
.icon-button-sortie:before {content:'🚪'; padding-right: .4em;}
|
||||
.icon-button-defense:before {content:'🛡️'; padding-right: .4em;}
|
||||
.icon-button-storming:before {content:'⚔️'; padding-right: .4em;}
|
||||
.icon-button-looting:before {content:'☠️'; padding-right: .4em;}
|
||||
.icon-button-surrendering:before {content:'🏳️'; padding-right: .4em;}
|
||||
.icon-button-surprise:before {content:'⚡'; padding-right: .4em;}
|
||||
.icon-button-shock:before {content:'💫'; padding-right: .4em;}
|
||||
.icon-button-flee:before {content:'⛵'; padding-right: .4em;}
|
||||
.icon-button-waiting:before {content:'⌛'; padding-right: .4em;}
|
||||
.icon-button-maneuvering:before {content:'💢'; padding-right: .4em;}
|
||||
.icon-button-dogfight:before {content:'🐕'; padding-right: .4em;}
|
||||
|
||||
.icon-button-field:before {content:'🗡️'; padding-right: .4em;}
|
||||
.icon-button-naval:before {content:'🌊'; padding-right: .4em;}
|
||||
.icon-button-siege:before {content:'🏰'; padding-right: .4em;}
|
||||
.icon-button-ambush:before {content:'🌳'; padding-right: .4em;}
|
||||
.icon-button-landing:before {content:'⚓'; padding-right: .4em;}
|
||||
.icon-button-air:before {content:'💨'; padding-right: .4em;}
|
||||
BIN
images/icon.ico
Normal file
BIN
images/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 195 KiB |
149
index.css
149
index.css
|
|
@ -101,7 +101,7 @@ button, select, a, .pointer {
|
|||
fill-rule: evenodd;
|
||||
}
|
||||
|
||||
#lakes, #coastline, #armies {
|
||||
#lakes, #coastline, #armies, #ice {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
|
@ -114,7 +114,7 @@ button, select, a, .pointer {
|
|||
fill-rule: evenodd;
|
||||
}
|
||||
|
||||
#oceanLayers {
|
||||
#oceanLayers, #terrs {
|
||||
fill-rule: evenodd;
|
||||
}
|
||||
|
||||
|
|
@ -123,7 +123,7 @@ button, select, a, .pointer {
|
|||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
#regions, #cults, #relig, #biomes, #provs, #terrs, #biomes, #tooltip, #temperature, #texture, #landmass {
|
||||
#regions, #cults, #relig, #biomes, #provs, #terrs, #biomes, #tooltip, #temperature, #texture, #landmass, #fogging {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
|
@ -333,13 +333,13 @@ div.tab > button#optionsHide {
|
|||
.tab {
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid #5d4651;
|
||||
height: 2.3em;
|
||||
height: 2.2em;
|
||||
}
|
||||
|
||||
#options p {
|
||||
font-style: italic;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0;
|
||||
margin: .8em 0 0 0;
|
||||
}
|
||||
|
||||
#aboutContent {
|
||||
|
|
@ -353,6 +353,7 @@ div.tab > button#optionsHide {
|
|||
#aboutContent a {
|
||||
color: #1d1b1c;
|
||||
font-weight: bold;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#optionsContent span {
|
||||
|
|
@ -482,7 +483,6 @@ input[type="color"]::-webkit-color-swatch-wrapper {
|
|||
width: 100%;
|
||||
background-color: white;
|
||||
text-align: left;
|
||||
height: 1.5em;
|
||||
}
|
||||
|
||||
#optionsContent input[type="range"] {
|
||||
|
|
@ -579,10 +579,9 @@ input[type="color"]::-webkit-color-swatch-wrapper {
|
|||
}
|
||||
|
||||
.tab > button.options {
|
||||
/* width: 23.25%; */
|
||||
width: 18.6%;
|
||||
height: 100%;
|
||||
padding: 7px 0px;
|
||||
padding: 6px 0px;
|
||||
}
|
||||
|
||||
button.options {
|
||||
|
|
@ -590,6 +589,7 @@ button.options {
|
|||
font-weight: bold;
|
||||
float: left;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
padding: 8px 10px;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
|
@ -949,19 +949,80 @@ body button.noicon {
|
|||
stroke: #2c0808;
|
||||
}
|
||||
|
||||
#battleBody > table {
|
||||
padding: .2em .6em .2em .6em;
|
||||
border: 1px solid #ccc;
|
||||
margin: .2em 0 .4em 0;
|
||||
display: block;
|
||||
overflow: auto;
|
||||
max-height: 34vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#battleBody > table .regiment {
|
||||
width: 13em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
tr.battleCasualties, tr.battleSurvivors {
|
||||
font-style: italic;
|
||||
font-size: .9em;
|
||||
}
|
||||
|
||||
#battleBody div.battlePhases,
|
||||
#battleBottom div.battleTypes {
|
||||
position: fixed;
|
||||
background-color: #ffffff30;
|
||||
}
|
||||
|
||||
#battleBody div.battlePhases > button,
|
||||
#battleBottom div.battleTypes > button {
|
||||
width: 3.2em;
|
||||
display: block;
|
||||
margin: .2em 0;
|
||||
}
|
||||
|
||||
div#regimentSelectorBody {
|
||||
max-height: 50vh;
|
||||
font-size: .9em;
|
||||
}
|
||||
|
||||
div#regimentSelectorBody > div {
|
||||
padding: .1em;
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
|
||||
div#regimentSelectorBody > div:hover {
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
div#regimentSelectorBody > div.selected {
|
||||
border: 1px solid #b28585;
|
||||
}
|
||||
|
||||
div#regimentSelectorBody > div.inactive {
|
||||
background-color: #eee;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
div#regimentSelectorBody > div > div {
|
||||
display: inline-block;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.drag-trigger {
|
||||
border-left: 12px solid transparent;
|
||||
border-right: 12px solid #916e7f;
|
||||
border-top: 12px solid transparent;
|
||||
border-left: 1em solid transparent;
|
||||
border-right: 1em solid #000;
|
||||
border-top: 1em solid transparent;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 100%;
|
||||
margin-top: -12px;
|
||||
right: -1px;
|
||||
bottom: -1px;
|
||||
opacity: .3;
|
||||
}
|
||||
|
||||
.drag-trigger:hover {
|
||||
cursor: move;
|
||||
border-right-color: #5e4fa2;
|
||||
opacity: .6;
|
||||
}
|
||||
|
||||
.tint {
|
||||
|
|
@ -969,10 +1030,10 @@ body button.noicon {
|
|||
}
|
||||
|
||||
.color-div {
|
||||
width: 2.7em;
|
||||
height: 1.1em;
|
||||
width: 3em;
|
||||
height: 1em;
|
||||
display: inline-block;
|
||||
margin: .1em .2em;
|
||||
margin: 0 .16em;
|
||||
border: 1px #c5c5c5 groove;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
@ -1151,10 +1212,21 @@ div.slider .ui-slider-handle {
|
|||
|
||||
.table {
|
||||
max-height: 75vh;
|
||||
max-width: 75vw;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.overflow {
|
||||
max-width: 93vw;
|
||||
overflow: auto;
|
||||
max-height: 75vh;
|
||||
}
|
||||
|
||||
.overflow > div {
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
div.header > div {
|
||||
font-weight: bold;
|
||||
font-size: .9em;
|
||||
|
|
@ -1657,7 +1729,6 @@ rect.fillRect {
|
|||
|
||||
#militaryOptionsTable input {
|
||||
width: 9em;
|
||||
padding-left: 3px;
|
||||
border: 1px solid #d4d4d4;
|
||||
}
|
||||
|
||||
|
|
@ -1665,6 +1736,10 @@ rect.fillRect {
|
|||
width: 4em;
|
||||
}
|
||||
|
||||
#militaryOptionsTable button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#gridOverlay {
|
||||
fill: none;
|
||||
}
|
||||
|
|
@ -1691,7 +1766,7 @@ ul.share-buttons li {
|
|||
}
|
||||
|
||||
ul.share-buttons img {
|
||||
width: 18px;
|
||||
width: 2em;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
|
|
@ -1760,24 +1835,6 @@ div.textual span, .textual legend {
|
|||
vertical-align: top;
|
||||
}
|
||||
|
||||
#markerIconTable {
|
||||
font-size: 1.6em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#markerIconTable td:hover {
|
||||
transition: .1s;
|
||||
color: #3c3ca9;
|
||||
}
|
||||
|
||||
#markerIconTable td:active {
|
||||
transform: translate(0px, 1px);
|
||||
}
|
||||
|
||||
#markerIconTable td.selected {
|
||||
outline: 1px solid #9b9b9b;
|
||||
}
|
||||
|
||||
.highlighted {
|
||||
outline-width: 2px;
|
||||
outline-style: dashed;
|
||||
|
|
@ -1808,7 +1865,9 @@ div#notesHeader {
|
|||
}
|
||||
|
||||
div#notesBody {
|
||||
padding: 0 10px;
|
||||
padding: 0 1em;
|
||||
max-height: 80vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
svg.button {
|
||||
|
|
@ -2025,7 +2084,7 @@ svg.button {
|
|||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
max-width: 21em;
|
||||
max-width: 22em;
|
||||
background-color: #fff;
|
||||
padding: 1.2em;
|
||||
border: solid 1px #000;
|
||||
|
|
@ -2056,6 +2115,16 @@ svg.button {
|
|||
text-shadow: 0px 1px 4px #4c3a35;
|
||||
}
|
||||
|
||||
.epgrid line {
|
||||
stroke: lightgrey;
|
||||
stroke-opacity: .7;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
|
||||
.epgrid path {
|
||||
stroke-width: 0;
|
||||
}
|
||||
|
||||
#debug {
|
||||
font-size: 1px;
|
||||
opacity: .8;
|
||||
|
|
|
|||
406
index.html
406
index.html
|
|
@ -34,11 +34,11 @@
|
|||
#loading-text span:nth-child(3), #mapOverlay > span:nth-child(3) {animation-delay: 2s;}
|
||||
@keyframes blink {0% {opacity: 0;} 20% {opacity: 1;} 100% {opacity: .1;}}
|
||||
</style>
|
||||
<link rel="preload" href="index.css?version=1.3" as="style">
|
||||
<link rel="preload" href="icons.css?version=1.3" as="style">
|
||||
<link rel="preload" href="index.css?version=1.4" as="style">
|
||||
<link rel="preload" href="icons.css?version=1.4" as="style">
|
||||
<link rel="preload" href="libs/jquery-ui.css" as="style">
|
||||
<link rel="stylesheet" href="index.css?version=1.3">
|
||||
<link rel="stylesheet" href="icons.css?version=1.3">
|
||||
<link rel="stylesheet" href="index.css?version=1.4">
|
||||
<link rel="stylesheet" href="icons.css?version=1.4">
|
||||
<link rel="stylesheet" href="libs/jquery-ui.css">
|
||||
|
||||
</head>
|
||||
|
|
@ -214,8 +214,8 @@
|
|||
<mask id="water">
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="white"></rect>
|
||||
</mask>
|
||||
<mask id="fog">
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="white"></rect>
|
||||
<mask id="fog" style="stroke-width: 10; stroke: black; stroke-linejoin: round; stroke-opacity: .1;">
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="white" stroke="none"></rect>
|
||||
</mask>
|
||||
<g id="textPaths"></g>
|
||||
<g id="statePaths"></g>
|
||||
|
|
@ -899,7 +899,7 @@
|
|||
<div id="loading">
|
||||
<div id="titleName"><t data-t="titleName">Azgaar's</t></div>
|
||||
<div id="title"><t data-t="title">Fantasy Map Generator</t></div>
|
||||
<div id="version"><t data-t="version">v. </t>1.3</div>
|
||||
<div id="version"><t data-t="version">v. </t>1.4</div>
|
||||
<p id="loading-text"><t data-t="loading">LOADING</t><span>.</span><span>.</span><span>.</span></p>
|
||||
</div>
|
||||
|
||||
|
|
@ -960,6 +960,7 @@
|
|||
<li id="toggleRoutes"data-tip="Trade routes: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style. Shortcut: U" onclick="toggleRoutes(event)">Ro<u>u</u>tes</li>
|
||||
<li id="toggleTemp" data-tip="Temperature map: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style. Shortcut: T" class="buttonoff" onclick="toggleTemp(event)"><u>T</u>emperature</li>
|
||||
<li id="togglePopulation" data-tip="Population map: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style. Shortcut: N" class="buttonoff" onclick="togglePopulation(event)">Populatio<u>n</u></li>
|
||||
<li id="toggleIce" data-tip="Icebergs and glaciers: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style. Shortcut: J" class="buttonoff" onclick="toggleIce(event)">Ice</li>
|
||||
<li id="togglePrec" data-tip="Precipitation map: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style. Shortcut: A" class="buttonoff" onclick="togglePrec(event)">Precipit<u>a</u>tion</li>
|
||||
<li id="toggleLabels" data-tip="Labels: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style. Shortcut: L" onclick="toggleLabels(event)"><u>L</u>abels</li>
|
||||
<li id="toggleIcons" data-tip="Burg icons: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style. Shortcut: I" onclick="toggleIcons(event)"><u>I</u>cons</li>
|
||||
|
|
@ -1002,6 +1003,7 @@
|
|||
<option value="fogging">Fogging</option>
|
||||
<option value="gridOverlay">Grid</option>
|
||||
<option value="terrs">Heightmap</option>
|
||||
<option value="ice">Ice</option>
|
||||
<option value="labels">Labels</option>
|
||||
<option value="lakes">Lakes</option>
|
||||
<option value="landmass">Landmass</option>
|
||||
|
|
@ -1022,7 +1024,6 @@
|
|||
<option value="texture">Texture</option>
|
||||
<option value="compass">Wind Rose</option>
|
||||
<option value="zones">Zones</option>
|
||||
<option value="seaIce">Ice</option>
|
||||
</select>
|
||||
<!-- <button id="restoreStyle" data-tip="Click to restore default style for all elements" class="icon-ccw styleButton" onclick="askToRestoreDefaultStyle()"></button> -->
|
||||
|
||||
|
|
@ -1580,7 +1581,7 @@
|
|||
</td>
|
||||
</tr>
|
||||
|
||||
<tr data-tip="Click to set up map name to be used for downloaded files">
|
||||
<tr data-tip="Define map name (will be used to name downloaded files)">
|
||||
<td>
|
||||
<i data-locked=0 id="lock_mapName" class="icon-lock-open"></i>
|
||||
</td>
|
||||
|
|
@ -1593,6 +1594,20 @@
|
|||
</td>
|
||||
</tr>
|
||||
|
||||
<tr data-tip="Define current year and era name">
|
||||
<td>
|
||||
<i data-locked=0 id="lock_era" class="icon-lock-open"></i>
|
||||
</td>
|
||||
<td>Year and era</td>
|
||||
<td>
|
||||
<input id="yearInput" data-stored="year" type="number" step=1 class="paired" style="width: 24%; float: left; font-size: smaller">
|
||||
<input id="eraInput" data-stored="era" autocorrect="off" spellcheck="false" type="text" style="width: 75%; float: right" class="long">
|
||||
</td>
|
||||
<td>
|
||||
<i id="optionsEraRegenerate" data-tip="Regenerate era" class="icon-arrows-cw"></i>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr data-tip="Select template to be used for a Heightmap generation">
|
||||
<td>
|
||||
<i data-locked=0 id="lock_template" class="icon-lock-open"></i>
|
||||
|
|
@ -1684,7 +1699,7 @@
|
|||
<input id="powerInput" data-stored="power" type="range" min=0 max=10 step=.2 value=5>
|
||||
</td>
|
||||
<td>
|
||||
<input id="powerOutput" data-stored="power" type="number" min=0 max=10 step=.2 value=5>
|
||||
<input id="powerOutput" data-stored="power" type="number" min=0 max=10 step=.1 value=5>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
|
@ -1775,17 +1790,6 @@
|
|||
</td>
|
||||
</tr>
|
||||
|
||||
<tr data-tip="Define scale of a saved png/jpeg image (e.g. 5x). Saving big images is slow and may cause a browser crash!">
|
||||
<td></td>
|
||||
<td>PNG/JPEG scale</td>
|
||||
<td>
|
||||
<input id="pngResolutionInput" data-stored="pngResolution" type="range" min=1 max=8 value=1>
|
||||
</td>
|
||||
<td>
|
||||
<input id="pngResolutionOutput" data-stored="pngResolution" type="number" min=1 max=8 value=1>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr data-tip="Set minimum and maximum possible zoom level">
|
||||
<td></td>
|
||||
<td>Zoom extent</td>
|
||||
|
|
@ -1796,7 +1800,8 @@
|
|||
<input data-tip="Maximal possible zoom level (should be > 1)" id="zoomExtentMax" class="paired" type="number" min=1 max=50 value=20>
|
||||
</td>
|
||||
<td>
|
||||
<i data-tip="Restore default [1, 20] zoom extent" id="zoomExtentDefault" class="icon-ccw"></i>
|
||||
<i data-tip="Restore default zoom extent (1, 20)" id="zoomExtentDefault" class="icon-ccw"></i>
|
||||
<i data-tip="Allow to drag map beyond canvas borders" id="translateExtent" data-on=0 class="icon-hand-paper-o"></i>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
|
@ -1857,6 +1862,7 @@
|
|||
<button id="regenerateProvinces" data-tip="Click to regenerate provinces. States will remain as they are">Provinces</button>
|
||||
<button id="regenerateReligions" data-tip="Click to regenerate religions">Religions</button>
|
||||
<button id="regenerateMilitary" data-tip="Click to recalculate military forces based on current military options">Military</button>
|
||||
<button id="regenerateIce" data-tip="Click to icebergs and glaciers">Ice</button>
|
||||
<button id="regenerateMarkers" data-tip="Click to regenerate markers. Hold Ctrl and click to set markers number multiplier">Markers</button>
|
||||
<button id="regenerateZones" data-tip="Click to regenerate zones. Hold Ctrl and click to set zones number multiplier">Zones</button>
|
||||
</div>
|
||||
|
|
@ -1877,8 +1883,8 @@
|
|||
<button data-tip="Display brushes panel" id="paintBrushes">Paint Brushes</button>
|
||||
<button data-tip="Open template editor" id="applyTemplate" style="display: none">Template Editor</button>
|
||||
<button data-tip="Open Image Converter" id="convertImage" style="display: none">Image Converter</button>
|
||||
<button data-tip="Render heightmap data as a small monochrome image" id="heightmapPreview">Heightmap Preview</button>
|
||||
<button data-tip="Preview heightmap in 3D scene" id="heightmap3DView">3D</button>
|
||||
<button data-tip="Render heightmap data as a small monochrome image" id="heightmapPreview">Preview</button>
|
||||
<button data-tip="Preview heightmap in 3D scene" id="heightmap3DView">3D scene</button>
|
||||
</div>
|
||||
|
||||
<div id="customizeOptions">
|
||||
|
|
@ -1910,22 +1916,21 @@
|
|||
|
||||
<div id="aboutContent" class="tabcontent">
|
||||
<p><a href="https://github.com/Azgaar/Fantasy-Map-Generator" target="_blank">Fantasy Map Generator</a> is a free <a href="https://github.com/Azgaar/Fantasy-Map-Generator/blob/master/LICENSE" target="_blank">open source</a> tool which procedurally generates fantasy maps. You may use auto-generated maps as they are, edit them or even create a new map from scratch. Check out the <a href="https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Quick-Start-Tutorial" target="_blank">quick start tutorial</a>, <a href="https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Q&A" target="_blank">Q&A</a> and <a href="https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys" target="_blank">hotkeys</a> for guidance.</p>
|
||||
<p>Join our <a href='https://discordapp.com/invite/X7E84HU' target='_blank'>Discord server</a> and <a href="https://www.reddit.com/r/FantasyMapGenerator/" target="_blank">Reddit community</a> to ask questions, get help and share created maps. You may support the project on <a href='https://www.patreon.com/azgaar' target='_blank'>Patreon</a>.</p>
|
||||
<p>The project is under active development. Creator and main maintainer: Azgaar. For older versions see the <a href="https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog" target="_blank">changelog</a>. To track the development progress see the <a href="https://trello.com/b/7x832DG4/fantasy-map-generator" target="_blank">devboard</a>. Please report bugs <a href="https://github.com/Azgaar/Fantasy-Map-Generator/issues" target="_blank">here</a>. You can also contact me directly via <a href="mailto:azgaar.fmg@yandex.by" target="_blank">email</a>.</p>
|
||||
<p>Special thanks to all supporters on Patreon! <i data-tip="Click to see supporters names" class="collapsible icon-down-open pointer"></i></p>
|
||||
<p style="display:none">Patreon Supporters: Aaron Meyer, Ahmad Amerih, AstralJacks, aymeric, Billy Dean Goehring, Branndon Edwards,
|
||||
Chase Mayers, Curt Flood, cyninge, Dino Princip, E.M. White, es, Fondue, Fritjof Olsson, Gatsu, Johan Fröberg, Jonathan Moore,
|
||||
Joseph Miranda, Kate, KC138, Luke Nelson, Markus Finster, Massimo Vella, Mikey, Nathan Mitchell, Paavi1, Pat, Ryan Westcott,
|
||||
Sasquatch, Shawn Spencer, Sizz_TV, Timothée CALLET, UTG community, Vlad Tomash, Wil Sisney, William Merriott, Xariun,
|
||||
Gun Metal Games, Scott Marner, Spencer Sherman, Valerii Matskevych, Alloyed Clavicle, Stewart Walsh, Ruthlyn Mollett (Javan),
|
||||
Benjamin Mair-Pratt, Diagonath, Alexander Thomas, Ashley Wilson-Savoury, William Henry, Preston Brooks, JOSHUA QUALTIERI,
|
||||
Hilton Williams, Katharina Haase, Hisham Bedri, Ian arless, Karnat, Bird, Kevin, Jessica Thomas, Steve Hyatt, Logicspren,
|
||||
Alfred García, Jonathan Killstring, John Ackley, Invad3r233, Norbert Žigmund, Jennifer, PoliticsBuff, _gfx_, Maggie,
|
||||
Connor McMartin, Jared McDaris, BlastWind, Franc Casanova Ferrer, Dead & Devil, Michael Carmody, Valerie Elise, naikibens220,
|
||||
Jordon Phillips, William Pucs, The Dungeon Masters, Brady R Rathbun, J, Shadow, Matthew Tiffany, Huw Williams, Joseph Hamilton,
|
||||
FlippantFeline, Tamashi Toh, kms, Stephen Herron, MidnightMoon, Whakomatic x, Barished, Aaron bateson, Brice Moss, Diklyquill,
|
||||
PatronUser, Michael Greiner, Steven Bennett, Jacob Harrington, Miguel C., Reya C., Giant Monster Games, Noirbard, Brian Drennen,
|
||||
Ben Craigie, Alex Smolin and many others!</p>
|
||||
<p>Join our <a href='https://discordapp.com/invite/X7E84HU' target='_blank'>Discord server</a> and <a href="https://www.reddit.com/r/FantasyMapGenerator/" target="_blank">Reddit community</a> to ask questions, get help and share maps.</p>
|
||||
<p>The project is under active development. Creator and main maintainer: Azgaar. To track the development progress see the <a href="https://trello.com/b/7x832DG4/fantasy-map-generator" target="_blank">devboard</a>. For older versions see the <a href="https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog" target="_blank">changelog</a>. Please report bugs <a href="https://github.com/Azgaar/Fantasy-Map-Generator/issues" target="_blank">here</a>. You can also contact me directly via <a href="mailto:azgaar.fmg@yandex.by" target="_blank">email</a>.</p>
|
||||
<div style="background-color: #e85b46; padding: .4em; width: max-content; margin: .6em auto 0 auto; border: 1px solid #943838">
|
||||
<a href="https://www.patreon.com/azgaar" target="_blank" style="color: white; text-decoration: none; font-family: sans-serif">
|
||||
<div>
|
||||
<div style="width: .8em; display: inline-block; padding: 0 .2em; fill: white">
|
||||
<svg viewBox="0 0 569 546">
|
||||
<circle cx="362.589996" cy="204.589996" data-fill="1" id="Oval" r="204.589996"></circle>
|
||||
<rect data-fill="2" height="545.799988" id="Rectangle" width="100" x="0" y="0"></rect>
|
||||
</svg>
|
||||
</div>SUPPORT ON PATREON
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<p>Special thanks to <a data-tip="Click to see list of supporters" onclick="showSupporters()">all supporters</a> on Patreon!</p>
|
||||
|
||||
<ul class="share-buttons">
|
||||
<li><a href="https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fazgaar.github.io%2FFantasy-Map-Generator%2F"e=" data-tip="Share on Facebook" target="_blank"><img alt="Share on Facebook" src="images/Facebook.png" /></a></li>
|
||||
|
|
@ -2098,7 +2103,7 @@
|
|||
|
||||
<button id="labelAlign" data-tip="Turn text path into a straight line" class="icon-resize-horizontal"></button>
|
||||
<button id="labelLegend" data-tip="Edit free text notes (legend) for this label" class="icon-edit"></button>
|
||||
<button id="labelRemoveSingle" data-tip="Remove the label. Shortcut: Delete" class="icon-trash"></button>
|
||||
<button id="labelRemoveSingle" data-tip="Remove the label. Shortcut: Delete" class="icon-trash fastDelete"></button>
|
||||
</div>
|
||||
|
||||
<div id="riverEditor" class="dialog" style="display: none">
|
||||
|
|
@ -2122,9 +2127,30 @@
|
|||
|
||||
<button id="riverEditStyle" data-tip="Edit style for all rivers in Style Editor" class="icon-brush"></button>
|
||||
<button id="riverLength" data-tip="River length in selected units">0</button>
|
||||
<button id="riverElevationProfile" data-tip="Show the elevation profile for the river" class="icon-chart-area"></button>
|
||||
<button id="riverNew" data-tip="Create new river clicking on map" class="icon-map-pin"></button>
|
||||
<button id="riverLegend" data-tip="Edit free text notes (legend) for the river" class="icon-edit"></button>
|
||||
<button id="riverRemove" data-tip="Remove river. Shortcut: Delete" class="icon-trash"></button>
|
||||
<button id="riverRemove" data-tip="Remove river. Shortcut: Delete" class="icon-trash fastDelete"></button>
|
||||
</div>
|
||||
|
||||
<div id="elevationProfile" class="dialog" style="display: none" width="100%">
|
||||
<div id="elevationGraph" data-tip="Elevation profile"></div>
|
||||
<div style="text-align: center">
|
||||
<div id="epControls">
|
||||
<span data-tip="Set height scale">Height scale: <input id="epScaleRange" type="range" min=1 max=100 value=50></span>
|
||||
<span data-tip="Set curve profile">Curve:
|
||||
<select id="epCurve">
|
||||
<option>Linear</option>
|
||||
<option selected>Basis spline</option>
|
||||
<option>Bundle</option>
|
||||
<option>Cubic Catmull-Rom</option>
|
||||
<option>Monotone X</option>
|
||||
<option>Natural</option>
|
||||
</select>
|
||||
</span>
|
||||
<span><button id="epSave" data-tip="Download the chart data as a CSV file" class="icon-download"></button></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="routeEditor" class="dialog" style="display: none">
|
||||
|
|
@ -2139,10 +2165,11 @@
|
|||
|
||||
<button id="routeEditStyle" data-tip="Edit route group style in Style Editor" class="icon-brush"></button>
|
||||
<button id="routeLength" data-tip="Route length in selected units">0</button>
|
||||
<button id="routeElevationProfile" data-tip="Show the elevation profile for the route" class="icon-chart-area"></button>
|
||||
<button id="routeSplit" data-tip="Click on a control point to split the route" class="icon-unlink"></button>
|
||||
<button id="routeLegend" data-tip="Edit free text notes (legend) for the route" class="icon-edit"></button>
|
||||
<button id="routeNew" data-tip="Create new route clicking on map" class="icon-map-pin"></button>
|
||||
<button id="routeRemove" data-tip="Remove route. Shortcut: Delete" class="icon-trash"></button>
|
||||
<button id="routeRemove" data-tip="Remove route. Shortcut: Delete" class="icon-trash fastDelete"></button>
|
||||
</div>
|
||||
|
||||
<div id="lakeEditor" class="dialog" style="display: none">
|
||||
|
|
@ -2160,6 +2187,14 @@
|
|||
<button id="lakeLegend" data-tip="Edit free text notes (legend) for the lake" class="icon-edit"></button>
|
||||
</div>
|
||||
|
||||
<div id="iceEditor" class="dialog" style="display: none">
|
||||
<button id="iceEditStyle" data-tip="Edit style in Style Editor" class="icon-brush"></button>
|
||||
<button id="iceRandomize" data-tip="Randomize Iceberd shape" class="icon-shuffle"></button>
|
||||
<input id="iceSize" data-tip="Change Iceberg size" type="range" min=".05" max="1" step=".01">
|
||||
<button id="iceNew" data-tip="Add an Iceberg (click on map)" class="icon-plus"></button>
|
||||
<button id="iceRemove" data-tip="Remove the element. Shortcut: Delete" class="icon-trash fastDelete"></button>
|
||||
</div>
|
||||
|
||||
<div id="coastlineEditor" class="dialog" style="display: none">
|
||||
<button id="coastlineGroupsShow" data-tip="Show the group selection" class="icon-tags"></button>
|
||||
<div id="coastlineGroupsSelection" style="display: none">
|
||||
|
|
@ -2171,7 +2206,7 @@
|
|||
</div>
|
||||
|
||||
<button id="coastlineEditStyle" data-tip="Edit coastline group style in Style Editor" class="icon-brush"></button>
|
||||
<button id="coastlineArea" data-tip="Lake area in selected units">0</button>
|
||||
<button id="coastlineArea" data-tip="Landmass area in selected units">0</button>
|
||||
</div>
|
||||
|
||||
<div id="reliefEditor" class="dialog" style="display: none">
|
||||
|
|
@ -2303,7 +2338,7 @@
|
|||
<button id="reliefCopy" data-tip="Copy selected relief icon" class="icon-clone"></button>
|
||||
<button id="reliefMoveFront" data-tip="Move selected relief icon to front" class="icon-level-up"></button>
|
||||
<button id="reliefMoveBack" data-tip="Move selected relief icon back" class="icon-level-down"></button>
|
||||
<button id="reliefRemove" data-tip="Remove selected relief icon. Shortcut: Delete" class="icon-trash"></button>
|
||||
<button id="reliefRemove" data-tip="Remove selected relief icon. Shortcut: Delete" class="icon-trash fastDelete"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -2356,7 +2391,7 @@
|
|||
<button id="burgOpenCOA" data-tip="Open burg's COA in the Iron Arachne Heraldry Generator. Ctrl + click to change the seed" class="icon-shield-alt"></button>
|
||||
<button id="burgRelocate" data-tip="Relocate burg" class="icon-target"></button>
|
||||
<button id="burglLegend" data-tip="Edit free text notes (legend) for this burg" class="icon-edit"></button>
|
||||
<button id="burgRemove" data-tip="Remove non-capital burg. Shortcut: Delete" class="icon-trash"></button>
|
||||
<button id="burgRemove" data-tip="Remove non-capital burg. Shortcut: Delete" class="icon-trash fastDelete"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -2373,15 +2408,13 @@
|
|||
<button id="markerIcon" data-tip="Change marker icon and edit positioning" class="icon-star"></button>
|
||||
<div id="markerIconSection" style="display: none">
|
||||
<i data-tip="Change marker icon size" class="icon-resize-full"></i>
|
||||
<input id="markerIconSize" data-tip="Change marker icon size" type="range" min=10 max=30 step=.5 value=22 style="width:12em">
|
||||
<input id="markerIconSize" data-tip="Change marker icon size" type="range" min=5 max=30 step=.5 value=22 style="width:12em"><br>
|
||||
<i data-tip="Marker Icon" class="icon-info"></i>
|
||||
<button id="markerIconSelect" data-tip="Click to select icon"></button>
|
||||
<i data-tip="Change marker horizontal shift" class="icon-resize-horizontal"></i>
|
||||
<input id="markerIconShiftX" data-tip="Change icon horizontal shift" type="number" value=50 style="width:3em">
|
||||
<i data-tip="Change marker vertical shift" class="icon-resize-vertical"></i>
|
||||
<input id="markerIconShiftY" data-tip="Change vertical shift" type="number" min=0 max=100 value=50 style="width:3em">
|
||||
<span data-tip="Paste custom unicode symbol to use as icon">Paste here:</span>
|
||||
<input id="markerIconCustom" data-tip="Paste custom unicode symbol to use as icon" style="width:3em">
|
||||
<table id="markerIconTable"></table>
|
||||
<div style="font-style: italic; color: darkgrey;">Emojis look different in different browsers. Visit <a href="https://emojipedia.org/" rel="noopener" target="_blank">Emojipedia</a> to check and find more</div>
|
||||
</div>
|
||||
|
||||
<button id="markerStyle" data-tip="Change marker size and colors" class="icon-brush"></button>
|
||||
|
|
@ -2399,7 +2432,7 @@
|
|||
<button id="markerToggleBubble" data-tip="Toggle pin (bubble) display" class="icon-info-circled"></button>
|
||||
<button id="markerLegendButton" data-tip="Edit place legend (free text notes)" class="icon-edit"></button>
|
||||
<button id="markerAdd" data-tip="Add additional marker of that type" class="icon-plus"></button>
|
||||
<button id="markerRemove" data-tip="Remove the marker. Shortcut: Delete" class="icon-trash"></button>
|
||||
<button id="markerRemove" data-tip="Remove the marker. Shortcut: Delete" class="icon-trash fastDelete"></button>
|
||||
</div>
|
||||
|
||||
<div id="regimentEditor" class="dialog" style="display: none">
|
||||
|
|
@ -2417,19 +2450,157 @@
|
|||
<button id="regimentEmblemSelect" style="padding: 0; width: 4.5em">select</button>
|
||||
</div>
|
||||
|
||||
<div id="regimentComposition" style="padding: .1em"></div>
|
||||
<div id="regimentComposition" class="table"></div>
|
||||
</div>
|
||||
|
||||
<div id="regimentBottom">
|
||||
<button id="regimentAttack" data-tip="Attack foreign regiment" class="icon-target"></button>
|
||||
<button id="regimentAdd" data-tip="Create new regiment or fleet" class="icon-user-plus"></button>
|
||||
<button id="regimentSplit" data-tip="Split regiment into 2 separate ones" class="icon-half"></button>
|
||||
<button id="regimentAttach" data-tip="Attach regiment to another one (include this regiment to another one)" class="icon-attach"></button>
|
||||
<button id="regimentRegenerateLegend" data-tip="Regenerate legend for this regiment" class="icon-retweet"></button>
|
||||
<button id="regimentLegend" data-tip="Edit free text notes (legend) for this regiment" class="icon-edit"></button>
|
||||
<button id="regimentRemove" data-tip="Remove regiment. Shortcut: Delete" class="icon-trash"></button>
|
||||
<button id="regimentRemove" data-tip="Remove regiment. Shortcut: Delete" class="icon-trash fastDelete"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="battleScreen" class="dialog stable" style="display: none">
|
||||
<div id="battleBody" class="overflow">
|
||||
<template id="battlePhases_field">
|
||||
<button data-tip="Skirmish phase. Ranged units excel" data-phase="skirmish" class="icon-button-skirmish"></button>
|
||||
<button data-tip="Melee phase. Melee units excel" data-phase="melee" class="icon-button-melee"></button>
|
||||
<button data-tip="Pursue phase. Mounted units excel" data-phase="pursue" class="icon-button-pursue"></button>
|
||||
<button data-tip="Retreat phase. Units strength reduced" data-phase="retreat" class="icon-button-retreat"></button>
|
||||
</template>
|
||||
|
||||
<template id="battlePhases_naval">
|
||||
<button data-tip="Shelling phase. Naval artillery bombardment of enemy fleet" data-phase="shelling" class="icon-button-shelling"></button>
|
||||
<button data-tip="Boarding phase. Melee units go aboard" data-phase="boarding" class="icon-button-boarding"></button>
|
||||
<button data-tip="Сhase phase. Naval units pursue and rarely shell enemy fleet" data-phase="chase" class="icon-button-chase"></button>
|
||||
<button data-tip="Withdrawal phase. Naval units try to escape enemy fleet" data-phase="withdrawal" class="icon-button-withdrawal"></button>
|
||||
</template>
|
||||
|
||||
<template id="battlePhases_siege_attackers">
|
||||
<button data-tip="Blockade phase. Prepare or hold the blockade" data-phase="blockade" class="icon-button-blockade"></button>
|
||||
<button data-tip="Bombardment phase. Attack enemy with machinery units" data-phase="bombardment" class="icon-button-bombardment"></button>
|
||||
<button data-tip="Storming phase. Storm enemy town. Melee units excel" data-phase="storming" class="icon-button-storming"></button>
|
||||
<button data-tip="Looting phase. Plunder the town. Units strength increased" data-phase="looting" class="icon-button-looting"></button>
|
||||
<button data-tip="Retreat phase. Units strength reduced" data-phase="retreat" class="icon-button-retreat"></button>
|
||||
</template>
|
||||
|
||||
<template id="battlePhases_siege_defenders">
|
||||
<button data-tip="Sheltering phase. Hide behind the walls and wait" data-phase="sheltering" class="icon-button-sheltering"></button>
|
||||
<button data-tip="Sortie phase. Make a sortie from besieged town. Melee units excel" data-phase="sortie" class="icon-button-sortie"></button>
|
||||
<button data-tip="Bombardment phase. Attack enemy with machinery units" data-phase="bombardment" class="icon-button-bombardment"></button>
|
||||
<button data-tip="Defense phase. Ranged and melee units excel" data-phase="defense" class="icon-button-defense"></button>
|
||||
<button data-tip="Surrendering phase. Give up the defense. Units strength reduced" data-phase="surrendering" class="icon-button-surrendering"></button>
|
||||
<button data-tip="Pursue phase. Mounted units excel" data-phase="pursue" class="icon-button-pursue"></button>
|
||||
</template>
|
||||
|
||||
<template id="battlePhases_ambush_attackers">
|
||||
<button data-tip="Shock phase. Units strength reduced" data-phase="shock" class="icon-button-shock"></button>
|
||||
<button data-tip="Melee phase. Melee units excel" data-phase="melee" class="icon-button-melee"></button>
|
||||
<button data-tip="Pursue phase. Mounted units excel" data-phase="pursue" class="icon-button-pursue"></button>
|
||||
<button data-tip="Retreat phase. Units strength reduced" data-phase="retreat" class="icon-button-retreat"></button>
|
||||
</template>
|
||||
|
||||
<template id="battlePhases_ambush_defenders">
|
||||
<button data-tip="Surprice attack phase. Units strength increased, ranged units excel" data-phase="surprise" class="icon-button-surprise"></button>
|
||||
<button data-tip="Melee phase. Melee units excel" data-phase="melee" class="icon-button-melee"></button>
|
||||
<button data-tip="Pursue phase. Mounted units excel" data-phase="pursue" class="icon-button-pursue"></button>
|
||||
<button data-tip="Retreat phase. Units strength reduced" data-phase="retreat" class="icon-button-retreat"></button>
|
||||
</template>
|
||||
|
||||
<template id="battlePhases_landing_attackers">
|
||||
<button data-tip="Landing phase. Amphibious attack. Units are vulnerable against prepared defense" data-phase="landing" class="icon-button-landing"></button>
|
||||
<button data-tip="Melee phase. Melee units excel" data-phase="melee" class="icon-button-melee"></button>
|
||||
<button data-tip="Pursue phase. Mounted units excel" data-phase="pursue" class="icon-button-pursue"></button>
|
||||
<button data-tip="Flee phase. Units strength reduced" data-phase="flee" class="icon-button-flee"></button>
|
||||
</template>
|
||||
|
||||
<template id="battlePhases_landing_defenders">
|
||||
<button data-tip="Shock phase. Units are not prepared for a defense" data-phase="shock" class="icon-button-shock"></button>
|
||||
<button data-tip="Defense phase. Prepared defense. Units strength increased" data-phase="defense" class="icon-button-defense"></button>
|
||||
<button data-tip="Melee phase. Melee units excel" data-phase="melee" class="icon-button-melee"></button>
|
||||
<button data-tip="Waiting phase. Cannot pursue fleeing naval" data-phase="waiting" class="icon-button-waiting"></button>
|
||||
<button data-tip="Pursue phase. Try to intercept fleeing attackers. Mounted units excel" data-phase="pursue" class="icon-button-pursue"></button>
|
||||
<button data-tip="Retreat phase. Units strength reduced" data-phase="retreat" class="icon-button-retreat"></button>
|
||||
</template>
|
||||
|
||||
<template id="battlePhases_air">
|
||||
<button data-tip="Maneuvering phase. Units strength reduced" data-phase="maneuvering" class="icon-button-maneuvering"></button>
|
||||
<button data-tip="Dogfight phase. Units strength increased" data-phase="dogfight" class="icon-button-dogfight"></button>
|
||||
<button data-tip="Pursue phase. Units strength increased" data-phase="pursue" class="icon-button-pursue"></button>
|
||||
<button data-tip="Retreat phase. Units strength reduced" data-phase="retreat" class="icon-button-retreat"></button>
|
||||
</template>
|
||||
|
||||
<div style="font-size:1.2em; font-weight: bold; width: unset">
|
||||
<span>Attackers</span>
|
||||
<div style="float: right; font-size: .7em">
|
||||
<meter id="battleMorale_attackers" data-tip="Attackers morale: " min=0 max=100 low=33 high=66 optimum=80></meter>
|
||||
<div id="battlePower_attackers" data-tip="Attackers strength during this phase. Strength defines dealt damage" style="width: 3.2em; display: inline-block; text-align: center" class="icon-button-power"></div>
|
||||
<div style="display: inline-block;">
|
||||
<button id="battlePhase_attackers" style="width: 3.2em"></button>
|
||||
<div class="battlePhases" style="display: none"></div>
|
||||
</div>
|
||||
<button id="battleDie_attackers" data-tip="Random factor for attackers. Click to re-roll" style="padding: .1em .2em; width: 3.2em" class="icon-button-die"></button>
|
||||
</div>
|
||||
</div>
|
||||
<table id="battleAttackers"></table>
|
||||
<div style="font-size:1.2em; font-weight: bold; width: unset">
|
||||
<span></span>Defenders</span>
|
||||
<div style="float: right; font-size: .7em">
|
||||
<meter id="battleMorale_defenders" data-tip="Defenders morale: " min=0 max=100 low=33 high=66 optimum=80></meter>
|
||||
<div id="battlePower_defenders" data-tip="Defenders strength during this phase. Strength defines dealt damage" style="width: 3.2em; display: inline-block; text-align: center" class="icon-button-power"></div>
|
||||
<div style="display: inline-block;">
|
||||
<button id="battlePhase_defenders" style="width: 3.2em"></button>
|
||||
<div class="battlePhases" style="display: none"></div>
|
||||
</div>
|
||||
<button id="battleDie_defenders" data-tip="Random factor for defenders. Click to re-roll" style="padding: .1em .2em; width: 3.2em" class="icon-button-die"></button>
|
||||
</div>
|
||||
</div>
|
||||
<table id="battleDefenders"></table>
|
||||
</div>
|
||||
|
||||
<div id="battleBottom">
|
||||
<button id="battleType" data-tip="Battle type. Click to change"></button>
|
||||
<div class="battleTypes" style="display: none">
|
||||
<button data-tip="Field Battle: a standard type of combat" data-type="field" class="icon-button-field"></button>
|
||||
<button data-tip="Naval Battle: naval units combat" data-type="naval" class="icon-button-naval"></button>
|
||||
<button data-tip="Siege: burg blockade and storming" data-type="siege" class="icon-button-siege"></button>
|
||||
<button data-tip="Ambush: surprise attack" data-type="ambush" class="icon-button-ambush"></button>
|
||||
<button data-tip="Landing: amphibious attack" data-type="landing" class="icon-button-landing"></button>
|
||||
<button data-tip="Air Battle: maneuring fight of avia units" data-type="air" class="icon-button-air"></button>
|
||||
</div>
|
||||
|
||||
<button id="battleNameShow" data-tip="Set battle name" class="icon-font"></button>
|
||||
<div id="battleNameSection" style="display: none">
|
||||
<button id="battleNameHide" data-tip="Hide the battle name section" class="icon-font"></button>
|
||||
<input id="battleNamePlace" data-tip="Type place name"" style="width: 30%">
|
||||
<input id="battleNameFull" data-tip="Type full battle name"" style="width: 46%">
|
||||
<button id="battleNameCulture" data-tip="Generate culture-specific name for place and battle" class="icon-book"></button>
|
||||
<button id="battleNameRandom" data-tip="Generate random name for place and battle" class="icon-globe"></button>
|
||||
</div>
|
||||
|
||||
<button id="battleAddRegiment" data-tip="Add regiment to the battle" class="icon-user-plus"></button>
|
||||
<button id="battleRoll" data-tip="Roll dice to update random factor" class="icon-die"></button>
|
||||
<button id="battleRun" data-tip="Iterate battle" class="icon-play"></button>
|
||||
<button id="battleApply" data-tip="End battle: apply current results and close the screen" class="icon-check"></button>
|
||||
<button id="battleCancel" data-tip="Cancel battle: roll back results and close the screen" class="icon-cancel"></button>
|
||||
<button id="battleWiki" data-tip="Open Battle Simulation Tutorial" class="icon-info"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="regimentSelectorScreen" class="dialog" style="display: none">
|
||||
<div id="regimentSelectorHeader" class="header">
|
||||
<div style="left: 1.2em;" data-tip="Click to sort by state name" class="sortable alphabetically" data-sortby="state">State </div>
|
||||
<div style="left: 9.2em;" data-tip="Click to sort by regiment name" class="sortable alphabetically" data-sortby="regiment">Regiment </div>
|
||||
<div style="left: 22.4em;" data-tip="Click to sort by total military forces" class="sortable" data-sortby="total">Total </div>
|
||||
<div style="left: 28em;" data-tip="Click to sort by distance to the battlefield" class="sortable icon-sort-number-up" data-sortby="distance">Distance </div>
|
||||
</div>
|
||||
<div id="regimentSelectorBody"></div>
|
||||
</div>
|
||||
|
||||
<div id="brushesPanel" class="dialog stable" style="display: none">
|
||||
<div id="brushesButtons" style="display: inline-block">
|
||||
<button id="brushRaise" data-tip="Raise brush: increase height of cells in radius by Power value">
|
||||
|
|
@ -2573,7 +2744,7 @@
|
|||
<button id="templateSave" data-tip="Download the template as a text file" class="icon-download"></button>
|
||||
<button id="templateLoad" data-tip="Open previously downloaded template" class="icon-upload"></button>
|
||||
<button id="templateCA" data-tip="Find or share custom template on Cartography Assets portal" class="icon-drafting-compass" onclick="openURL('https://www.cartographyassets.com/assets/categories/templates.89')"></button>
|
||||
<button id="templateTutorial" data-tip="Open Template Editor Tutorial" class="icon-info" onclick="openURL('https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Heightmap-template-editor')"></button>
|
||||
<button id="templateTutorial" data-tip="Open Template Editor Tutorial" class="icon-info" onclick="wiki('Heightmap-template-editor')"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -2581,32 +2752,33 @@
|
|||
|
||||
<div id="convertImageButtons">
|
||||
<button id="convertImageLoad" data-tip="Load image to convert" class="icon-upload"></button>
|
||||
<button id="convertAutoLum" data-tip="Auto-assign colors based on liminosity (good to monochrome images)" class="icon-adjust"></button>
|
||||
<button id="convertAutoHue" data-tip="Auto-assign colors based on hue (good to colored images)" class="icon-brush"></button>
|
||||
<button id="convertColorsButton" data-tip="Set number of colors" class="icon-signal"></button>
|
||||
<input id="convertColors" value="18" style="display: none"/>
|
||||
<button id="convertAutoLum" data-tip="Auto-assign colors based on liminosity (good for monochrome images)" class="icon-adjust"></button>
|
||||
<button id="convertAutoHue" data-tip="Auto-assign colors based on hue (good for colored images)" class="icon-paint-roller"></button>
|
||||
<button id="convertAutoFMG" data-tip="Auto-assign colors using generator scheme (for exported colored heightmaps)" class="icon-layer-group"></button>
|
||||
<button id="convertColorsButton" data-tip="Set maximum number of colors" class="icon-signal"></button>
|
||||
<input id="convertColors" value="100" style="display: none"/>
|
||||
<button id="convertComplete" data-tip="Complete the conversion. All unassigned colors will be considered as ocean" class="icon-check"></button>
|
||||
<button id="convertCancel" data-tip="Cancel the conversion. Previous heoghtmap will be restored" class="icon-cancel"></button>
|
||||
<button id="convertCancel" data-tip="Cancel the conversion. Previous heightmap will be restored" class="icon-cancel"></button>
|
||||
</div>
|
||||
|
||||
<div data-tip="Set opacity of the loaded image" style="padding-top: 4px"><i>Overlay opacity:</i><br>
|
||||
<input id="convertOverlay" type="range" min=0 max=1 step=.01 value=0 style="width: 11.5em">
|
||||
<input id="convertOverlayNumber" type="number" min=0 max=1 step=.01 value=0 style="width: 3.5em">
|
||||
<div data-tip="Set opacity of the loaded image" style="padding-top: .4em"><i>Overlay opacity:</i><br>
|
||||
<input id="convertOverlay" type="range" min=0 max=1 step=.01 value=0 style="width: 12.6em">
|
||||
<input id="convertOverlayNumber" type="number" min=0 max=1 step=.01 value=0 style="width: 4.2em">
|
||||
</div>
|
||||
|
||||
<div data-tip="Select a color below and assign a height value for it" id="colorsSelect" style="display: none">
|
||||
<i>Set height: </i>
|
||||
<span id="colorsSelectValue"></span>
|
||||
<span>(<span id="colorsSelectFriendly">0</span>)</span><br>
|
||||
<div id="colorScheme"></div>
|
||||
<div id="imageConverterPalette"></div>
|
||||
</div>
|
||||
|
||||
<div data-tip="Select a color to re-assign the height value" id="colorsAssigned" style="display: none">
|
||||
<i>Assigned colors: </i><br>
|
||||
<i>Assigned colors (<span id="colorsAssignedNumber"></span>):</i><br>
|
||||
</div>
|
||||
|
||||
<div data-tip="Select a color to assign a height value" id="colorsUnassigned" style="display: none">
|
||||
<i>Unassigned colors: </i><br>
|
||||
<i>Unassigned colors (<span id="colorsUnassignedNumber"></span>):</i><br>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -2776,7 +2948,7 @@
|
|||
<option data-form="Union" value="United States">United States</option>
|
||||
<option data-form="Wild" value="United Tribes">United Tribes</option>
|
||||
</select>
|
||||
<input id="stateNameEditorCustomForm" placeholder="type form name" data-tip="Create custom state form name" style="display: none; width: 11em;">
|
||||
<input id="stateNameEditorCustomForm" placeholder="type form name" data-tip="Enter custom form name" style="display: none; width: 11em;">
|
||||
<span id="stateNameEditorAddForm" data-tip="Click to add custom state form name to the list" class="icon-plus pointer"></span>
|
||||
</div>
|
||||
|
||||
|
|
@ -2813,6 +2985,7 @@
|
|||
<div id="provincesBottom">
|
||||
<button id="provincesEditorRefresh" data-tip="Refresh the Editor" class="icon-cw"></button>
|
||||
<button id="provincesEditStyle" data-tip="Edit provinces style in Style Editor" class="icon-adjust"></button>
|
||||
<button id="provincesRecolor" data-tip="Recolor listed provinces based on state color" class="icon-paint-roller"></button>
|
||||
<button id="provincesPercentage" data-tip="Toggle percentage / absolute values views" class="icon-percent"></button>
|
||||
<button id="provincesChart" data-tip="Show provinces chart" class="icon-chart-area"></button>
|
||||
<button id="provincesToggleLabels" data-tip="Toggle province labels" class="icon-font"></button>
|
||||
|
|
@ -2839,7 +3012,7 @@
|
|||
<div id="diplomacyEditor" class="dialog stable" style="display: none">
|
||||
<div id="diplomacyHeader" class="header">
|
||||
<div style="left:.2em" data-tip="Click to sort by state name" class="sortable alphabetically" data-sortby="name">State </div>
|
||||
<div style="left:12.4em" data-tip="Click to sort by diplomatical relations" class="sortable alphabetically" data-sortby="relations">Relations </div>
|
||||
<div style="left:13.4em" data-tip="Click to sort by diplomatical relations" class="sortable alphabetically" data-sortby="relations">Relations </div>
|
||||
</div>
|
||||
|
||||
<div id="diplomacyBodySection" class="table"></div>
|
||||
|
|
@ -2973,9 +3146,7 @@
|
|||
<input id="namesbaseMin" data-tip="Recommended minimum name length" type="number" min=2 max=100>
|
||||
<input id="namesbaseMax" data-tip="Recommended maximum name length" type="number" min=2 value=10>
|
||||
<span>Double: </span>
|
||||
<input id="namesbaseDouble" data-tip="Populate with letters that can used twice in a row" autocorrect="off" spellcheck="false" style="width:5em">
|
||||
<span>Multi: </span>
|
||||
<input id="namesbaseMulti" data-tip="Multi-word names rate. 1 - allow all multi-word names, 0 - all names should spelled as a single word" type="number" min=0 max=1 step=.1 value=0 style="width:3.6em">
|
||||
<input id="namesbaseDouble" data-tip="Populate with letters that can used twice in a row (geminates)" autocorrect="off" spellcheck="false" style="width:10em">
|
||||
</div>
|
||||
<fieldset>
|
||||
<legend>Generated examples: </legend>
|
||||
|
|
@ -3048,7 +3219,7 @@
|
|||
<button id="notesFocus" data-tip="Focus on selected object" class="icon-target"></button>
|
||||
<button id="notesDownload" data-tip="Download notes to PC" class="icon-download"></button>
|
||||
<button id="notesUpload" data-tip="Upload notes from PC" class="icon-upload"></button>
|
||||
<button id="notesRemove" data-tip="Remove this note" class="icon-trash"></button>
|
||||
<button id="notesRemove" data-tip="Remove this note" class="icon-trash fastDelete"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -3203,7 +3374,7 @@
|
|||
<input id="populationRate" data-stored="populationRate" type="number" min=10 max=9990 step=10 value=1000 data-value=1000 style="width:4.5em">
|
||||
</div>
|
||||
|
||||
<div data-tip="Set ubranization rate: burgs population relative to all population">
|
||||
<div data-tip="Set urbranization rate: burgs population relative to all population">
|
||||
<div>Urbanization rate:</div>
|
||||
<input id="urbanizationOutput" type="range" min=.01 max=5 step=.01 value=1>
|
||||
<input id="urbanization" data-stored="urbanization" type="number" min=.01 max=5 step=.01 value=1 data-value=1>
|
||||
|
|
@ -3279,16 +3450,18 @@
|
|||
</div>
|
||||
|
||||
<div id="militaryOverview" class="dialog stable" style="display: none">
|
||||
<div id="militaryHeader" class="header">
|
||||
<div data-tip="State name. Click to sort" style="left:1.8em; width: 7.4em;" class="sortable alphabetically" data-sortby="state">State </div>
|
||||
<div data-tip="Total military personnel (considering crew). Click to sort" id="militaryTotal" class="sortable icon-sort-number-down" data-sortby="total">Total </div>
|
||||
<div data-tip="State population. Click to sort" style="width: 6.5em; margin-left: -1em" class="sortable" data-sortby="population">Population </div>
|
||||
<div data-tip="Military personnel rate (% of state population). Depends on war alert. Click to sort" style="width: 3.7em" class="sortable" data-sortby="rate">Rate </div>
|
||||
<div data-tip="War Alert. Modifier to military forces number, depends of political situation. Click to sort" class="sortable" data-sortby="alert">War Alert </div>
|
||||
<div class="overflow">
|
||||
<div id="militaryHeader" class="header">
|
||||
<div data-tip="State name. Click to sort" style="margin-left: 1.8em; width: 5.6em" class="sortable alphabetically" data-sortby="state">State </div>
|
||||
<div data-tip="Total military personnel (considering crew). Click to sort" id="militaryTotal" class="sortable icon-sort-number-down" data-sortby="total">Total </div>
|
||||
<div data-tip="State population. Click to sort" style="width: 6.5em; margin-left: -1em" class="sortable" data-sortby="population">Population </div>
|
||||
<div data-tip="Military personnel rate (% of state population). Depends on war alert. Click to sort" style="width: 3.7em" class="sortable" data-sortby="rate">Rate </div>
|
||||
<div data-tip="War Alert. Modifier to military forces number, depends of political situation. Click to sort" class="sortable" data-sortby="alert">War Alert </div>
|
||||
</div>
|
||||
|
||||
<div id="militaryBody" data-type="absolute"></div>
|
||||
</div>
|
||||
|
||||
<div id="militaryBody" class="table" data-type="absolute"></div>
|
||||
|
||||
<div id="militaryFooter" class="totalLine">
|
||||
<div data-tip="States number" style="margin-left: 4px">States: <span id="militaryFooterStates">0</span></div>
|
||||
<div data-tip="Total military forces" style="margin-left: 14px">Total forces: <span id="militaryFooterForcesTotal">0</span></div>
|
||||
|
|
@ -3304,18 +3477,21 @@
|
|||
<button id="militaryPercentage" data-tip="Toggle percentage / absolute values views" class="icon-percent"></button>
|
||||
<button id="militaryOverviewRecalculate" data-tip="Recalculate military forces based on current options" class="icon-retweet"></button>
|
||||
<button id="militaryExport" data-tip="Save military-related data as a text file (.csv)" class="icon-download"></button>
|
||||
<button id="militaryWiki" data-tip="Open Military Forces Tutorial" class="icon-info"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="regimentsOverview" class="dialog stable" style="display: none">
|
||||
<div id="regimentsHeader" class="header">
|
||||
<div data-tip="State name. Click to sort" style="left:1.8em; width: 9em" class="sortable alphabetically" data-sortby="state">State </div>
|
||||
<div data-tip="Regiment emblem and name. Click to sort by name" style="width: 12em" class="sortable alphabetically" data-sortby="name">Name </div>
|
||||
<div data-tip="Total military personnel (not considering crew). Click to sort" style="margin-left: .8em" id="regimentsTotal" class="sortable icon-sort-number-down" data-sortby="total">Total </div>
|
||||
<div class="overflow">
|
||||
<div id="regimentsHeader" class="header">
|
||||
<div data-tip="State name. Click to sort" style="left:1.8em; width: 9em" class="sortable alphabetically" data-sortby="state">State </div>
|
||||
<div data-tip="Regiment emblem and name. Click to sort by name" style="width: 12em" class="sortable alphabetically" data-sortby="name">Name </div>
|
||||
<div data-tip="Total military personnel (not considering crew). Click to sort" style="margin-left: .8em" id="regimentsTotal" class="sortable icon-sort-number-down" data-sortby="total">Total </div>
|
||||
</div>
|
||||
|
||||
<div id="regimentsBody" data-type="absolute"></div>
|
||||
</div>
|
||||
|
||||
<div id="regimentsBody" class="table" data-type="absolute"></div>
|
||||
|
||||
<div id="regimentsBottom">
|
||||
<button id="regimentsOverviewRefresh" data-tip="Refresh the overview screen" class="icon-cw"></button>
|
||||
<button id="regimentsPercentage" data-tip="Toggle percentage / absolute values views" class="icon-percent"></button>
|
||||
|
|
@ -3326,20 +3502,24 @@
|
|||
</div>
|
||||
|
||||
<div id="militaryOptions" class="dialog stable" style="display: none">
|
||||
<table id="militaryOptionsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-tip="Unit name. If name is changed for existing unit, old unit will be replaced">Military unit</th>
|
||||
<th data-tip="Conscription percentage for rural population">Rural %</th>
|
||||
<th data-tip="Conscription percentage for urban population">Urban %</th>
|
||||
<th data-tip="Average number of people in crew">Crew</th>
|
||||
<th data-tip="Unit type to apply special rules on forces recalculation">Type</th>
|
||||
<th data-tip="Check if unit is separate and can be stacked only with units of the same type">Sep.</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="table">
|
||||
<table id="militaryOptionsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-tip="Unit icon">Icon</th>
|
||||
<th data-tip="Unit name. If name is changed for existing unit, old unit will be replaced">Unit name</th>
|
||||
<th data-tip="Conscription percentage for rural population">Rural</th>
|
||||
<th data-tip="Conscription percentage for urban population">Urban</th>
|
||||
<th data-tip="Average number of people in crew (used for total personnel calculation)">Crew</th>
|
||||
<th data-tip="Unit military power (used for battle simulation)">Power</th>
|
||||
<th data-tip="Unit type to apply special rules on forces recalculation">Type</th>
|
||||
<th data-tip="Check if unit is separate and can be stacked only with units of the same type">Sep.</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="styleSaver" class="dialog stable textual" style="display: none">
|
||||
|
|
@ -3382,6 +3562,15 @@
|
|||
<p><b>Burg:</b> <span id="infoBurg">n/a</span></p>
|
||||
</div>
|
||||
|
||||
<div id="iconSelector" style="display: none" class="dialog">
|
||||
<table id="iconTable" class="table pointer" style="font-size: 2em; text-align: center"></table>
|
||||
<div style="font-style: italic; font-size: 1.2em; margin: .4em 0 0 .4em">
|
||||
<span>Select from the list or paste a Unicode character here: </span>
|
||||
<input id="iconInput" style="width: 2em">
|
||||
<span>. See <a href="https://emojipedia.org" target="_blank">Emojipedia</a> for reference</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="options3d" class="dialog stable" style="display: none">
|
||||
|
||||
<div id="options3dMesh" style="display: none">
|
||||
|
|
@ -3451,7 +3640,7 @@
|
|||
<div id="preview3d" class="dialog stable" style="display: none; padding: 0px"></div>
|
||||
|
||||
<div id="saveMapData" style="display: none" class="dialog">
|
||||
<div style="margin-bottom: .3em">Please select a saving variant:</div>
|
||||
<div style="margin-bottom: .3em; font-weight: bold">Please select saving variant:</div>
|
||||
<div>
|
||||
<button onclick="saveMap()" data-tip="Download the map as fully-functional .map file to your machine">.map</button>
|
||||
<button onclick="saveSVG()" data-tip="Download the map as vector image (open in browser or Inkscape)">.svg</button>
|
||||
|
|
@ -3461,6 +3650,11 @@
|
|||
<button onclick="quickSave()" data-tip="Save fully-functional map to browser storage. Shortcut: F6">storage</button>
|
||||
</div>
|
||||
<p style="font-style: italic">Generator uses pop-up window to download files. Please ensure your browser does not block popups</p>
|
||||
<div data-tip="Define scale of a saved png/jpeg image (e.g. 5x). Saving big images is slow and may cause a browser crash!" style="margin-bottom: .3em">
|
||||
PNG / JPEG scale:
|
||||
<input id="pngResolutionInput" data-stored="pngResolution" type="range" min=1 max=8 value=1 style="width: 10.8em">
|
||||
<input id="pngResolutionOutput" data-stored="pngResolution" type="number" min=1 max=8 value=1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loadMapData" style="display: none" class="dialog">
|
||||
|
|
@ -3523,6 +3717,7 @@
|
|||
<script src="modules/religions-generator.js"></script>
|
||||
<script src="modules/military-generator.js"></script>
|
||||
<script src="libs/polylabel.min.js"></script>
|
||||
<script src="libs/lineclip.js"></script>
|
||||
<script src="libs/jquery-ui.min.js"></script>
|
||||
<script src="libs/seedrandom.min.js"></script>
|
||||
<script src="modules/ui/layers.js"></script>
|
||||
|
|
@ -3532,7 +3727,7 @@
|
|||
<script defer src="modules/ui/style.js"></script>
|
||||
<script defer src="modules/ui/measurers.js"></script>
|
||||
<script defer src="modules/save-and-load.js"></script>
|
||||
<script defer src="main.js?version=1.0"></script>
|
||||
<script defer src="main.js?version=1.4"></script>
|
||||
<script defer src="modules/relief-icons.js"></script>
|
||||
<script defer src="modules/ui/tools.js"></script>
|
||||
<script defer src="modules/ui/world-configurator.js"></script>
|
||||
|
|
@ -3542,7 +3737,9 @@
|
|||
<script defer src="modules/ui/biomes-editor.js"></script>
|
||||
<script defer src="modules/ui/cultures-editor.js"></script>
|
||||
<script defer src="modules/ui/namesbase-editor.js"></script>
|
||||
<script defer src="modules/ui/elevation-profile.js"></script>
|
||||
<script defer src="modules/ui/routes-editor.js"></script>
|
||||
<script defer src="modules/ui/ice-editor.js"></script>
|
||||
<script defer src="modules/ui/lakes-editor.js"></script>
|
||||
<script defer src="modules/ui/coastline-editor.js"></script>
|
||||
<script defer src="modules/ui/labels-editor.js"></script>
|
||||
|
|
@ -3560,9 +3757,10 @@
|
|||
<script defer src="modules/ui/military-overview.js"></script>
|
||||
<script defer src="modules/ui/regiments-overview.js"></script>
|
||||
<script defer src="modules/ui/regiment-editor.js"></script>
|
||||
<script defer src="modules/ui/battle-screen.js"></script>
|
||||
<script defer src="modules/ui/editors.js"></script>
|
||||
<script defer src="modules/ui/3d.js"></script>
|
||||
<script defer src="libs/quantize.min.js"></script>
|
||||
<script defer src="libs/rgbquant.js"></script>
|
||||
<script defer src="libs/jquery.ui.touch-punch.min.js"></script>
|
||||
<script defer src="libs/publicstorage.js"></script>
|
||||
</body>
|
||||
|
|
|
|||
2
libs/jquery-ui.min.js
vendored
2
libs/jquery-ui.min.js
vendored
File diff suppressed because one or more lines are too long
103
libs/lineclip.js
Normal file
103
libs/lineclip.js
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
'use strict';
|
||||
// lineclip by mourner, https://github.com/mapbox/lineclip
|
||||
// Cohen-Sutherland line clippign algorithm, adapted to efficiently
|
||||
// handle polylines rather than just segments
|
||||
function lineclip(points, bbox, result) {
|
||||
var len = points.length,
|
||||
codeA = bitCode(points[0], bbox),
|
||||
part = [],
|
||||
i, a, b, codeB, lastCode;
|
||||
if (!result) result = [];
|
||||
|
||||
for (i = 1; i < len; i++) {
|
||||
a = points[i - 1];
|
||||
b = points[i];
|
||||
codeB = lastCode = bitCode(b, bbox);
|
||||
|
||||
while (true) {
|
||||
if (!(codeA | codeB)) { // accept
|
||||
part.push(a);
|
||||
|
||||
if (codeB !== lastCode) { // segment went outside
|
||||
part.push(b);
|
||||
if (i < len - 1) { // start a new line
|
||||
result.push(part);
|
||||
part = [];
|
||||
}
|
||||
} else if (i === len - 1) {
|
||||
part.push(b);
|
||||
}
|
||||
break;
|
||||
|
||||
} else if (codeA & codeB) { // trivial reject
|
||||
break;
|
||||
} else if (codeA) { // a outside, intersect with clip edge
|
||||
a = intersect(a, b, codeA, bbox);
|
||||
codeA = bitCode(a, bbox);
|
||||
} else { // b outside
|
||||
b = intersect(a, b, codeB, bbox);
|
||||
codeB = bitCode(b, bbox);
|
||||
}
|
||||
}
|
||||
codeA = lastCode;
|
||||
}
|
||||
|
||||
if (part.length) result.push(part);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Sutherland-Hodgeman polygon clipping algorithm
|
||||
function polygonclip(points, bbox, secure = 0) {
|
||||
var result, edge, prev, prevInside, inter, i, p, inside;
|
||||
|
||||
// clip against each side of the clip rectangle
|
||||
for (edge = 1; edge <= 8; edge *= 2) {
|
||||
result = [];
|
||||
prev = points[points.length-1];
|
||||
prevInside = !(bitCode(prev, bbox) & edge);
|
||||
|
||||
for (i = 0; i < points.length; i++) {
|
||||
p = points[i];
|
||||
inside = !(bitCode(p, bbox) & edge);
|
||||
inter = inside !== prevInside; // segment goes through the clip window
|
||||
|
||||
const pi = intersect(prev, p, edge, bbox);
|
||||
if (inter) result.push(pi); // add an intersection point
|
||||
if (secure && inter) result.push(pi, pi); // add additional intersection points to secure correct d3 curve
|
||||
if (inside) result.push(p); // add a point if it's inside
|
||||
|
||||
prev = p;
|
||||
prevInside = inside;
|
||||
}
|
||||
points = result;
|
||||
if (!points.length) break;
|
||||
}
|
||||
//result.forEach(p => debug.append("circle").attr("cx", p[0]).attr("cy", p[1]).attr("r", .6).attr("fill", "red"));
|
||||
return result;
|
||||
}
|
||||
|
||||
// intersect a segment against one of the 4 lines that make up the bbox
|
||||
function intersect(a, b, edge, bbox) {
|
||||
return edge & 8 ? [a[0] + (b[0] - a[0]) * (bbox[3] - a[1]) / (b[1] - a[1]), bbox[3]] : // top
|
||||
edge & 4 ? [a[0] + (b[0] - a[0]) * (bbox[1] - a[1]) / (b[1] - a[1]), bbox[1]] : // bottom
|
||||
edge & 2 ? [bbox[2], a[1] + (b[1] - a[1]) * (bbox[2] - a[0]) / (b[0] - a[0])] : // right
|
||||
edge & 1 ? [bbox[0], a[1] + (b[1] - a[1]) * (bbox[0] - a[0]) / (b[0] - a[0])] : null; // left
|
||||
}
|
||||
|
||||
// bit code reflects the point position relative to the bbox:
|
||||
// left mid right
|
||||
// top 1001 1000 1010
|
||||
// mid 0001 0000 0010
|
||||
// bottom 0101 0100 0110
|
||||
function bitCode(p, bbox) {
|
||||
var code = 0;
|
||||
|
||||
if (p[0] < bbox[0]) code |= 1; // left
|
||||
else if (p[0] > bbox[2]) code |= 2; // right
|
||||
|
||||
if (p[1] < bbox[1]) code |= 4; // bottom
|
||||
else if (p[1] > bbox[3]) code |= 8; // top
|
||||
|
||||
return code;
|
||||
}
|
||||
1
libs/quantize.min.js
vendored
1
libs/quantize.min.js
vendored
|
|
@ -1 +0,0 @@
|
|||
if(!pv)var pv={map:function(r,n){var o={};return n?r.map(function(r,t){return o.index=t,n.call(o,r)}):r.slice()},naturalOrder:function(r,n){return r<n?-1:r>n?1:0},sum:function(r,n){var o={};return r.reduce(n?function(r,t,u){return o.index=u,r+n.call(o,t)}:function(r,n){return r+n},0)},max:function(r,n){return Math.max.apply(null,n?pv.map(r,n):r)}};var MMCQ=function(){var r=5,n=8-r,o=1e3,t=.75;function u(n,o,t){return(n<<2*r)+(o<<r)+t}function e(r){var n=[],o=!1;function t(){n.sort(r),o=!0}return{push:function(r){n.push(r),o=!1},peek:function(r){return o||t(),void 0===r&&(r=n.length-1),n[r]},pop:function(){return o||t(),n.pop()},size:function(){return n.length},map:function(r){return n.map(r)},debug:function(){return o||t(),n}}}function i(r,n,o,t,u,e,i){var c=this;c.r1=r,c.r2=n,c.g1=o,c.g2=t,c.b1=u,c.b2=e,c.histo=i}function c(){this.vboxes=new e(function(r,n){return pv.naturalOrder(r.vbox.count()*r.vbox.volume(),n.vbox.count()*n.vbox.volume())})}function f(r,n){if(n.count()){var o=n.r2-n.r1+1,t=n.g2-n.g1+1,e=n.b2-n.b1+1,i=pv.max([o,t,e]);if(1==n.count())return[n.copy()];var c,f,a,v,s=0,p=[],l=[];if(i==o)for(c=n.r1;c<=n.r2;c++){for(v=0,f=n.g1;f<=n.g2;f++)for(a=n.b1;a<=n.b2;a++)v+=r[u(c,f,a)]||0;s+=v,p[c]=s}else if(i==t)for(c=n.g1;c<=n.g2;c++){for(v=0,f=n.r1;f<=n.r2;f++)for(a=n.b1;a<=n.b2;a++)v+=r[u(f,c,a)]||0;s+=v,p[c]=s}else for(c=n.b1;c<=n.b2;c++){for(v=0,f=n.r1;f<=n.r2;f++)for(a=n.g1;a<=n.g2;a++)v+=r[u(f,a,c)]||0;s+=v,p[c]=s}return p.forEach(function(r,n){l[n]=s-r}),h(i==o?"r":i==t?"g":"b")}function h(r){var o,t,u,e,i,f=r+"1",a=r+"2",v=0;for(c=n[f];c<=n[a];c++)if(p[c]>s/2){for(u=n.copy(),e=n.copy(),i=(o=c-n[f])<=(t=n[a]-c)?Math.min(n[a]-1,~~(c+t/2)):Math.max(n[f],~~(c-1-o/2));!p[i];)i++;for(v=l[i];!v&&p[i-1];)v=l[--i];return u[a]=i,e[f]=u[a]+1,[u,e]}}}return i.prototype={volume:function(r){var n=this;return n._volume&&!r||(n._volume=(n.r2-n.r1+1)*(n.g2-n.g1+1)*(n.b2-n.b1+1)),n._volume},count:function(r){var n=this,o=n.histo;if(!n._count_set||r){var t,e,i,c=0;for(t=n.r1;t<=n.r2;t++)for(e=n.g1;e<=n.g2;e++)for(i=n.b1;i<=n.b2;i++)c+=o[u(t,e,i)]||0;n._count=c,n._count_set=!0}return n._count},copy:function(){var r=this;return new i(r.r1,r.r2,r.g1,r.g2,r.b1,r.b2,r.histo)},avg:function(n){var o=this,t=o.histo;if(!o._avg||n){var e,i,c,f,a=0,v=1<<8-r,s=0,p=0,l=0;for(i=o.r1;i<=o.r2;i++)for(c=o.g1;c<=o.g2;c++)for(f=o.b1;f<=o.b2;f++)a+=e=t[u(i,c,f)]||0,s+=e*(i+.5)*v,p+=e*(c+.5)*v,l+=e*(f+.5)*v;o._avg=a?[~~(s/a),~~(p/a),~~(l/a)]:[~~(v*(o.r1+o.r2+1)/2),~~(v*(o.g1+o.g2+1)/2),~~(v*(o.b1+o.b2+1)/2)]}return o._avg},contains:function(r){var o=this,t=r[0]>>n;return gval=r[1]>>n,bval=r[2]>>n,t>=o.r1&&t<=o.r2&&gval>=o.g1&&gval<=o.g2&&bval>=o.b1&&bval<=o.b2}},c.prototype={push:function(r){this.vboxes.push({vbox:r,color:r.avg()})},palette:function(){return this.vboxes.map(function(r){return r.color})},size:function(){return this.vboxes.size()},map:function(r){for(var n=this.vboxes,o=0;o<n.size();o++)if(n.peek(o).vbox.contains(r))return n.peek(o).color;return this.nearest(r)},nearest:function(r){for(var n,o,t,u=this.vboxes,e=0;e<u.size();e++)((o=Math.sqrt(Math.pow(r[0]-u.peek(e).color[0],2)+Math.pow(r[1]-u.peek(e).color[1],2)+Math.pow(r[2]-u.peek(e).color[2],2)))<n||void 0===n)&&(n=o,t=u.peek(e).color);return t},forcebw:function(){var r=this.vboxes;r.sort(function(r,n){return pv.naturalOrder(pv.sum(r.color),pv.sum(n.color))});var n=r[0].color;n[0]<5&&n[1]<5&&n[2]<5&&(r[0].color=[0,0,0]);var o=r.length-1,t=r[o].color;t[0]>251&&t[1]>251&&t[2]>251&&(r[o].color=[255,255,255])}},{quantize:function(a,v){if(v++,!a.length||v<2||v>256)return!1;var s,p,l,h,b,g,m=(s=a,g=new Array(1<<3*r),s.forEach(function(r){l=r[0]>>n,h=r[1]>>n,b=r[2]>>n,p=u(l,h,b),g[p]=(g[p]||0)+1}),g);m.forEach(function(){});var x,_,d,w,z,M,y,k,O,E,q=(x=m,z=1e6,M=0,y=1e6,k=0,O=1e6,E=0,a.forEach(function(r){_=r[0]>>n,d=r[1]>>n,w=r[2]>>n,_<z?z=_:_>M&&(M=_),d<y?y=d:d>k&&(k=d),w<O?O=w:w>E&&(E=w)}),new i(z,M,y,k,O,E,x)),A=new e(function(r,n){return pv.naturalOrder(r.count(),n.count())});function C(r,n){for(var t,u=1,e=0;e<o;)if((t=r.pop()).count()){var i=f(m,t),c=i[0],a=i[1];if(!c)return;if(r.push(c),a&&(r.push(a),u++),u>=n)return;if(e++>o)return}else r.push(t),e++}A.push(q),C(A,t*v);for(var Q=new e(function(r,n){return pv.naturalOrder(r.count()*r.volume(),n.count()*n.volume())});A.size();)Q.push(A.pop());C(Q,v-Q.size());for(var j=new c;Q.size();)j.push(Q.pop());return j}}}();
|
||||
935
libs/rgbquant.js
Normal file
935
libs/rgbquant.js
Normal file
|
|
@ -0,0 +1,935 @@
|
|||
/*
|
||||
* Copyright (c) 2015, Leon Sorokin
|
||||
* All rights reserved. (MIT Licensed)
|
||||
*
|
||||
* RgbQuant.js - an image quantization lib
|
||||
*/
|
||||
|
||||
(function(){
|
||||
function RgbQuant(opts) {
|
||||
opts = opts || {};
|
||||
|
||||
// 1 = by global population, 2 = subregion population threshold
|
||||
this.method = opts.method || 2;
|
||||
// desired final palette size
|
||||
this.colors = opts.colors || 256;
|
||||
// # of highest-frequency colors to start with for palette reduction
|
||||
this.initColors = opts.initColors || 4096;
|
||||
// color-distance threshold for initial reduction pass
|
||||
this.initDist = opts.initDist || 0.01;
|
||||
// subsequent passes threshold
|
||||
this.distIncr = opts.distIncr || 0.005;
|
||||
// palette grouping
|
||||
this.hueGroups = opts.hueGroups || 10;
|
||||
this.satGroups = opts.satGroups || 10;
|
||||
this.lumGroups = opts.lumGroups || 10;
|
||||
// if > 0, enables hues stats and min-color retention per group
|
||||
this.minHueCols = opts.minHueCols || 0;
|
||||
// HueStats instance
|
||||
this.hueStats = this.minHueCols ? new HueStats(this.hueGroups, this.minHueCols) : null;
|
||||
|
||||
// subregion partitioning box size
|
||||
this.boxSize = opts.boxSize || [64,64];
|
||||
// number of same pixels required within box for histogram inclusion
|
||||
this.boxPxls = opts.boxPxls || 2;
|
||||
// palette locked indicator
|
||||
this.palLocked = false;
|
||||
// palette sort order
|
||||
// this.sortPal = ['hue-','lum-','sat-'];
|
||||
|
||||
// dithering/error diffusion kernel name
|
||||
this.dithKern = opts.dithKern || null;
|
||||
// dither serpentine pattern
|
||||
this.dithSerp = opts.dithSerp || false;
|
||||
// minimum color difference (0-1) needed to dither
|
||||
this.dithDelta = opts.dithDelta || 0;
|
||||
|
||||
// accumulated histogram
|
||||
this.histogram = {};
|
||||
// palette - rgb triplets
|
||||
this.idxrgb = opts.palette ? opts.palette.slice(0) : [];
|
||||
// palette - int32 vals
|
||||
this.idxi32 = [];
|
||||
// reverse lookup {i32:idx}
|
||||
this.i32idx = {};
|
||||
// {i32:rgb}
|
||||
this.i32rgb = {};
|
||||
// enable color caching (also incurs overhead of cache misses and cache building)
|
||||
this.useCache = opts.useCache !== false;
|
||||
// min color occurance count needed to qualify for caching
|
||||
this.cacheFreq = opts.cacheFreq || 10;
|
||||
// allows pre-defined palettes to be re-indexed (enabling palette compacting and sorting)
|
||||
this.reIndex = opts.reIndex || this.idxrgb.length == 0;
|
||||
// selection of color-distance equation
|
||||
this.colorDist = opts.colorDist == "manhattan" ? distManhattan : distEuclidean;
|
||||
|
||||
// if pre-defined palette, build lookups
|
||||
if (this.idxrgb.length > 0) {
|
||||
var self = this;
|
||||
this.idxrgb.forEach(function(rgb, i) {
|
||||
var i32 = (
|
||||
(255 << 24) | // alpha
|
||||
(rgb[2] << 16) | // blue
|
||||
(rgb[1] << 8) | // green
|
||||
rgb[0] // red
|
||||
) >>> 0;
|
||||
|
||||
self.idxi32[i] = i32;
|
||||
self.i32idx[i32] = i;
|
||||
self.i32rgb[i32] = rgb;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// gathers histogram info
|
||||
RgbQuant.prototype.sample = function sample(img, width) {
|
||||
if (this.palLocked)
|
||||
throw "Cannot sample additional images, palette already assembled.";
|
||||
|
||||
var data = getImageData(img, width);
|
||||
|
||||
switch (this.method) {
|
||||
case 1: this.colorStats1D(data.buf32); break;
|
||||
case 2: this.colorStats2D(data.buf32, data.width); break;
|
||||
}
|
||||
};
|
||||
|
||||
// image quantizer
|
||||
// todo: memoize colors here also
|
||||
// @retType: 1 - Uint8Array (default), 2 - Indexed array, 3 - Match @img type (unimplemented, todo)
|
||||
RgbQuant.prototype.reduce = function reduce(img, retType, dithKern, dithSerp) {
|
||||
if (!this.palLocked)
|
||||
this.buildPal();
|
||||
|
||||
dithKern = dithKern || this.dithKern;
|
||||
dithSerp = typeof dithSerp != "undefined" ? dithSerp : this.dithSerp;
|
||||
|
||||
retType = retType || 1;
|
||||
|
||||
// reduce w/dither
|
||||
if (dithKern)
|
||||
var out32 = this.dither(img, dithKern, dithSerp);
|
||||
else {
|
||||
var data = getImageData(img),
|
||||
buf32 = data.buf32,
|
||||
len = buf32.length,
|
||||
out32 = new Uint32Array(len);
|
||||
|
||||
for (var i = 0; i < len; i++) {
|
||||
var i32 = buf32[i];
|
||||
out32[i] = this.nearestColor(i32);
|
||||
}
|
||||
}
|
||||
|
||||
if (retType == 1)
|
||||
return new Uint8Array(out32.buffer);
|
||||
|
||||
if (retType == 2) {
|
||||
var out = [],
|
||||
len = out32.length;
|
||||
|
||||
for (var i = 0; i < len; i++) {
|
||||
var i32 = out32[i];
|
||||
out[i] = this.i32idx[i32];
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
// adapted from http://jsbin.com/iXofIji/2/edit by PAEz
|
||||
RgbQuant.prototype.dither = function(img, kernel, serpentine) {
|
||||
// http://www.tannerhelland.com/4660/dithering-eleven-algorithms-source-code/
|
||||
var kernels = {
|
||||
FloydSteinberg: [
|
||||
[7 / 16, 1, 0],
|
||||
[3 / 16, -1, 1],
|
||||
[5 / 16, 0, 1],
|
||||
[1 / 16, 1, 1]
|
||||
],
|
||||
FalseFloydSteinberg: [
|
||||
[3 / 8, 1, 0],
|
||||
[3 / 8, 0, 1],
|
||||
[2 / 8, 1, 1]
|
||||
],
|
||||
Stucki: [
|
||||
[8 / 42, 1, 0],
|
||||
[4 / 42, 2, 0],
|
||||
[2 / 42, -2, 1],
|
||||
[4 / 42, -1, 1],
|
||||
[8 / 42, 0, 1],
|
||||
[4 / 42, 1, 1],
|
||||
[2 / 42, 2, 1],
|
||||
[1 / 42, -2, 2],
|
||||
[2 / 42, -1, 2],
|
||||
[4 / 42, 0, 2],
|
||||
[2 / 42, 1, 2],
|
||||
[1 / 42, 2, 2]
|
||||
],
|
||||
Atkinson: [
|
||||
[1 / 8, 1, 0],
|
||||
[1 / 8, 2, 0],
|
||||
[1 / 8, -1, 1],
|
||||
[1 / 8, 0, 1],
|
||||
[1 / 8, 1, 1],
|
||||
[1 / 8, 0, 2]
|
||||
],
|
||||
Jarvis: [ // Jarvis, Judice, and Ninke / JJN?
|
||||
[7 / 48, 1, 0],
|
||||
[5 / 48, 2, 0],
|
||||
[3 / 48, -2, 1],
|
||||
[5 / 48, -1, 1],
|
||||
[7 / 48, 0, 1],
|
||||
[5 / 48, 1, 1],
|
||||
[3 / 48, 2, 1],
|
||||
[1 / 48, -2, 2],
|
||||
[3 / 48, -1, 2],
|
||||
[5 / 48, 0, 2],
|
||||
[3 / 48, 1, 2],
|
||||
[1 / 48, 2, 2]
|
||||
],
|
||||
Burkes: [
|
||||
[8 / 32, 1, 0],
|
||||
[4 / 32, 2, 0],
|
||||
[2 / 32, -2, 1],
|
||||
[4 / 32, -1, 1],
|
||||
[8 / 32, 0, 1],
|
||||
[4 / 32, 1, 1],
|
||||
[2 / 32, 2, 1],
|
||||
],
|
||||
Sierra: [
|
||||
[5 / 32, 1, 0],
|
||||
[3 / 32, 2, 0],
|
||||
[2 / 32, -2, 1],
|
||||
[4 / 32, -1, 1],
|
||||
[5 / 32, 0, 1],
|
||||
[4 / 32, 1, 1],
|
||||
[2 / 32, 2, 1],
|
||||
[2 / 32, -1, 2],
|
||||
[3 / 32, 0, 2],
|
||||
[2 / 32, 1, 2],
|
||||
],
|
||||
TwoSierra: [
|
||||
[4 / 16, 1, 0],
|
||||
[3 / 16, 2, 0],
|
||||
[1 / 16, -2, 1],
|
||||
[2 / 16, -1, 1],
|
||||
[3 / 16, 0, 1],
|
||||
[2 / 16, 1, 1],
|
||||
[1 / 16, 2, 1],
|
||||
],
|
||||
SierraLite: [
|
||||
[2 / 4, 1, 0],
|
||||
[1 / 4, -1, 1],
|
||||
[1 / 4, 0, 1],
|
||||
],
|
||||
};
|
||||
|
||||
if (!kernel || !kernels[kernel]) {
|
||||
throw 'Unknown dithering kernel: ' + kernel;
|
||||
}
|
||||
|
||||
var ds = kernels[kernel];
|
||||
|
||||
var data = getImageData(img),
|
||||
// buf8 = data.buf8,
|
||||
buf32 = data.buf32,
|
||||
width = data.width,
|
||||
height = data.height,
|
||||
len = buf32.length;
|
||||
|
||||
var dir = serpentine ? -1 : 1;
|
||||
|
||||
for (var y = 0; y < height; y++) {
|
||||
if (serpentine)
|
||||
dir = dir * -1;
|
||||
|
||||
var lni = y * width;
|
||||
|
||||
for (var x = (dir == 1 ? 0 : width - 1), xend = (dir == 1 ? width : 0); x !== xend; x += dir) {
|
||||
// Image pixel
|
||||
var idx = lni + x,
|
||||
i32 = buf32[idx],
|
||||
r1 = (i32 & 0xff),
|
||||
g1 = (i32 & 0xff00) >> 8,
|
||||
b1 = (i32 & 0xff0000) >> 16;
|
||||
|
||||
// Reduced pixel
|
||||
var i32x = this.nearestColor(i32),
|
||||
r2 = (i32x & 0xff),
|
||||
g2 = (i32x & 0xff00) >> 8,
|
||||
b2 = (i32x & 0xff0000) >> 16;
|
||||
|
||||
buf32[idx] =
|
||||
(255 << 24) | // alpha
|
||||
(b2 << 16) | // blue
|
||||
(g2 << 8) | // green
|
||||
r2;
|
||||
|
||||
// dithering strength
|
||||
if (this.dithDelta) {
|
||||
var dist = this.colorDist([r1, g1, b1], [r2, g2, b2]);
|
||||
if (dist < this.dithDelta)
|
||||
continue;
|
||||
}
|
||||
|
||||
// Component distance
|
||||
var er = r1 - r2,
|
||||
eg = g1 - g2,
|
||||
eb = b1 - b2;
|
||||
|
||||
for (var i = (dir == 1 ? 0 : ds.length - 1), end = (dir == 1 ? ds.length : 0); i !== end; i += dir) {
|
||||
var x1 = ds[i][1] * dir,
|
||||
y1 = ds[i][2];
|
||||
|
||||
var lni2 = y1 * width;
|
||||
|
||||
if (x1 + x >= 0 && x1 + x < width && y1 + y >= 0 && y1 + y < height) {
|
||||
var d = ds[i][0];
|
||||
var idx2 = idx + (lni2 + x1);
|
||||
|
||||
var r3 = (buf32[idx2] & 0xff),
|
||||
g3 = (buf32[idx2] & 0xff00) >> 8,
|
||||
b3 = (buf32[idx2] & 0xff0000) >> 16;
|
||||
|
||||
var r4 = Math.max(0, Math.min(255, r3 + er * d)),
|
||||
g4 = Math.max(0, Math.min(255, g3 + eg * d)),
|
||||
b4 = Math.max(0, Math.min(255, b3 + eb * d));
|
||||
|
||||
buf32[idx2] =
|
||||
(255 << 24) | // alpha
|
||||
(b4 << 16) | // blue
|
||||
(g4 << 8) | // green
|
||||
r4; // red
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return buf32;
|
||||
};
|
||||
|
||||
// reduces histogram to palette, remaps & memoizes reduced colors
|
||||
RgbQuant.prototype.buildPal = function buildPal(noSort) {
|
||||
if (this.palLocked || this.idxrgb.length > 0 && this.idxrgb.length <= this.colors) return;
|
||||
|
||||
var histG = this.histogram,
|
||||
sorted = sortedHashKeys(histG, true);
|
||||
|
||||
if (sorted.length == 0)
|
||||
throw "Nothing has been sampled, palette cannot be built.";
|
||||
|
||||
switch (this.method) {
|
||||
case 1:
|
||||
var cols = this.initColors,
|
||||
last = sorted[cols - 1],
|
||||
freq = histG[last];
|
||||
|
||||
var idxi32 = sorted.slice(0, cols);
|
||||
|
||||
// add any cut off colors with same freq as last
|
||||
var pos = cols, len = sorted.length;
|
||||
while (pos < len && histG[sorted[pos]] == freq)
|
||||
idxi32.push(sorted[pos++]);
|
||||
|
||||
// inject min huegroup colors
|
||||
if (this.hueStats)
|
||||
this.hueStats.inject(idxi32);
|
||||
|
||||
break;
|
||||
case 2:
|
||||
var idxi32 = sorted;
|
||||
break;
|
||||
}
|
||||
|
||||
// int32-ify values
|
||||
idxi32 = idxi32.map(function(v){return +v;});
|
||||
|
||||
this.reducePal(idxi32);
|
||||
|
||||
if (!noSort && this.reIndex)
|
||||
this.sortPal();
|
||||
|
||||
// build cache of top histogram colors
|
||||
if (this.useCache)
|
||||
this.cacheHistogram(idxi32);
|
||||
|
||||
this.palLocked = true;
|
||||
};
|
||||
|
||||
RgbQuant.prototype.palette = function palette(tuples, noSort) {
|
||||
this.buildPal(noSort);
|
||||
return tuples ? this.idxrgb : new Uint8Array((new Uint32Array(this.idxi32)).buffer);
|
||||
};
|
||||
|
||||
RgbQuant.prototype.prunePal = function prunePal(keep) {
|
||||
var i32;
|
||||
|
||||
for (var j = 0; j < this.idxrgb.length; j++) {
|
||||
if (!keep[j]) {
|
||||
i32 = this.idxi32[j];
|
||||
this.idxrgb[j] = null;
|
||||
this.idxi32[j] = null;
|
||||
delete this.i32idx[i32];
|
||||
}
|
||||
}
|
||||
|
||||
// compact
|
||||
if (this.reIndex) {
|
||||
var idxrgb = [],
|
||||
idxi32 = [],
|
||||
i32idx = {};
|
||||
|
||||
for (var j = 0, i = 0; j < this.idxrgb.length; j++) {
|
||||
if (this.idxrgb[j]) {
|
||||
i32 = this.idxi32[j];
|
||||
idxrgb[i] = this.idxrgb[j];
|
||||
i32idx[i32] = i;
|
||||
idxi32[i] = i32;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
this.idxrgb = idxrgb;
|
||||
this.idxi32 = idxi32;
|
||||
this.i32idx = i32idx;
|
||||
}
|
||||
};
|
||||
|
||||
// reduces similar colors from an importance-sorted Uint32 rgba array
|
||||
RgbQuant.prototype.reducePal = function reducePal(idxi32) {
|
||||
// if pre-defined palette's length exceeds target
|
||||
if (this.idxrgb.length > this.colors) {
|
||||
// quantize histogram to existing palette
|
||||
var len = idxi32.length, keep = {}, uniques = 0, idx, pruned = false;
|
||||
|
||||
for (var i = 0; i < len; i++) {
|
||||
// palette length reached, unset all remaining colors (sparse palette)
|
||||
if (uniques == this.colors && !pruned) {
|
||||
this.prunePal(keep);
|
||||
pruned = true;
|
||||
}
|
||||
|
||||
idx = this.nearestIndex(idxi32[i]);
|
||||
|
||||
if (uniques < this.colors && !keep[idx]) {
|
||||
keep[idx] = true;
|
||||
uniques++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!pruned) {
|
||||
this.prunePal(keep);
|
||||
pruned = true;
|
||||
}
|
||||
}
|
||||
// reduce histogram to create initial palette
|
||||
else {
|
||||
// build full rgb palette
|
||||
var idxrgb = idxi32.map(function(i32) {
|
||||
return [
|
||||
(i32 & 0xff),
|
||||
(i32 & 0xff00) >> 8,
|
||||
(i32 & 0xff0000) >> 16,
|
||||
];
|
||||
});
|
||||
|
||||
var len = idxrgb.length,
|
||||
palLen = len,
|
||||
thold = this.initDist;
|
||||
|
||||
// palette already at or below desired length
|
||||
if (palLen > this.colors) {
|
||||
while (palLen > this.colors) {
|
||||
var memDist = [];
|
||||
|
||||
// iterate palette
|
||||
for (var i = 0; i < len; i++) {
|
||||
var pxi = idxrgb[i], i32i = idxi32[i];
|
||||
if (!pxi) continue;
|
||||
|
||||
for (var j = i + 1; j < len; j++) {
|
||||
var pxj = idxrgb[j], i32j = idxi32[j];
|
||||
if (!pxj) continue;
|
||||
|
||||
var dist = this.colorDist(pxi, pxj);
|
||||
|
||||
if (dist < thold) {
|
||||
// store index,rgb,dist
|
||||
memDist.push([j, pxj, i32j, dist]);
|
||||
|
||||
// kill squashed value
|
||||
delete(idxrgb[j]);
|
||||
palLen--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// palette reduction pass
|
||||
// console.log("palette length: " + palLen);
|
||||
|
||||
// if palette is still much larger than target, increment by larger initDist
|
||||
thold += (palLen > this.colors * 3) ? this.initDist : this.distIncr;
|
||||
}
|
||||
|
||||
// if palette is over-reduced, re-add removed colors with largest distances from last round
|
||||
if (palLen < this.colors) {
|
||||
// sort descending
|
||||
sort.call(memDist, function(a,b) {
|
||||
return b[3] - a[3];
|
||||
});
|
||||
|
||||
var k = 0;
|
||||
while (palLen < this.colors) {
|
||||
// re-inject rgb into final palette
|
||||
idxrgb[memDist[k][0]] = memDist[k][1];
|
||||
|
||||
palLen++;
|
||||
k++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var len = idxrgb.length;
|
||||
for (var i = 0; i < len; i++) {
|
||||
if (!idxrgb[i]) continue;
|
||||
|
||||
this.idxrgb.push(idxrgb[i]);
|
||||
this.idxi32.push(idxi32[i]);
|
||||
|
||||
this.i32idx[idxi32[i]] = this.idxi32.length - 1;
|
||||
this.i32rgb[idxi32[i]] = idxrgb[i];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// global top-population
|
||||
RgbQuant.prototype.colorStats1D = function colorStats1D(buf32) {
|
||||
var histG = this.histogram,
|
||||
num = 0, col,
|
||||
len = buf32.length;
|
||||
|
||||
for (var i = 0; i < len; i++) {
|
||||
col = buf32[i];
|
||||
|
||||
// skip transparent
|
||||
if ((col & 0xff000000) >> 24 == 0) continue;
|
||||
|
||||
// collect hue stats
|
||||
if (this.hueStats)
|
||||
this.hueStats.check(col);
|
||||
|
||||
if (col in histG)
|
||||
histG[col]++;
|
||||
else
|
||||
histG[col] = 1;
|
||||
}
|
||||
};
|
||||
|
||||
// population threshold within subregions
|
||||
// FIXME: this can over-reduce (few/no colors same?), need a way to keep
|
||||
// important colors that dont ever reach local thresholds (gradients?)
|
||||
RgbQuant.prototype.colorStats2D = function colorStats2D(buf32, width) {
|
||||
var boxW = this.boxSize[0],
|
||||
boxH = this.boxSize[1],
|
||||
area = boxW * boxH,
|
||||
boxes = makeBoxes(width, buf32.length / width, boxW, boxH),
|
||||
histG = this.histogram,
|
||||
self = this;
|
||||
|
||||
boxes.forEach(function(box) {
|
||||
var effc = Math.max(Math.round((box.w * box.h) / area) * self.boxPxls, 2),
|
||||
histL = {}, col;
|
||||
|
||||
iterBox(box, width, function(i) {
|
||||
col = buf32[i];
|
||||
|
||||
// skip transparent
|
||||
if ((col & 0xff000000) >> 24 == 0) return;
|
||||
|
||||
// collect hue stats
|
||||
if (self.hueStats)
|
||||
self.hueStats.check(col);
|
||||
|
||||
if (col in histG)
|
||||
histG[col]++;
|
||||
else if (col in histL) {
|
||||
if (++histL[col] >= effc)
|
||||
histG[col] = histL[col];
|
||||
}
|
||||
else
|
||||
histL[col] = 1;
|
||||
});
|
||||
});
|
||||
|
||||
if (this.hueStats)
|
||||
this.hueStats.inject(histG);
|
||||
};
|
||||
|
||||
// TODO: group very low lum and very high lum colors
|
||||
// TODO: pass custom sort order
|
||||
RgbQuant.prototype.sortPal = function sortPal() {
|
||||
var self = this;
|
||||
|
||||
this.idxi32.sort(function(a,b) {
|
||||
var idxA = self.i32idx[a],
|
||||
idxB = self.i32idx[b],
|
||||
rgbA = self.idxrgb[idxA],
|
||||
rgbB = self.idxrgb[idxB];
|
||||
|
||||
var hslA = rgb2hsl(rgbA[0],rgbA[1],rgbA[2]),
|
||||
hslB = rgb2hsl(rgbB[0],rgbB[1],rgbB[2]);
|
||||
|
||||
// sort all grays + whites together
|
||||
var hueA = (rgbA[0] == rgbA[1] && rgbA[1] == rgbA[2]) ? -1 : hueGroup(hslA.h, self.hueGroups);
|
||||
var hueB = (rgbB[0] == rgbB[1] && rgbB[1] == rgbB[2]) ? -1 : hueGroup(hslB.h, self.hueGroups);
|
||||
|
||||
var hueDiff = hueB - hueA;
|
||||
if (hueDiff) return -hueDiff;
|
||||
|
||||
var lumDiff = lumGroup(+hslB.l.toFixed(2)) - lumGroup(+hslA.l.toFixed(2));
|
||||
if (lumDiff) return -lumDiff;
|
||||
|
||||
var satDiff = satGroup(+hslB.s.toFixed(2)) - satGroup(+hslA.s.toFixed(2));
|
||||
if (satDiff) return -satDiff;
|
||||
});
|
||||
|
||||
// sync idxrgb & i32idx
|
||||
this.idxi32.forEach(function(i32, i) {
|
||||
self.idxrgb[i] = self.i32rgb[i32];
|
||||
self.i32idx[i32] = i;
|
||||
});
|
||||
};
|
||||
|
||||
// TOTRY: use HUSL - http://boronine.com/husl/
|
||||
RgbQuant.prototype.nearestColor = function nearestColor(i32) {
|
||||
var idx = this.nearestIndex(i32);
|
||||
return idx === null ? 0 : this.idxi32[idx];
|
||||
};
|
||||
|
||||
// TOTRY: use HUSL - http://boronine.com/husl/
|
||||
RgbQuant.prototype.nearestIndex = function nearestIndex(i32) {
|
||||
// alpha 0 returns null index
|
||||
if ((i32 & 0xff000000) >> 24 == 0)
|
||||
return null;
|
||||
|
||||
if (this.useCache && (""+i32) in this.i32idx)
|
||||
return this.i32idx[i32];
|
||||
|
||||
var min = 1000,
|
||||
idx,
|
||||
rgb = [
|
||||
(i32 & 0xff),
|
||||
(i32 & 0xff00) >> 8,
|
||||
(i32 & 0xff0000) >> 16,
|
||||
],
|
||||
len = this.idxrgb.length;
|
||||
|
||||
for (var i = 0; i < len; i++) {
|
||||
if (!this.idxrgb[i]) continue; // sparse palettes
|
||||
|
||||
var dist = this.colorDist(rgb, this.idxrgb[i]);
|
||||
|
||||
if (dist < min) {
|
||||
min = dist;
|
||||
idx = i;
|
||||
}
|
||||
}
|
||||
|
||||
return idx;
|
||||
};
|
||||
|
||||
RgbQuant.prototype.cacheHistogram = function cacheHistogram(idxi32) {
|
||||
for (var i = 0, i32 = idxi32[i]; i < idxi32.length && this.histogram[i32] >= this.cacheFreq; i32 = idxi32[i++])
|
||||
this.i32idx[i32] = this.nearestIndex(i32);
|
||||
};
|
||||
|
||||
function HueStats(numGroups, minCols) {
|
||||
this.numGroups = numGroups;
|
||||
this.minCols = minCols;
|
||||
this.stats = {};
|
||||
|
||||
for (var i = -1; i < numGroups; i++)
|
||||
this.stats[i] = {num: 0, cols: []};
|
||||
|
||||
this.groupsFull = 0;
|
||||
}
|
||||
|
||||
HueStats.prototype.check = function checkHue(i32) {
|
||||
if (this.groupsFull == this.numGroups + 1)
|
||||
this.check = function() {return;};
|
||||
|
||||
var r = (i32 & 0xff),
|
||||
g = (i32 & 0xff00) >> 8,
|
||||
b = (i32 & 0xff0000) >> 16,
|
||||
hg = (r == g && g == b) ? -1 : hueGroup(rgb2hsl(r,g,b).h, this.numGroups),
|
||||
gr = this.stats[hg],
|
||||
min = this.minCols;
|
||||
|
||||
gr.num++;
|
||||
|
||||
if (gr.num > min)
|
||||
return;
|
||||
if (gr.num == min)
|
||||
this.groupsFull++;
|
||||
|
||||
if (gr.num <= min)
|
||||
this.stats[hg].cols.push(i32);
|
||||
};
|
||||
|
||||
HueStats.prototype.inject = function injectHues(histG) {
|
||||
for (var i = -1; i < this.numGroups; i++) {
|
||||
if (this.stats[i].num <= this.minCols) {
|
||||
switch (typeOf(histG)) {
|
||||
case "Array":
|
||||
this.stats[i].cols.forEach(function(col){
|
||||
if (histG.indexOf(col) == -1)
|
||||
histG.push(col);
|
||||
});
|
||||
break;
|
||||
case "Object":
|
||||
this.stats[i].cols.forEach(function(col){
|
||||
if (!histG[col])
|
||||
histG[col] = 1;
|
||||
else
|
||||
histG[col]++;
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Rec. 709 (sRGB) luma coef
|
||||
var Pr = .2126,
|
||||
Pg = .7152,
|
||||
Pb = .0722;
|
||||
|
||||
// http://alienryderflex.com/hsp.html
|
||||
function rgb2lum(r,g,b) {
|
||||
return Math.sqrt(
|
||||
Pr * r*r +
|
||||
Pg * g*g +
|
||||
Pb * b*b
|
||||
);
|
||||
}
|
||||
|
||||
var rd = 255,
|
||||
gd = 255,
|
||||
bd = 255;
|
||||
|
||||
var euclMax = Math.sqrt(Pr*rd*rd + Pg*gd*gd + Pb*bd*bd);
|
||||
// perceptual Euclidean color distance
|
||||
function distEuclidean(rgb0, rgb1) {
|
||||
var rd = rgb1[0]-rgb0[0],
|
||||
gd = rgb1[1]-rgb0[1],
|
||||
bd = rgb1[2]-rgb0[2];
|
||||
|
||||
return Math.sqrt(Pr*rd*rd + Pg*gd*gd + Pb*bd*bd) / euclMax;
|
||||
}
|
||||
|
||||
var manhMax = Pr*rd + Pg*gd + Pb*bd;
|
||||
// perceptual Manhattan color distance
|
||||
function distManhattan(rgb0, rgb1) {
|
||||
var rd = Math.abs(rgb1[0]-rgb0[0]),
|
||||
gd = Math.abs(rgb1[1]-rgb0[1]),
|
||||
bd = Math.abs(rgb1[2]-rgb0[2]);
|
||||
|
||||
return (Pr*rd + Pg*gd + Pb*bd) / manhMax;
|
||||
}
|
||||
|
||||
// http://rgb2hsl.nichabi.com/javascript-function.php
|
||||
function rgb2hsl(r, g, b) {
|
||||
var max, min, h, s, l, d;
|
||||
r /= 255;
|
||||
g /= 255;
|
||||
b /= 255;
|
||||
max = Math.max(r, g, b);
|
||||
min = Math.min(r, g, b);
|
||||
l = (max + min) / 2;
|
||||
if (max == min) {
|
||||
h = s = 0;
|
||||
} else {
|
||||
d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
switch (max) {
|
||||
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
|
||||
case g: h = (b - r) / d + 2; break;
|
||||
case b: h = (r - g) / d + 4; break
|
||||
}
|
||||
h /= 6;
|
||||
}
|
||||
// h = Math.floor(h * 360)
|
||||
// s = Math.floor(s * 100)
|
||||
// l = Math.floor(l * 100)
|
||||
return {
|
||||
h: h,
|
||||
s: s,
|
||||
l: rgb2lum(r,g,b),
|
||||
};
|
||||
}
|
||||
|
||||
function hueGroup(hue, segs) {
|
||||
var seg = 1/segs,
|
||||
haf = seg/2;
|
||||
|
||||
if (hue >= 1 - haf || hue <= haf)
|
||||
return 0;
|
||||
|
||||
for (var i = 1; i < segs; i++) {
|
||||
var mid = i*seg;
|
||||
if (hue >= mid - haf && hue <= mid + haf)
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
function satGroup(sat) {
|
||||
return sat;
|
||||
}
|
||||
|
||||
function lumGroup(lum) {
|
||||
return lum;
|
||||
}
|
||||
|
||||
function typeOf(val) {
|
||||
return Object.prototype.toString.call(val).slice(8,-1);
|
||||
}
|
||||
|
||||
var sort = isArrSortStable() ? Array.prototype.sort : stableSort;
|
||||
|
||||
// must be used via stableSort.call(arr, fn)
|
||||
function stableSort(fn) {
|
||||
var type = typeOf(this[0]);
|
||||
|
||||
if (type == "Number" || type == "String") {
|
||||
var ord = {}, len = this.length, val;
|
||||
|
||||
for (var i = 0; i < len; i++) {
|
||||
val = this[i];
|
||||
if (ord[val] || ord[val] === 0) continue;
|
||||
ord[val] = i;
|
||||
}
|
||||
|
||||
return this.sort(function(a,b) {
|
||||
return fn(a,b) || ord[a] - ord[b];
|
||||
});
|
||||
}
|
||||
else {
|
||||
var ord = this.map(function(v){return v});
|
||||
|
||||
return this.sort(function(a,b) {
|
||||
return fn(a,b) || ord.indexOf(a) - ord.indexOf(b);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// test if js engine's Array#sort implementation is stable
|
||||
function isArrSortStable() {
|
||||
var str = "abcdefghijklmnopqrstuvwxyz";
|
||||
|
||||
return "xyzvwtursopqmnklhijfgdeabc" == str.split("").sort(function(a,b) {
|
||||
return ~~(str.indexOf(b)/2.3) - ~~(str.indexOf(a)/2.3);
|
||||
}).join("");
|
||||
}
|
||||
|
||||
// returns uniform pixel data from various img
|
||||
// TODO?: if array is passed, createimagedata, createlement canvas? take a pxlen?
|
||||
function getImageData(img, width) {
|
||||
var can, ctx, imgd, buf8, buf32, height;
|
||||
|
||||
switch (typeOf(img)) {
|
||||
case "HTMLImageElement":
|
||||
can = document.createElement("canvas");
|
||||
can.width = img.naturalWidth;
|
||||
can.height = img.naturalHeight;
|
||||
ctx = can.getContext("2d");
|
||||
ctx.drawImage(img,0,0);
|
||||
case "Canvas":
|
||||
case "HTMLCanvasElement":
|
||||
can = can || img;
|
||||
ctx = ctx || can.getContext("2d");
|
||||
case "CanvasRenderingContext2D":
|
||||
ctx = ctx || img;
|
||||
can = can || ctx.canvas;
|
||||
imgd = ctx.getImageData(0, 0, can.width, can.height);
|
||||
case "ImageData":
|
||||
imgd = imgd || img;
|
||||
width = imgd.width;
|
||||
if (typeOf(imgd.data) == "CanvasPixelArray")
|
||||
buf8 = new Uint8Array(imgd.data);
|
||||
else
|
||||
buf8 = imgd.data;
|
||||
case "Array":
|
||||
case "CanvasPixelArray":
|
||||
buf8 = buf8 || new Uint8Array(img);
|
||||
case "Uint8Array":
|
||||
case "Uint8ClampedArray":
|
||||
buf8 = buf8 || img;
|
||||
buf32 = new Uint32Array(buf8.buffer);
|
||||
case "Uint32Array":
|
||||
buf32 = buf32 || img;
|
||||
buf8 = buf8 || new Uint8Array(buf32.buffer);
|
||||
width = width || buf32.length;
|
||||
height = buf32.length / width;
|
||||
}
|
||||
|
||||
return {
|
||||
can: can,
|
||||
ctx: ctx,
|
||||
imgd: imgd,
|
||||
buf8: buf8,
|
||||
buf32: buf32,
|
||||
width: width,
|
||||
height: height,
|
||||
};
|
||||
}
|
||||
|
||||
// partitions a rect of wid x hgt into
|
||||
// array of bboxes of w0 x h0 (or less)
|
||||
function makeBoxes(wid, hgt, w0, h0) {
|
||||
var wnum = ~~(wid/w0), wrem = wid%w0,
|
||||
hnum = ~~(hgt/h0), hrem = hgt%h0,
|
||||
xend = wid-wrem, yend = hgt-hrem;
|
||||
|
||||
var bxs = [];
|
||||
for (var y = 0; y < hgt; y += h0)
|
||||
for (var x = 0; x < wid; x += w0)
|
||||
bxs.push({x:x, y:y, w:(x==xend?wrem:w0), h:(y==yend?hrem:h0)});
|
||||
|
||||
return bxs;
|
||||
}
|
||||
|
||||
// iterates @bbox within a parent rect of width @wid; calls @fn, passing index within parent
|
||||
function iterBox(bbox, wid, fn) {
|
||||
var b = bbox,
|
||||
i0 = b.y * wid + b.x,
|
||||
i1 = (b.y + b.h - 1) * wid + (b.x + b.w - 1),
|
||||
cnt = 0, incr = wid - b.w + 1, i = i0;
|
||||
|
||||
do {
|
||||
fn.call(this, i);
|
||||
i += (++cnt % b.w == 0) ? incr : 1;
|
||||
} while (i <= i1);
|
||||
}
|
||||
|
||||
// returns array of hash keys sorted by their values
|
||||
function sortedHashKeys(obj, desc) {
|
||||
var keys = [];
|
||||
|
||||
for (var key in obj)
|
||||
keys.push(key);
|
||||
|
||||
return sort.call(keys, function(a,b) {
|
||||
return desc ? obj[b] - obj[a] : obj[a] - obj[b];
|
||||
});
|
||||
}
|
||||
|
||||
// expose
|
||||
this.RgbQuant = RgbQuant;
|
||||
|
||||
// expose to commonJS
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = RgbQuant;
|
||||
}
|
||||
|
||||
}).call(this);
|
||||
2
libs/three.min.js
vendored
2
libs/three.min.js
vendored
|
|
@ -88,7 +88,7 @@ var t=a.getRenderTarget();return{isWebGL2:c.isWebGL2,shaderID:h,precision:m,inst
|
|||
emissiveMap:!!b.emissiveMap,emissiveMapEncoding:d(b.emissiveMap,a.gammaInput),bumpMap:!!b.bumpMap,normalMap:!!b.normalMap,objectSpaceNormalMap:1===b.normalMapType,tangentSpaceNormalMap:0===b.normalMapType,clearcoatNormalMap:!!b.clearcoatNormalMap,displacementMap:!!b.displacementMap,roughnessMap:!!b.roughnessMap,metalnessMap:!!b.metalnessMap,specularMap:!!b.specularMap,alphaMap:!!b.alphaMap,gradientMap:!!b.gradientMap,sheen:!!b.sheen,combine:b.combine,vertexTangents:b.normalMap&&b.vertexTangents,vertexColors:b.vertexColors,
|
||||
vertexUvs:!!b.map||!!b.bumpMap||!!b.normalMap||!!b.specularMap||!!b.alphaMap||!!b.emissiveMap||!!b.roughnessMap||!!b.metalnessMap||!!b.clearcoatNormalMap,fog:!!q,useFog:b.fog,fogExp2:q&&q.isFogExp2,flatShading:b.flatShading,sizeAttenuation:b.sizeAttenuation,logarithmicDepthBuffer:c.logarithmicDepthBuffer,skinning:b.skinning&&0<l,maxBones:l,useVertexTexture:c.floatVertexTextures,morphTargets:b.morphTargets,morphNormals:b.morphNormals,maxMorphTargets:a.maxMorphTargets,maxMorphNormals:a.maxMorphNormals,
|
||||
numDirLights:e.directional.length,numPointLights:e.point.length,numSpotLights:e.spot.length,numRectAreaLights:e.rectArea.length,numHemiLights:e.hemi.length,numDirLightShadows:e.directionalShadowMap.length,numPointLightShadows:e.pointShadowMap.length,numSpotLightShadows:e.spotShadowMap.length,numClippingPlanes:u,numClipIntersection:k,dithering:b.dithering,shadowMapEnabled:a.shadowMap.enabled&&0<g.length,shadowMapType:a.shadowMap.type,toneMapping:b.toneMapped?a.toneMapping:0,physicallyCorrectLights:a.physicallyCorrectLights,
|
||||
premultipliedAlpha:b.premultipliedAlpha,alphaTest:b.alphaTest,doubleSided:2===b.side,flipSided:1===b.side,depthPacking:void 0!==b.depthPacking?b.depthPacking:!1}};this.getProgramCode=function(b,c){var d=[];c.shaderID?d.push(c.shaderID):(d.push(b.fragmentShader),d.push(b.vertexShader));if(void 0!==b.defines)for(var e in b.defines)d.push(e),d.push(b.defines[e]);for(e=0;e<g.length;e++)d.push(c[g[e]]);d.push(b.onBeforeCompile.toString());d.push(a.gammaOutput);d.push(a.gammaFactor);return d.join()};this.acquireProgram=
|
||||
premultipliedAlpha:b.premultipliedAlpha,alphaTest:b.alphaTest,doubleSided:2===b.side,flipSided:1===b.side,depthPacking:void 0!==b.depthPacking?b.depthPacking:!1}};this.getProgramCode=function(b,c){var d=[];c.shaderID?d.push(c.shaderID):(d.push(b.fragmentShader),d.push(b.vertexShader));if(void 0!==b.defines)for(var e in b.defines)d.push(e),d.push(b.defines[e]);for(e=0;e<g.length;e++)d.push(c[g[e]]);d.push(b.onBeforeCompile.toString());d.push(a.gammaOutput);d.push(a.gammaFactor);return d.join("")};this.acquireProgram=
|
||||
function(c,d,f,g){for(var h,l=0,m=e.length;l<m;l++){var q=e[l];if(q.code===g){h=q;++h.usedTimes;break}}void 0===h&&(h=new Wj(a,b,g,c,d,f),e.push(h));return h};this.releaseProgram=function(a){if(0===--a.usedTimes){var b=e.indexOf(a);e[b]=e[e.length-1];e.pop();a.destroy()}};this.programs=e}function Zj(){var a=new WeakMap;return{get:function(b){var c=a.get(b);void 0===c&&(c={},a.set(b,c));return c},remove:function(b){a.delete(b)},update:function(b,c,d){a.get(b)[c]=d},dispose:function(){a=new WeakMap}}}
|
||||
function ak(a,b){return a.groupOrder!==b.groupOrder?a.groupOrder-b.groupOrder:a.renderOrder!==b.renderOrder?a.renderOrder-b.renderOrder:a.program!==b.program?a.program.id-b.program.id:a.material.id!==b.material.id?a.material.id-b.material.id:a.z!==b.z?a.z-b.z:a.id-b.id}function bk(a,b){return a.groupOrder!==b.groupOrder?a.groupOrder-b.groupOrder:a.renderOrder!==b.renderOrder?a.renderOrder-b.renderOrder:a.z!==b.z?b.z-a.z:a.id-b.id}function Hh(){function a(a,d,e,m,q,u){var g=b[c];void 0===g?(g={id:a.id,
|
||||
object:a,geometry:d,material:e,program:e.program||f,groupOrder:m,renderOrder:a.renderOrder,z:q,group:u},b[c]=g):(g.id=a.id,g.object=a,g.geometry=d,g.material=e,g.program=e.program||f,g.groupOrder=m,g.renderOrder=a.renderOrder,g.z=q,g.group=u);c++;return g}var b=[],c=0,d=[],e=[],f={id:-1};return{opaque:d,transparent:e,init:function(){c=0;d.length=0;e.length=0},push:function(b,c,f,m,q,u){b=a(b,c,f,m,q,u);(!0===f.transparent?e:d).push(b)},unshift:function(b,c,f,m,q,u){b=a(b,c,f,m,q,u);(!0===f.transparent?
|
||||
|
|
|
|||
204
main.js
204
main.js
|
|
@ -7,7 +7,7 @@
|
|||
// See also https://github.com/Azgaar/Fantasy-Map-Generator/issues/153
|
||||
|
||||
"use strict";
|
||||
const version = "1.3"; // generator version
|
||||
const version = "1.4"; // generator version
|
||||
document.title += " v" + version;
|
||||
|
||||
// if map version is not stored, clear localStorage and show a message
|
||||
|
|
@ -52,6 +52,7 @@ let trails = routes.append("g").attr("id", "trails");
|
|||
let searoutes = routes.append("g").attr("id", "searoutes");
|
||||
let temperature = viewbox.append("g").attr("id", "temperature");
|
||||
let coastline = viewbox.append("g").attr("id", "coastline");
|
||||
let ice = viewbox.append("g").attr("id", "ice").style("display", "none");
|
||||
let prec = viewbox.append("g").attr("id", "prec").style("display", "none");
|
||||
let population = viewbox.append("g").attr("id", "population");
|
||||
let labels = viewbox.append("g").attr("id", "labels");
|
||||
|
|
@ -60,8 +61,7 @@ let burgIcons = icons.append("g").attr("id", "burgIcons");
|
|||
let anchors = icons.append("g").attr("id", "anchors");
|
||||
let armies = viewbox.append("g").attr("id", "armies").style("display", "none");
|
||||
let markers = viewbox.append("g").attr("id", "markers").style("display", "none");
|
||||
let fogging = viewbox.append("g").attr("id", "fogging-cont").attr("mask", "url(#fog)")
|
||||
.append("g").attr("id", "fogging").style("display", "none");
|
||||
let fogging = viewbox.append("g").attr("id", "fogging-cont").attr("mask", "url(#fog)").append("g").attr("id", "fogging").style("display", "none");
|
||||
let ruler = viewbox.append("g").attr("id", "ruler").style("display", "none");
|
||||
let debug = viewbox.append("g").attr("id", "debug");
|
||||
|
||||
|
|
@ -93,6 +93,7 @@ population.append("g").attr("id", "urban");
|
|||
|
||||
// fogging
|
||||
fogging.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%");
|
||||
fogging.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%").attr("fill", "#e8f0f6").attr("filter", "url(#splotch)");
|
||||
|
||||
// assign events separately as not a viewbox child
|
||||
scaleBar.on("mousemove", () => tip("Click to open Units Editor"));
|
||||
|
|
@ -101,7 +102,7 @@ legend.on("mousemove", () => tip("Drag to change the position. Click to hide the
|
|||
// main data variables
|
||||
let grid = {}; // initial grapg based on jittered square grid and data
|
||||
let pack = {}; // packed graph and data
|
||||
let seed, mapHistory = [], elSelected, modules = {}, notes = [];
|
||||
let seed, mapId, mapHistory = [], elSelected, modules = {}, notes = [];
|
||||
let customization = 0; // 0 - no; 1 = heightmap draw; 2 - states draw; 3 - add state/burg; 4 - cultures draw
|
||||
|
||||
let biomesData = applyDefaultBiomesSystem();
|
||||
|
|
@ -120,11 +121,6 @@ let options = {}; // options object
|
|||
let mapCoordinates = {}; // map coordinates on globe
|
||||
options.winds = [225, 45, 225, 315, 135, 315]; // default wind directions
|
||||
|
||||
// woldbuilding options
|
||||
options.year = rand(100, 2000); // current year
|
||||
options.era = Names.getBaseShort(P(.7) ? 1 : rand(nameBases.length)) + " Era"; // current era name, global for all cultures
|
||||
options.eraShort = options.era[0] + "E"; // short name for era
|
||||
|
||||
applyStoredOptions();
|
||||
let graphWidth = +mapWidthInput.value, graphHeight = +mapHeightInput.value; // voronoi graph extention, cannot be changed arter generation
|
||||
let svgWidth = graphWidth, svgHeight = graphHeight; // svg canvas resolution, can be changed
|
||||
|
|
@ -220,7 +216,7 @@ function focusOn() {
|
|||
const url = new URL(window.location.href);
|
||||
const params = url.searchParams;
|
||||
|
||||
if (params.get("from") === "MFCG") {
|
||||
if (params.get("from") === "MFCG" && document.referrer) {
|
||||
if (params.get("seed").length === 13) {
|
||||
// show back burg from MFCG
|
||||
params.set("burg", params.get("seed").slice(-4));
|
||||
|
|
@ -313,12 +309,12 @@ function applyDefaultBiomesSystem() {
|
|||
const iconsDensity = [0,3,2,120,120,120,120,150,150,100,5,0,150];
|
||||
const icons = [{},{dune:3, cactus:6, deadTree:1},{dune:9, deadTree:1},{acacia:1, grass:9},{grass:1},{acacia:8, palm:1},{deciduous:1},{acacia:5, palm:3, deciduous:1, swamp:1},{deciduous:6, swamp:1},{conifer:1},{grass:1},{},{swamp:1}];
|
||||
const cost = [10,200,150,60,50,70,70,80,90,200,1000,5000,150]; // biome movement cost
|
||||
const biomesMartix = [ // hot ↔ cold; dry ↕ wet
|
||||
new Uint8Array([1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2]),
|
||||
new Uint8Array([3,3,3,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,9,9,9,9,9,10,10]),
|
||||
const biomesMartix = [ // hot ↔ cold [>19°C; <-4°C]; dry ↕ wet
|
||||
new Uint8Array([1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,10]),
|
||||
new Uint8Array([3,3,3,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,9,9,9,9,10,10,10]),
|
||||
new Uint8Array([5,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,9,9,9,9,9,10,10,10]),
|
||||
new Uint8Array([5,6,6,6,6,6,6,8,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,10,10,10]),
|
||||
new Uint8Array([7,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,10,10,10])
|
||||
new Uint8Array([7,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,10,10])
|
||||
];
|
||||
|
||||
// parse icons weighted array into a simple array
|
||||
|
|
@ -334,7 +330,7 @@ function applyDefaultBiomesSystem() {
|
|||
}
|
||||
|
||||
function showWelcomeMessage() {
|
||||
const post = link("https://www.reddit.com/r/FantasyMapGenerator/comments/ft5b41/update_new_version_is_published_military_update_v/", "Main changes:"); // announcement on Reddit
|
||||
const post = link("https://www.reddit.com/r/FantasyMapGenerator/comments/ft5b41/update_new_version_is_published_into_the_battle_v14/", "Main changes:"); // announcement on Reddit
|
||||
const changelog = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "previous version");
|
||||
const reddit = link("https://www.reddit.com/r/FantasyMapGenerator", "Reddit community");
|
||||
const discord = link("https://discordapp.com/invite/X7E84HU", "Discord server");
|
||||
|
|
@ -345,10 +341,14 @@ function showWelcomeMessage() {
|
|||
This version is compatible with ${changelog}, loaded <i>.map</i> files will be auto-updated.
|
||||
|
||||
<ul>${post}
|
||||
<li>Military Forces generation</li>
|
||||
<li>Military Forces overview</li>
|
||||
<li>Military Units editor</li>
|
||||
<li>Regiments editor</li>
|
||||
<li>Military forces changes (${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Military-Forces", "detailed description")})</li>
|
||||
<li>Battle simulation (${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Battle-Simulator", "detailed description")})</li>
|
||||
<li>Ice layer and Ice editor</li>
|
||||
<li>Route and River Elevation profile (by EvolvedExperiment)</li>
|
||||
<li>Image Converter enhancement</li>
|
||||
<li>Name generator improvement</li>
|
||||
<li>Improved integration with City Generator</li>
|
||||
<li>Fogging restyle</li>
|
||||
</ul>
|
||||
|
||||
<p>You can can also download a ${desktop}.</p>
|
||||
|
|
@ -527,7 +527,6 @@ function generate() {
|
|||
elevateLakes();
|
||||
Rivers.generate();
|
||||
defineBiomes();
|
||||
//drawSeaIce();
|
||||
|
||||
rankCells();
|
||||
Cultures.generate();
|
||||
|
|
@ -701,8 +700,9 @@ function openNearSeaLakes() {
|
|||
// define map size and position based on template and random factor
|
||||
function defineMapSize() {
|
||||
const [size, latitude] = getSizeAndLatitude();
|
||||
if (!locked("mapSize")) mapSizeOutput.value = mapSizeInput.value = size;
|
||||
if (!locked("latitude")) latitudeOutput.value = latitudeInput.value = latitude;
|
||||
const randomize = new URL(window.location.href).searchParams.get("options") === "default"; // ignore stored options
|
||||
if (randomize || !locked("mapSize")) mapSizeOutput.value = mapSizeInput.value = size;
|
||||
if (randomize || !locked("latitude")) latitudeOutput.value = latitudeInput.value = latitude;
|
||||
|
||||
function getSizeAndLatitude() {
|
||||
const template = document.getElementById("templateInput").value; // heightmap template
|
||||
|
|
@ -963,7 +963,7 @@ function drawCoastline() {
|
|||
let vchain = connectVertices(start, type);
|
||||
if (features[f].type === "lake") relax(vchain, 1.2);
|
||||
used[f] = 1;
|
||||
let points = vchain.map(v => vertices.p[v]);
|
||||
let points = clipPoly(vchain.map(v => vertices.p[v]), 1);
|
||||
const area = d3.polygonArea(points); // area with lakes/islands
|
||||
if (area > 0 && features[f].type === "lake") {
|
||||
points = points.reverse();
|
||||
|
|
@ -1056,7 +1056,6 @@ function reMarkFeatures() {
|
|||
const start = queue[0]; // first cell
|
||||
cells.f[start] = i; // assign feature number
|
||||
const land = cells.h[start] >= 20;
|
||||
//const frozen = !land && temp[cells.g[start]] < -5; // check if water is frozen
|
||||
let border = false; // true if feature touches map border
|
||||
let cellNumber = 1; // to count cells number in a feature
|
||||
|
||||
|
|
@ -1075,7 +1074,6 @@ function reMarkFeatures() {
|
|||
else if (!cells.t[q] && cells.t[e] === 1) cells.t[q] = 2;
|
||||
}
|
||||
if (!cells.f[e] && land === eLand) {
|
||||
//if (!land && frozen !== temp[cells.g[e]] < -5) return;
|
||||
queue.push(e);
|
||||
cells.f[e] = i;
|
||||
cellNumber++;
|
||||
|
|
@ -1137,28 +1135,32 @@ function elevateLakes() {
|
|||
// assign biome id for each cell
|
||||
function defineBiomes() {
|
||||
console.time("defineBiomes");
|
||||
const cells = pack.cells, f = pack.features;
|
||||
const cells = pack.cells, f = pack.features, temp = grid.cells.temp, prec = grid.cells.prec;
|
||||
cells.biome = new Uint8Array(cells.i.length); // biomes array
|
||||
|
||||
for (const i of cells.i) {
|
||||
if (f[cells.f[i]].group === "freshwater") cells.h[i] = 19; // de-elevate lakes
|
||||
const temp = grid.cells.temp[cells.g[i]]; // temperature
|
||||
if (f[cells.f[i]].group === "freshwater") cells.h[i] = 19; // de-elevate lakes; here to save some resources
|
||||
const t = temp[cells.g[i]]; // cell temperature
|
||||
const h = cells.h[i]; // cell height
|
||||
const m = h < 20 ? 0 : calculateMoisture(i); // cell moisture
|
||||
cells.biome[i] = getBiomeId(m, t, h);
|
||||
}
|
||||
|
||||
if (cells.h[i] < 20 && temp > -6) continue; // liquid water cells have biome 0
|
||||
let moist = grid.cells.prec[cells.g[i]];
|
||||
function calculateMoisture(i) {
|
||||
let moist = prec[cells.g[i]];
|
||||
if (cells.r[i]) moist += Math.max(cells.fl[i] / 20, 2);
|
||||
const n = cells.c[i].filter(isLand).map(c => grid.cells.prec[cells.g[c]]).concat([moist]);
|
||||
moist = rn(4 + d3.mean(n));
|
||||
cells.biome[i] = getBiomeId(moist, temp, cells.h[i]);
|
||||
const n = cells.c[i].filter(isLand).map(c => prec[cells.g[c]]).concat([moist]);
|
||||
return rn(4 + d3.mean(n));
|
||||
}
|
||||
|
||||
console.timeEnd("defineBiomes");
|
||||
}
|
||||
|
||||
// assign biome id to a cell
|
||||
function getBiomeId(moisture, temperature, height) {
|
||||
if (temperature < -5) return 11; // permafrost biome, including sea ice
|
||||
if (height < 20) return 0; // liquid water cells have marine biome
|
||||
if (moisture > 40 && height < 25 || moisture > 24 && height > 24) return 12; // wetland biome
|
||||
if (height < 20) return 0; // marine biome: liquid water cells
|
||||
if (moisture > 40 && temperature > -2 && (height < 25 || moisture > 24 && height > 24)) return 12; // wetland biome
|
||||
const m = Math.min(moisture / 5 | 0, 4); // moisture band from 0 to 4
|
||||
const t = Math.min(Math.max(20 - temperature, 0), 25); // temparature band from 0 to 25
|
||||
return biomesData.biomesMartix[m][t];
|
||||
|
|
@ -1175,6 +1177,7 @@ function rankCells() {
|
|||
const areaMean = d3.mean(cells.area); // to adjust population by cell area
|
||||
|
||||
for (const i of cells.i) {
|
||||
if (cells.h[i] < 20) continue; // no population in water
|
||||
let s = +biomesData.habitability[cells.biome[i]]; // base suitability derived from biome habitability
|
||||
if (!s) continue; // uninhabitable biomes has 0 suitability
|
||||
if (flMean) s += normalize(cells.fl[i] + cells.conf[i], flMean, flMax) * 250; // big rivers and confluences are valued
|
||||
|
|
@ -1211,20 +1214,15 @@ function addMarkers(number = 1) {
|
|||
void function addVolcanoes() {
|
||||
let mounts = Array.from(cells.i).filter(i => cells.h[i] > 70).sort((a, b) => cells.h[b] - cells.h[a]);
|
||||
let count = mounts.length < 10 ? 0 : Math.ceil(mounts.length / 300 * number);
|
||||
if (count) addMarker("volcano", "🌋", 52, 52, 17.5);
|
||||
if (count) addMarker("volcano", "🌋", 52, 50, 13);
|
||||
|
||||
while (count && mounts.length) {
|
||||
const cell = mounts.splice(biased(0, mounts.length-1, 5), 1);
|
||||
const x = cells.p[cell][0], y = cells.p[cell][1];
|
||||
const id = getNextId("markerElement");
|
||||
markers.append("use").attr("id", id).attr("data-cell", cell)
|
||||
.attr("xlink:href", "#marker_volcano").attr("data-id", "#marker_volcano")
|
||||
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
|
||||
.attr("data-size", 1).attr("width", 30).attr("height", 30);
|
||||
const height = getFriendlyHeight([x, y]);
|
||||
const id = appendMarker(cell, "volcano");
|
||||
const proper = Names.getCulture(cells.culture[cell]);
|
||||
const name = P(.3) ? "Mount " + proper : Math.random() > .3 ? proper + " Volcano" : proper;
|
||||
notes.push({id, name, legend:`Active volcano. Height: ${height}`});
|
||||
notes.push({id, name, legend:`Active volcano. Height: ${getFriendlyHeight([x, y])}`});
|
||||
count--;
|
||||
}
|
||||
}()
|
||||
|
|
@ -1232,17 +1230,11 @@ function addMarkers(number = 1) {
|
|||
void function addHotSprings() {
|
||||
let springs = Array.from(cells.i).filter(i => cells.h[i] > 50).sort((a, b) => cells.h[b]-cells.h[a]);
|
||||
let count = springs.length < 30 ? 0 : Math.ceil(springs.length / 1000 * number);
|
||||
if (count) addMarker("hot_springs", "♨", 50, 50, 19.5);
|
||||
if (count) addMarker("hot_springs", "♨️", 50, 52, 12.5);
|
||||
|
||||
while (count && springs.length) {
|
||||
const cell = springs.splice(biased(1, springs.length-1, 3), 1);
|
||||
const x = cells.p[cell][0], y = cells.p[cell][1];
|
||||
const id = getNextId("markerElement");
|
||||
markers.append("use").attr("id", id)
|
||||
.attr("xlink:href", "#marker_hot_springs").attr("data-id", "#marker_hot_springs")
|
||||
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
|
||||
.attr("data-size", 1).attr("width", 30).attr("height", 30);
|
||||
|
||||
const id = appendMarker(cell, "hot_springs");
|
||||
const proper = Names.getCulture(cells.culture[cell]);
|
||||
const temp = convertTemperature(gauss(30,15,20,100));
|
||||
notes.push({id, name: proper + " Hot Springs", legend:`A hot springs area. Temperature: ${temp}`});
|
||||
|
|
@ -1255,17 +1247,12 @@ function addMarkers(number = 1) {
|
|||
let count = !hills.length ? 0 : Math.ceil(hills.length / 7 * number);
|
||||
if (!count) return;
|
||||
|
||||
addMarker("mine", "⚒", 50, 50, 20);
|
||||
addMarker("mine", "⛏️", 48, 50, 13.5);
|
||||
const resources = {"salt":5, "gold":2, "silver":4, "copper":2, "iron":3, "lead":1, "tin":1};
|
||||
|
||||
while (count && hills.length) {
|
||||
const cell = hills.splice(Math.floor(Math.random() * hills.length), 1);
|
||||
const x = cells.p[cell][0], y = cells.p[cell][1];
|
||||
const id = getNextId("markerElement");
|
||||
markers.append("use").attr("id", id)
|
||||
.attr("xlink:href", "#marker_mine").attr("data-id", "#marker_mine")
|
||||
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
|
||||
.attr("data-size", 1).attr("width", 30).attr("height", 30);
|
||||
const id = appendMarker(cell, "mine");
|
||||
const resource = rw(resources);
|
||||
const burg = pack.burgs[cells.burg[cell]];
|
||||
const name = `${burg.name} — ${resource} mining town`;
|
||||
|
|
@ -1285,17 +1272,11 @@ function addMarkers(number = 1) {
|
|||
.sort((a, b) => (cells.road[b] + cells.fl[b] / 10) - (cells.road[a] + cells.fl[a] / 10));
|
||||
|
||||
let count = !bridges.length ? 0 : Math.ceil(bridges.length / 12 * number);
|
||||
if (count) addMarker("bridge", "🌉", 50, 50, 16.5);
|
||||
if (count) addMarker("bridge", "🌉", 50, 50, 14);
|
||||
|
||||
while (count && bridges.length) {
|
||||
const cell = bridges.splice(0, 1);
|
||||
const x = cells.p[cell][0], y = cells.p[cell][1];
|
||||
const id = getNextId("markerElement");
|
||||
markers.append("use").attr("id", id)
|
||||
.attr("xlink:href", "#marker_bridge").attr("data-id", "#marker_bridge")
|
||||
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
|
||||
.attr("data-size", 1).attr("width", 30).attr("height", 30);
|
||||
|
||||
const id = appendMarker(cell, "bridge");
|
||||
const burg = pack.burgs[cells.burg[cell]];
|
||||
const river = pack.rivers.find(r => r.i === pack.cells.r[cell]);
|
||||
const riverName = river ? `${river.name} ${river.type}` : "river";
|
||||
|
|
@ -1310,7 +1291,7 @@ function addMarkers(number = 1) {
|
|||
let taverns = Array.from(cells.i).filter(i => cells.crossroad[i] && cells.h[i] >= 20 && cells.road[i] > maxRoad);
|
||||
if (!taverns.length) return;
|
||||
const count = Math.ceil(4 * number);
|
||||
addMarker("inn", "🍻", 50, 50, 17.5);
|
||||
addMarker("inn", "🍻", 50, 50, 14.5);
|
||||
|
||||
const color = ["Dark", "Light", "Bright", "Golden", "White", "Black", "Red", "Pink", "Purple", "Blue", "Green", "Yellow", "Amber", "Orange", "Brown", "Grey"];
|
||||
const animal = ["Antelope", "Ape", "Badger", "Bear", "Beaver", "Bison", "Boar", "Buffalo", "Cat", "Crane", "Crocodile", "Crow", "Deer", "Dog", "Eagle", "Elk", "Fox", "Goat", "Goose", "Hare", "Hawk", "Heron", "Horse", "Hyena", "Ibis", "Jackal", "Jaguar", "Lark", "Leopard", "Lion", "Mantis", "Marten", "Moose", "Mule", "Narwhal", "Owl", "Panther", "Rat", "Raven", "Rook", "Scorpion", "Shark", "Sheep", "Snake", "Spider", "Swan", "Tiger", "Turtle", "Wolf", "Wolverine", "Camel", "Falcon", "Hound", "Ox"];
|
||||
|
|
@ -1318,14 +1299,7 @@ function addMarkers(number = 1) {
|
|||
|
||||
for (let i=0; i < taverns.length && i < count; i++) {
|
||||
const cell = taverns.splice(Math.floor(Math.random() * taverns.length), 1);
|
||||
const x = cells.p[cell][0], y = cells.p[cell][1];
|
||||
const id = getNextId("markerElement");
|
||||
|
||||
markers.append("use").attr("id", id)
|
||||
.attr("xlink:href", "#marker_inn").attr("data-id", "#marker_inn")
|
||||
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
|
||||
.attr("data-size", 1).attr("width", 30).attr("height", 30);
|
||||
|
||||
const id = appendMarker(cell, "inn");
|
||||
const type = P(.3) ? "inn" : "tavern";
|
||||
const name = P(.5) ? ra(color) + " " + ra(animal) : P(.6) ? ra(adj) + " " + ra(animal) : ra(adj) + " " + capitalize(type);
|
||||
notes.push({id, name: "The " + name, legend:`A big and famous roadside ${type}`});
|
||||
|
|
@ -1340,14 +1314,7 @@ function addMarkers(number = 1) {
|
|||
|
||||
for (let i=0; i < lighthouses.length && i < count; i++) {
|
||||
const cell = lighthouses[i][0], vertex = lighthouses[i][1];
|
||||
const x = pack.vertices.p[vertex][0], y = pack.vertices.p[vertex][1];
|
||||
const id = getNextId("markerElement");
|
||||
|
||||
markers.append("use").attr("id", id)
|
||||
.attr("xlink:href", "#marker_lighthouse").attr("data-id", "#marker_lighthouse")
|
||||
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
|
||||
.attr("data-size", 1).attr("width", 30).attr("height", 30);
|
||||
|
||||
const id = appendMarker(cell, "lighthouse");
|
||||
const proper = cells.burg[cell] ? pack.burgs[cells.burg[cell]].name : Names.getCulture(cells.culture[cell]);
|
||||
notes.push({id, name: getAdjective(proper) + " Lighthouse" + name, legend:`A lighthouse to keep the navigation safe`});
|
||||
}
|
||||
|
|
@ -1360,14 +1327,7 @@ function addMarkers(number = 1) {
|
|||
|
||||
for (let i=0; i < waterfalls.length && i < count; i++) {
|
||||
const cell = waterfalls[i];
|
||||
const x = cells.p[cell][0], y = cells.p[cell][1];
|
||||
const id = getNextId("markerElement");
|
||||
|
||||
markers.append("use").attr("id", id)
|
||||
.attr("xlink:href", "#marker_waterfall").attr("data-id", "#marker_waterfall")
|
||||
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
|
||||
.attr("data-size", 1).attr("width", 30).attr("height", 30);
|
||||
|
||||
const id = appendMarker(cell, "waterfall");
|
||||
const proper = cells.burg[cell] ? pack.burgs[cells.burg[cell]].name : Names.getCulture(cells.culture[cell]);
|
||||
notes.push({id, name: getAdjective(proper) + " Waterfall" + name, legend:`An extremely beautiful waterfall`});
|
||||
}
|
||||
|
|
@ -1376,17 +1336,11 @@ function addMarkers(number = 1) {
|
|||
void function addBattlefields() {
|
||||
let battlefields = Array.from(cells.i).filter(i => cells.state[i] && cells.pop[i] > 2 && cells.h[i] < 50 && cells.h[i] > 25);
|
||||
let count = battlefields.length < 100 ? 0 : Math.ceil(battlefields.length / 500 * number);
|
||||
if (count) addMarker("battlefield", "⚔", 50, 50, 20);
|
||||
if (count) addMarker("battlefield", "⚔️", 50, 52, 12);
|
||||
|
||||
while (count && battlefields.length) {
|
||||
const cell = battlefields.splice(Math.floor(Math.random() * battlefields.length), 1);
|
||||
const x = cells.p[cell][0], y = cells.p[cell][1];
|
||||
const id = getNextId("markerElement");
|
||||
markers.append("use").attr("id", id)
|
||||
.attr("xlink:href", "#marker_battlefield").attr("data-id", "#marker_battlefield")
|
||||
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
|
||||
.attr("data-size", 1).attr("width", 30).attr("height", 30);
|
||||
|
||||
const id = appendMarker(cell, "battlefield");
|
||||
const campaign = ra(states[cells.state[cell]].campaigns);
|
||||
const date = generateDate(campaign.start, campaign.end);
|
||||
const name = Names.getCulture(cells.culture[cell]) + " Battlefield";
|
||||
|
|
@ -1407,6 +1361,19 @@ function addMarkers(number = 1) {
|
|||
.attr("font-size", size+"px").attr("dominant-baseline", "central").text(icon);
|
||||
}
|
||||
|
||||
function appendMarker(cell, type) {
|
||||
const x = cells.p[cell][0], y = cells.p[cell][1];
|
||||
const id = getNextId("markerElement");
|
||||
const name = "#marker_" + type;
|
||||
|
||||
markers.append("use").attr("id", id)
|
||||
.attr("xlink:href", name).attr("data-id", name)
|
||||
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
|
||||
.attr("data-size", 1).attr("width", 30).attr("height", 30);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
console.timeEnd("addMarkers");
|
||||
}
|
||||
|
||||
|
|
@ -1593,14 +1560,11 @@ function addZones(number = 1) {
|
|||
}
|
||||
|
||||
function addEruption() {
|
||||
const volcanoes = [];
|
||||
markers.selectAll("use[data-id='#marker_volcano']").each(function() {
|
||||
volcanoes.push(this.dataset.cell);
|
||||
});
|
||||
if (!volcanoes.length) return;
|
||||
const volcano = document.getElementById("markers").querySelector("use[data-id='#marker_volcano']");
|
||||
if (!volcano) return;
|
||||
|
||||
const cell = +ra(volcanoes);
|
||||
const id = markers.select("use[data-cell='"+cell+"']").attr("id");
|
||||
const x = +volcano.dataset.x, y = +volcano.dataset.y, cell = findCell(x, y);
|
||||
const id = volcano.id;
|
||||
const note = notes.filter(n => n.id === id);
|
||||
|
||||
if (note[0]) note[0].legend = note[0].legend.replace("Active volcano", "Erupting volcano");
|
||||
|
|
@ -1613,7 +1577,7 @@ function addZones(number = 1) {
|
|||
cellsArray.push(q);
|
||||
if (cellsArray.length > power) break;
|
||||
cells.c[q].forEach(e => {
|
||||
if (used[e]) return;
|
||||
if (used[e] || cells.h[e] < 20) return;
|
||||
used[e] = 1;
|
||||
queue.push(e);
|
||||
});
|
||||
|
|
@ -1734,18 +1698,20 @@ function showStatistics() {
|
|||
const template = templateInput.value;
|
||||
const templateRandom = locked("template") ? "" : "(random)";
|
||||
const stats = ` Seed: ${seed}
|
||||
Canvas size: ${graphWidth}x${graphHeight}
|
||||
Template: ${template} ${templateRandom}
|
||||
Points: ${grid.points.length}
|
||||
Cells: ${pack.cells.i.length}
|
||||
Map size: ${mapSizeOutput.value}%
|
||||
States: ${pack.states.length-1}
|
||||
Provinces: ${pack.provinces.length-1}
|
||||
Burgs: ${pack.burgs.length-1}
|
||||
Religions: ${pack.religions.length-1}
|
||||
Culture set: ${culturesSet.selectedOptions[0].innerText}
|
||||
Cultures: ${pack.cultures.length-1}`;
|
||||
mapHistory.push({seed, width:graphWidth, height:graphHeight, template, created: Date.now()});
|
||||
Canvas size: ${graphWidth}x${graphHeight}
|
||||
Template: ${template} ${templateRandom}
|
||||
Points: ${grid.points.length}
|
||||
Cells: ${pack.cells.i.length}
|
||||
Map size: ${mapSizeOutput.value}%
|
||||
States: ${pack.states.length-1}
|
||||
Provinces: ${pack.provinces.length-1}
|
||||
Burgs: ${pack.burgs.length-1}
|
||||
Religions: ${pack.religions.length-1}
|
||||
Culture set: ${culturesSet.selectedOptions[0].innerText}
|
||||
Cultures: ${pack.cultures.length-1}`;
|
||||
|
||||
mapId = Date.now(); // unique map id is it's creation date number
|
||||
mapHistory.push({seed, width:graphWidth, height:graphHeight, template, created:mapId});
|
||||
console.log(stats);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -140,46 +140,15 @@
|
|||
// define burg coordinates, port status and define details
|
||||
const specifyBurgs = function() {
|
||||
console.time("specifyBurgs");
|
||||
const cells = pack.cells, vertices = pack.vertices, features = pack.features;
|
||||
|
||||
// separate arctic seas for correct searoutes generation
|
||||
void function checkAccessibility() {
|
||||
const oceanCells = cells.i.filter(i => cells.h[i] < 20 && features[cells.f[i]].type === "ocean");
|
||||
const marked = [];
|
||||
let firstCell = oceanCells.find(i => !marked[i]);
|
||||
|
||||
while (firstCell !== undefined) {
|
||||
const queue = [firstCell];
|
||||
const f = features[cells.f[firstCell]]; // old feature
|
||||
const i = last(features).i+1; // new feature id to assign
|
||||
const biome = cells.biome[firstCell];
|
||||
marked[firstCell] = 1;
|
||||
let cellNumber = 1;
|
||||
|
||||
while (queue.length) {
|
||||
for (const c of cells.c[queue.pop()]) {
|
||||
if (cells.biome[c] !== biome || cells.h[c] >= 20) continue;
|
||||
if (marked[c]) continue;
|
||||
queue.push(c);
|
||||
cells.f[c] = i;
|
||||
marked[c] = 1;
|
||||
cellNumber++;
|
||||
}
|
||||
}
|
||||
|
||||
const group = biome ? "frozen " + f.group : f.group;
|
||||
features.push({i, parent:f.i, land:false, border:f.border, type:"ocean", cells: cellNumber, firstCell, group});
|
||||
firstCell = oceanCells.find(i => !marked[i]);
|
||||
}
|
||||
}()
|
||||
const cells = pack.cells, vertices = pack.vertices, features = pack.features, temp = grid.cells.temp;
|
||||
|
||||
for (const b of pack.burgs) {
|
||||
if (!b.i) continue;
|
||||
const i = b.cell;
|
||||
|
||||
// asign port status
|
||||
// asign port status to some coastline burgs with temp > 0 °C
|
||||
const haven = cells.haven[i];
|
||||
if (haven && cells.biome[haven] === 0) {
|
||||
if (haven && temp[cells.g[i]] > 0) {
|
||||
const f = cells.f[haven]; // water body id
|
||||
// port is a capital with any harbor OR town with good harbor
|
||||
const port = features[f].cells > 1 && ((b.capital && cells.harbor[i]) || cells.harbor[i] === 1);
|
||||
|
|
@ -187,7 +156,7 @@
|
|||
} else b.port = 0;
|
||||
|
||||
// define burg population (keep urbanization at about 10% rate)
|
||||
b.population = rn(Math.max((cells.s[i] + cells.road[i]) / 8 + b.i / 1000 + i % 100 / 1000, .1), 3);
|
||||
b.population = rn(Math.max((cells.s[i] + cells.road[i] / 2) / 8 + b.i / 1000 + i % 100 / 1000, .1), 3);
|
||||
if (b.capital) b.population = rn(b.population * 1.3, 3); // increase capital population
|
||||
|
||||
if (b.port) {
|
||||
|
|
@ -219,8 +188,8 @@
|
|||
console.timeEnd("specifyBurgs");
|
||||
}
|
||||
|
||||
const defineBurgFeatures = function() {
|
||||
pack.burgs.filter(b => b.i && !b.removed).forEach(b => {
|
||||
const defineBurgFeatures = function(newburg) {
|
||||
pack.burgs.filter(b => newburg ? b.i == newburg.i : (b.i && !b.removed)).forEach(b => {
|
||||
const pop = b.population;
|
||||
b.citadel = b.capital || pop > 50 && P(.75) || P(.5) ? 1 : 0;
|
||||
b.plaza = pop > 50 || pop > 30 && P(.75) || pop > 10 && P(.5) || P(.25) ? 1 : 0;
|
||||
|
|
@ -420,7 +389,7 @@
|
|||
const passableLake = features[cells.f[c]].type === "lake" && features[cells.f[c]].cells < maxLake;
|
||||
if (cells.b[c] || (cells.state[c] !== state && !passableLake)) {hull.add(cells.v[q][d]); return;}
|
||||
const nC = cells.c[c].filter(n => cells.state[n] === state);
|
||||
const intersected = intersect(nQ, nC).length
|
||||
const intersected = common(nQ, nC).length
|
||||
if (hull.size > 20 && !intersected && !passableLake) {hull.add(cells.v[q][d]); return;}
|
||||
if (used[c]) return;
|
||||
used[c] = 1;
|
||||
|
|
@ -691,7 +660,7 @@
|
|||
|
||||
const naval = states[f].type === "Naval" && states[t].type === "Naval" && cells.f[states[f].center] !== cells.f[states[t].center];
|
||||
const neib = naval ? false : states[f].neighbors.includes(t);
|
||||
const neibOfNeib = naval || neib ? false : states[f].neighbors.map(n => states[n].neighbors).join().includes(t);
|
||||
const neibOfNeib = naval || neib ? false : states[f].neighbors.map(n => states[n].neighbors).join("").includes(t);
|
||||
|
||||
let status = naval ? rw(navals) : neib ? rw(neibs) : neibOfNeib ? rw(neibsOfNeibs) : rw(far);
|
||||
|
||||
|
|
@ -810,7 +779,7 @@
|
|||
});
|
||||
|
||||
const monarchy = ["Duchy", "Grand Duchy", "Principality", "Kingdom", "Empire"]; // per expansionism tier
|
||||
const republic = {Republic:70, Federation:2, Oligarchy:2, Tetrarchy:1, Triumvirate:1, Diarchy:1, "Trade Company":3}; // weighted random
|
||||
const republic = {Republic:75, Federation:4, Oligarchy: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
|
||||
|
||||
for (const s of states) {
|
||||
|
|
@ -870,11 +839,13 @@
|
|||
if (s.form === "Union") return rw(union);
|
||||
|
||||
if (s.form === "Theocracy") {
|
||||
// default name is "Theocracy", some culture bases have special names
|
||||
if ([0, 1, 2, 3, 4, 6, 8, 9, 13, 15, 20].includes(base)) return "Diocese"; // Euporean
|
||||
if ([7, 5].includes(base)) return "Eparchy"; // Greek, Ruthenian
|
||||
if ([21, 16].includes(base)) return "Imamah"; // Nigerian, Turkish
|
||||
if ([18, 17, 28].includes(base)) return "Caliphate"; // Arabic, Berber, Swahili
|
||||
// default name is "Theocracy"
|
||||
if (P(.5) && [0, 1, 2, 3, 4, 6, 8, 9, 13, 15, 20].includes(base)) return "Diocese"; // Euporean
|
||||
if (P(.9) && [7, 5].includes(base)) return "Eparchy"; // Greek, Ruthenian
|
||||
if (P(.9) && [21, 16].includes(base)) return "Imamah"; // Nigerian, Turkish
|
||||
if (P(.8) && [18, 17, 28].includes(base)) return "Caliphate"; // Arabic, Berber, Swahili
|
||||
if (P(.02)) return "Thearchy"; // "Thearchy" in very rare case
|
||||
if (P(.05)) return "See"; // "See" in rare case
|
||||
return "Theocracy";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,18 @@
|
|||
"magical": {"Nomadic":1, "Highland":2, "Lake":1, "Naval":1, "Hunting":1, "River":1}
|
||||
};
|
||||
|
||||
const cellTypeModifier = {
|
||||
"nomadic": {"melee":.2, "ranged":.5, "mounted":3, "machinery":.4, "naval":.3, "armored":1.6, "aviation":1, "magical":.5},
|
||||
"wetland": {"melee":.8, "ranged":2, "mounted":0.3, "machinery":1.2, "naval":1.0, "armored":0.2, "aviation":.5, "magical":0.5},
|
||||
"highland": {"melee":1.2, "ranged":1.6, "mounted":0.3, "machinery":3, "naval":1.0, "armored":0.8, "aviation":.3, "magical":2}
|
||||
}
|
||||
|
||||
const burgTypeModifier = {
|
||||
"nomadic": {"melee":.3, "ranged":.8, "mounted":3, "machinery":.4, "naval":1.0, "armored":1.6, "aviation":1, "magical":0.5},
|
||||
"wetland": {"melee":1, "ranged":1.6, "mounted":.2, "machinery":1.2, "naval":1.0, "armored":0.2, "aviation":0.5, "magical":0.5},
|
||||
"highland": {"melee":1.2, "ranged":2, "mounted":.3, "machinery":3, "naval":1.0, "armored":0.8, "aviation":0.3, "magical":2}
|
||||
}
|
||||
|
||||
valid.forEach(s => {
|
||||
const temp = s.temp = {}, d = s.diplomacy;
|
||||
const expansionRate = Math.min(Math.max((s.expansionism / expn) / (s.area / area), .25), 4); // how much state expansionism is realized
|
||||
|
|
@ -47,6 +59,13 @@
|
|||
|
||||
});
|
||||
|
||||
const getType = cell => {
|
||||
if ([1, 2, 3, 4].includes(cells.biome[cell])) return "nomadic";
|
||||
if ([7, 8, 9, 12].includes(cells.biome[cell])) return "wetland";
|
||||
if (cells.h[cell] >= 70) return "highland";
|
||||
return "generic";
|
||||
}
|
||||
|
||||
for (const i of cells.i) {
|
||||
if (!cells.pop[i]) continue;
|
||||
const s = states[cells.state[i]]; // cell state
|
||||
|
|
@ -56,39 +75,19 @@
|
|||
if (cells.culture[i] !== s.culture) m = s.form === "Union" ? m / 1.2 : m / 2; // non-dominant culture
|
||||
if (cells.religion[i] !== cells.religion[s.center]) m = s.form === "Theocracy" ? m / 2.2 : m / 1.4; // non-dominant religion
|
||||
if (cells.f[i] !== cells.f[s.center]) m = s.type === "Naval" ? m / 1.2 : m / 1.8; // different landmass
|
||||
|
||||
const nomadic = [1, 2, 3, 4].includes(cells.biome[i]);
|
||||
const wetland = [7, 8, 9, 12].includes(cells.biome[i]);
|
||||
const highland = cells.h[i] >= 70;
|
||||
const type = getType(i);
|
||||
|
||||
for (const u of options.military) {
|
||||
const perc = +u.rural;
|
||||
if (isNaN(perc) || perc <= 0 || !s.temp[u.name]) continue;
|
||||
|
||||
let army = m * perc; // basic army for rural cell
|
||||
if (nomadic) { // "nomadic" biomes special rules
|
||||
if (u.type === "melee") army /= 5; else
|
||||
if (u.type === "ranged") army /= 2; else
|
||||
if (u.type === "mounted") army *= 3;
|
||||
}
|
||||
|
||||
if (wetland) { // "wet" biomes special rules
|
||||
if (u.type === "melee") army *= 1.2; else
|
||||
if (u.type === "ranged") army *= 1.4; else
|
||||
if (u.type === "mounted") army /= 3;
|
||||
}
|
||||
|
||||
if (highland) { // highlands special rules
|
||||
if (u.type === "melee") army *= 1.2; else
|
||||
if (u.type === "ranged") army *= 1.6; else
|
||||
if (u.type === "mounted") army /= 3;
|
||||
}
|
||||
|
||||
const t = rn(army * s.temp[u.name] * populationRate.value);
|
||||
const mod = type === "generic" ? 1 : cellTypeModifier[type][u.type] // cell specific modifier
|
||||
const army = m * perc * mod; // rural cell army
|
||||
const t = rn(army * s.temp[u.name] * populationRate.value); // total troops
|
||||
if (!t) continue;
|
||||
let x = p[i][0], y = p[i][1], n = 0;
|
||||
if (u.type === "naval") {let haven = cells.haven[i]; x = p[haven][0], y = p[haven][1]; n = 1}; // place naval to sea
|
||||
s.temp.platoons.push({cell: i, a:t, t, x, y, u:u.name, n, s:u.separate});
|
||||
s.temp.platoons.push({cell: i, a:t, t, x, y, u:u.name, n, s:u.separate, type:u.type});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -101,47 +100,20 @@
|
|||
if (b.culture !== s.culture) m = s.form === "Union" ? m / 1.2 : m / 2; // non-dominant culture
|
||||
if (cells.religion[b.cell] !== cells.religion[s.center]) m = s.form === "Theocracy" ? m / 2.2 : m / 1.4; // non-dominant religion
|
||||
if (cells.f[b.cell] !== cells.f[s.center]) m = s.type === "Naval" ? m / 1.2 : m / 1.8; // different landmass
|
||||
|
||||
const biome = cells.biome[b.cell]; // burg biome
|
||||
const nomadic = [1, 2, 3, 4].includes(biome);
|
||||
const wetland = [7, 8, 9, 12].includes(biome);
|
||||
const highland = cells.h[b.cell] >= 70;
|
||||
const type = getType(b.cell);
|
||||
|
||||
for (const u of options.military) {
|
||||
if (u.type === "naval" && !b.port) continue; // only ports produce naval units
|
||||
const perc = +u.urban;
|
||||
if (isNaN(perc) || perc <= 0 || !s.temp[u.name]) continue;
|
||||
let army = m * perc; // basic army for rural cell
|
||||
|
||||
if (u.type === "naval" && !b.port) continue; // only ports produce naval units
|
||||
|
||||
if (nomadic) { // "nomadic" biomes special rules
|
||||
if (u.type === "melee") army /= 3; else
|
||||
if (u.type === "machinery") army /= 2; else
|
||||
if (u.type === "mounted") army *= 3; else
|
||||
if (u.type === "armored") army *= 2;
|
||||
}
|
||||
|
||||
if (wetland) { // "wet" biomes special rules
|
||||
if (u.type === "melee") army *= 1.2; else
|
||||
if (u.type === "ranged") army *= 1.4; else
|
||||
if (u.type === "machinery") army *= 1.2; else
|
||||
if (u.type === "mounted") army /= 4; else
|
||||
if (u.type === "armored") army /= 3;
|
||||
}
|
||||
|
||||
if (highland) { // highlands special rules
|
||||
if (u.type === "ranged") army *= 2; else
|
||||
if (u.type === "naval") army /= 3; else
|
||||
if (u.type === "mounted") army /= 3; else
|
||||
if (u.type === "armored") army /= 2; else
|
||||
if (u.type === "magical") army *= 2;
|
||||
}
|
||||
|
||||
const t = rn(army * s.temp[u.name] * populationRate.value);
|
||||
const mod = type === "generic" ? 1 : burgTypeModifier[type][u.type] // cell specific modifier
|
||||
const army = m * perc * mod; // urban cell army
|
||||
const t = rn(army * s.temp[u.name] * populationRate.value); // total troops
|
||||
if (!t) continue;
|
||||
let x = p[b.cell][0], y = p[b.cell][1], n = 0;
|
||||
if (u.type === "naval") {let haven = cells.haven[b.cell]; x = p[haven][0], y = p[haven][1]; n = 1}; // place naval to sea
|
||||
s.temp.platoons.push({cell: b.cell, a:t, t, x, y, u:u.name, n, s:u.separate});
|
||||
if (u.type === "naval") {let haven = cells.haven[b.cell]; x = p[haven][0], y = p[haven][1]; n = 1}; // place naval in sea cell
|
||||
s.temp.platoons.push({cell: b.cell, a:t, t, x, y, u:u.name, n, s:u.separate, type:u.type});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -154,7 +126,8 @@
|
|||
}()
|
||||
|
||||
const expected = 3 * populationRate.value; // expected regiment size
|
||||
const mergeable = (n, s) => (!n.s && !s.s) || n.u === s.u;
|
||||
const mergeable = (n0, n1) => (!n0.s && !n1.s) || n0.type === n1.type; // check if regiments can be merged
|
||||
|
||||
// get regiments for each state
|
||||
valid.forEach(s => {
|
||||
s.military = createRegiments(s.temp.platoons, s);
|
||||
|
|
@ -164,7 +137,7 @@
|
|||
|
||||
function createRegiments(nodes, s) {
|
||||
if (!nodes.length) return [];
|
||||
nodes.sort((a,b) => a.a - b.a);
|
||||
nodes.sort((a,b) => a.a - b.a); // form regiments in cells with most troops
|
||||
const tree = d3.quadtree(nodes, d => d.x, d => d.y);
|
||||
nodes.forEach(n => {
|
||||
tree.remove(n);
|
||||
|
|
@ -186,11 +159,11 @@
|
|||
n0.t = 0;
|
||||
}
|
||||
|
||||
// parse regiments data to easy-readable json
|
||||
// parse regiments data
|
||||
const regiments = nodes.filter(n => n.t).sort((a,b) => b.t - a.t).map((r, i) => {
|
||||
const u = {}; u[r.u] = r.a;
|
||||
(r.childen||[]).forEach(n => u[n.u] = u[n.u] ? u[n.u] += n.a : n.a);
|
||||
return {i, a:r.t, cell:r.cell, x:r.x, y:r.y, bx:r.x, by:r.y, u, n:r.n, name};
|
||||
return {i, a:r.t, cell:r.cell, x:r.x, y:r.y, bx:r.x, by:r.y, u, n:r.n, name, state: s.i};
|
||||
});
|
||||
|
||||
// generate name for regiments
|
||||
|
|
@ -208,11 +181,11 @@
|
|||
|
||||
const getDefaultOptions = function() {
|
||||
return [
|
||||
{name:"infantry", rural:.25, urban:.2, crew:1, type:"melee", separate:0},
|
||||
{name:"archers", rural:.12, urban:.2, crew:1, type:"ranged", separate:0},
|
||||
{name:"cavalry", rural:.12, urban:.03, crew:3, type:"mounted", separate:0},
|
||||
{name:"artillery", rural:0, urban:.03, crew:8, type:"machinery", separate:0},
|
||||
{name:"fleet", rural:0, urban:.015, crew:100, type:"naval", separate:1}
|
||||
{icon: "⚔️", name:"infantry", rural:.25, urban:.2, crew:1, power:1, type:"melee", separate:0},
|
||||
{icon: "🏹", name:"archers", rural:.12, urban:.2, crew:1, power:1, type:"ranged", separate:0},
|
||||
{icon: "🐴", name:"cavalry", rural:.12, urban:.03, crew:2, power:2, type:"mounted", separate:0},
|
||||
{icon: "💣", name:"artillery", rural:0, urban:.03, crew:8, power:12, type:"machinery", separate:0},
|
||||
{icon: "🌊", name:"fleet", rural:0, urban:.015, crew:100, power:50, type:"naval", separate:1}
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -256,6 +229,26 @@
|
|||
g.append("text").attr("class", "regimentIcon").attr("x", x1-size).attr("y", reg.y).text(reg.icon);
|
||||
}
|
||||
|
||||
// move one regiment to another
|
||||
const moveRegiment = function(reg, x, y) {
|
||||
const el = armies.select("g#army"+reg.state).select("g#regiment"+reg.state+"-"+reg.i);
|
||||
if (!el.size()) return;
|
||||
|
||||
const duration = Math.hypot(reg.x - x, reg.y - y) * 8;
|
||||
reg.x = x; reg.y = y;
|
||||
const size = +armies.attr("box-size");
|
||||
const w = reg.n ? size * 4 : size * 6;
|
||||
const h = size * 2;
|
||||
const x1 = x => rn(x - w / 2, 2);
|
||||
const y1 = y => rn(y - size, 2);
|
||||
|
||||
const move = d3.transition().duration(duration).ease(d3.easeSinInOut);
|
||||
el.select("rect").transition(move).attr("x", x1(x)).attr("y", y1(y));
|
||||
el.select("text").transition(move).attr("x", x).attr("y", y);
|
||||
el.selectAll("rect:nth-of-type(2)").transition(move).attr("x", x1(x)-h).attr("y", y1(y));
|
||||
el.select(".regimentIcon").transition(move).attr("x", x1(x)-size).attr("y", y);
|
||||
}
|
||||
|
||||
// utilize si function to make regiment total text fit regiment box
|
||||
const getTotal = reg => reg.a > (reg.n ? 999 : 99999) ? si(reg.a) : reg.a;
|
||||
|
||||
|
|
@ -270,17 +263,11 @@
|
|||
|
||||
// get default regiment emblem
|
||||
const getEmblem = function(r) {
|
||||
if (r.n) return "🌊";
|
||||
if (!Object.values(r.u).length) return "🛡️";
|
||||
const mainUnit = Object.entries(r.u).sort((a,b) => b[1]-a[1])[0][0];
|
||||
const type = options.military.find(u => u.name === mainUnit).type;
|
||||
if (type === "ranged") return "🏹";
|
||||
if (type === "mounted") return "🐴";
|
||||
if (type === "machinery") return "💣";
|
||||
if (type === "armored") return "🐢";
|
||||
if (type === "aviation") return "🦅";
|
||||
if (type === "magical") return "🔮";
|
||||
else return "⚔️";
|
||||
if (!r.n && !Object.values(r.u).length) return "🔰"; // "Newbie" regiment without troops
|
||||
if (!r.n && pack.states[r.state].form === "Monarchy" && cells.burg[r.cell] && pack.burgs[cells.burg[r.cell]].capital) return "👑"; // "Royal" regiment based in capital
|
||||
const mainUnit = Object.entries(r.u).sort((a,b) => b[1]-a[1])[0][0]; // unit with more troops in regiment
|
||||
const unit = options.military.find(u => u.name === mainUnit);
|
||||
return unit.icon;
|
||||
}
|
||||
|
||||
const generateNote = function(r, s) {
|
||||
|
|
@ -289,7 +276,7 @@
|
|||
const station = base ? `${r.name} is ${r.n ? "based" : "stationed"} in ${base}. ` : "";
|
||||
|
||||
const composition = r.a ? Object.keys(r.u).map(t => `— ${t}: ${r.u[t]}`).join("\r\n") : null;
|
||||
const troops = composition ? `\r\n\r\nRegiment composition:\r\n${composition}.` : "";
|
||||
const troops = composition ? `\r\n\r\nRegiment composition in ${options.year} ${options.eraShort}:\r\n${composition}.` : "";
|
||||
|
||||
const campaign = s.campaigns ? ra(s.campaigns) : null;
|
||||
const year = campaign ? rand(campaign.start, campaign.end) : gauss(options.year-100, 150, 1, options.year-6);
|
||||
|
|
@ -298,17 +285,6 @@
|
|||
notes.push({id:`regiment${s.i}-${r.i}`, name:`${r.icon} ${r.name}`, legend});
|
||||
}
|
||||
|
||||
// const updateNote = function(r, s) {
|
||||
// const id = `regiment${s}-${r.i}`;
|
||||
// const note = notes.find(n => n.id === id);
|
||||
// if (!note) return;
|
||||
|
||||
// const oldComposition = note.legend.split("composition:\r\n")[1]||"".split(".")[0];
|
||||
// if (!oldComposition) return;
|
||||
// const newComposition = Object.keys(r.u).map(t => `— ${t}: ${r.u[t]}`).join("\r\n") + ".";
|
||||
// note.legend = note.legend.replace(oldComposition, newComposition);
|
||||
// }
|
||||
|
||||
return {generate, getDefaultOptions, getName, generateNote, drawRegiments, drawRegiment, getTotal, getEmblem};
|
||||
return {generate, getDefaultOptions, getName, generateNote, drawRegiments, drawRegiment, moveRegiment, getTotal, getEmblem};
|
||||
|
||||
})));
|
||||
|
|
@ -8,25 +8,41 @@
|
|||
|
||||
// calculate Markov chain for a namesbase
|
||||
const calculateChain = function(string) {
|
||||
const chain = [];
|
||||
const d = string.toLowerCase().replace(/,/g, " ");
|
||||
const chain = [], array = string.split(",");
|
||||
|
||||
for (let i = -1, str = ""; i < d.length - 2; i += str.length, str = "") {
|
||||
let v = 0, f = " ";
|
||||
for (const n of array) {
|
||||
let name = n.trim().toLowerCase();
|
||||
const basic = !(/[^\u0000-\u007f]/.test(name)); // basic chars and English rules can be applied
|
||||
|
||||
for (let c=i+1; str.length < 5; c++) {
|
||||
if (d[c] === undefined) break;
|
||||
str += d[c];
|
||||
if (str === " ") break;
|
||||
if (d[c] !== "o" && d[c] !== "e" && vowel(d[c]) && d[c+1] === d[c]) break;
|
||||
if (d[c+2] === " ") {str += d[c+1]; break;}
|
||||
if (vowel(d[c])) v++;
|
||||
if (v && vowel(d[c+2])) break;
|
||||
// split word into pseudo-syllables
|
||||
for (let i=-1, syllable = ""; i < name.length; i += (syllable.length||1), syllable = "") {
|
||||
let prev = name[i] || ""; // pre-onset letter
|
||||
let v = 0; // 0 if no vowels in syllable
|
||||
|
||||
for (let c=i+1; name[c] && syllable.length < 5; c++) {
|
||||
const that = name[c], next = name[c+1]; // next char
|
||||
syllable += that;
|
||||
if (syllable === " " || syllable === "-") break; // syllable starts with space or hyphen
|
||||
if (!next || next === " " || next === "-") break; // no need to check
|
||||
|
||||
if (vowel(that)) v = 1; // check if letter is vowel
|
||||
|
||||
// do not split some diphthongs
|
||||
if (that === "y" && next === "e") continue; // 'ye'
|
||||
if (basic) { // English-like
|
||||
if (that === "o" && next === "o") continue; // 'oo'
|
||||
if (that === "e" && next === "e") continue; // 'ee'
|
||||
if (that === "a" && next === "e") continue; // 'ae'
|
||||
if (that === "c" && next === "h") continue; // 'ch'
|
||||
}
|
||||
|
||||
if (vowel(that) === next) break; // two same vowels in a row
|
||||
if (v && vowel(name[c+2])) break; // syllable has vowel and additional vowel is expected soon
|
||||
}
|
||||
|
||||
if (chain[prev] === undefined) chain[prev] = [];
|
||||
chain[prev].push(syllable);
|
||||
}
|
||||
|
||||
if (i >= 0) f = d[i];
|
||||
if (chain[f] === undefined) chain[f] = [];
|
||||
chain[f].push(str);
|
||||
}
|
||||
|
||||
return chain;
|
||||
|
|
@ -39,12 +55,12 @@
|
|||
const clearChains = () => chains = [];
|
||||
|
||||
// generate name using Markov's chain
|
||||
const getBase = function(base, min, max, dupl, multi) {
|
||||
const getBase = function(base, min, max, dupl) {
|
||||
if (base === undefined) {console.error("Please define a base"); return;}
|
||||
if (!chains[base]) updateChain(base);
|
||||
|
||||
const data = chains[base];
|
||||
if (!data || data[" "] === undefined) {
|
||||
if (!data || data[""] === undefined) {
|
||||
tip("Namesbase " + base + " is incorrect. Please check in namesbase editor", false, "error");
|
||||
console.error("Namebase " + base + " is incorrect!");
|
||||
return "ERROR";
|
||||
|
|
@ -53,31 +69,26 @@
|
|||
if (!min) min = nameBases[base].min;
|
||||
if (!max) max = nameBases[base].max;
|
||||
if (dupl !== "") dupl = nameBases[base].d;
|
||||
if (!multi) multi = nameBases[base].m;
|
||||
|
||||
let v = data[" "], cur = v[rand(v.length-1)], w = "";
|
||||
for (let i=0; i < 21; i++) {
|
||||
if (cur === " " && Math.random() > multi) {
|
||||
if (w.length < min) {cur = ""; w = ""; v = data[" "];} else break;
|
||||
let v = data[""], cur = ra(v), w = "";
|
||||
for (let i=0; i < 20; i++) {
|
||||
if (cur === "") { // end of word
|
||||
if (w.length < min) {cur = ""; w = ""; v = data[""];} else break;
|
||||
} else {
|
||||
if ((w+cur).length > max) {
|
||||
if (w.length + cur.length > max) { // word too long
|
||||
if (w.length < min) w += cur;
|
||||
break;
|
||||
} else if (cur === " " && w.length+1 < min) {
|
||||
cur = "";
|
||||
v = data[" "];
|
||||
} else {
|
||||
v = data[cur.slice(-1)] || data[" "];
|
||||
}
|
||||
} else v = data[last(cur)] || data[""];
|
||||
}
|
||||
|
||||
w += cur;
|
||||
cur = v[rand(v.length - 1)];
|
||||
cur = ra(v);
|
||||
}
|
||||
|
||||
// parse word to get a final name
|
||||
const l = last(w); // last letter
|
||||
if (l === "'" || l === " ") w = w.slice(0,-1); // not allow apostrophe and space at the end
|
||||
if (l === "'" || l === " " || l === "-") w = w.slice(0,-1); // not allow some characters at the end
|
||||
const basic = !(/[^\u0000-\u007f]/.test(w)); // true if word has only basic characters
|
||||
|
||||
let name = [...w].reduce(function(r, c, i, d) {
|
||||
if (c === d[i+1] && !dupl.includes(c)) return r; // duplication is not allowed
|
||||
|
|
@ -86,8 +97,8 @@
|
|||
if (r.slice(-1) === " ") return r + c.toUpperCase(); // capitalize letter after space
|
||||
if (r.slice(-1) === "-") return r + c.toUpperCase(); // capitalize letter after hyphen
|
||||
if (c === "a" && d[i+1] === "e") return r; // "ae" => "e"
|
||||
if (i+1 < d.length && !vowel(c) && !vowel(d[i-1]) && !vowel(d[i+1])) return r; // remove consonant between 2 consonants
|
||||
if (i+2 < d.length && c === d[i+1] && c === d[i+2]) return r; // remove tree same letters in a row
|
||||
if (basic && i+1 < d.length && !vowel(c) && !vowel(d[i-1]) && !vowel(d[i+1])) return r; // remove consonant between 2 consonants
|
||||
if (i+2 < d.length && c === d[i+1] && c === d[i+2]) return r; // remove three same letters in a row
|
||||
return r + c;
|
||||
}, "");
|
||||
|
||||
|
|
@ -95,7 +106,7 @@
|
|||
if (name.split(" ").some(part => part.length < 2)) name = name.split(" ").map((p,i) => i ? p.toLowerCase() : p).join("");
|
||||
|
||||
if (name.length < 2) {
|
||||
console.error("Name is too short! Random name to be selected");
|
||||
console.error("Name is too short! Random name will be selected");
|
||||
name = ra(nameBases[base].b.split(","));
|
||||
}
|
||||
|
||||
|
|
@ -103,10 +114,10 @@
|
|||
}
|
||||
|
||||
// generate name for culture
|
||||
const getCulture = function(culture, min, max, dupl, multi) {
|
||||
const getCulture = function(culture, min, max, dupl) {
|
||||
if (culture === undefined) {console.error("Please define a culture"); return;}
|
||||
const base = pack.cultures[culture].base;
|
||||
return getBase(base, min, max, dupl, multi);
|
||||
return getBase(base, min, max, dupl);
|
||||
}
|
||||
|
||||
// generate short name for culture
|
||||
|
|
@ -206,7 +217,7 @@
|
|||
}
|
||||
|
||||
const getNameBases = function() {
|
||||
// name, min length, max length, letters to allow duplication, multi-word name rate
|
||||
// name, min length, max length, letters to allow duplication, multi-word name rate [deprecated]
|
||||
return [
|
||||
// real-world bases by Azgaar:
|
||||
{name: "German", i: 0, min: 5, max: 12, d: "lt", m: 0, b: "Achern,Aichhalden,Aitern,Albbruck,Alpirsbach,Altensteig,Althengstett,Appenweier,Auggen,Wildbad,Badenen,Badenweiler,Baiersbronn,Ballrechten,Bellingen,Berghaupten,Bernau,Biberach,Biederbach,Binzen,Birkendorf,Birkenfeld,Bischweier,Blumberg,Bollen,Bollschweil,Bonndorf,Bosingen,Braunlingen,Breisach,Breisgau,Breitnau,Brigachtal,Buchenbach,Buggingen,Buhl,Buhlertal,Calw,Dachsberg,Dobel,Donaueschingen,Dornhan,Dornstetten,Dottingen,Dunningen,Durbach,Durrheim,Ebhausen,Ebringen,Efringen,Egenhausen,Ehrenkirchen,Ehrsberg,Eimeldingen,Eisenbach,Elzach,Elztal,Emmendingen,Endingen,Engelsbrand,Enz,Enzklosterle,Eschbronn,Ettenheim,Ettlingen,Feldberg,Fischerbach,Fischingen,Fluorn,Forbach,Freiamt,Freiburg,Freudenstadt,Friedenweiler,Friesenheim,Frohnd,Furtwangen,Gaggenau,Geisingen,Gengenbach,Gernsbach,Glatt,Glatten,Glottertal,Gorwihl,Gottenheim,Grafenhausen,Grenzach,Griesbach,Gutach,Gutenbach,Hag,Haiterbach,Hardt,Harmersbach,Hasel,Haslach,Hausach,Hausen,Hausern,Heitersheim,Herbolzheim,Herrenalb,Herrischried,Hinterzarten,Hochenschwand,Hofen,Hofstetten,Hohberg,Horb,Horben,Hornberg,Hufingen,Ibach,Ihringen,Inzlingen,Kandern,Kappel,Kappelrodeck,Karlsbad,Karlsruhe,Kehl,Keltern,Kippenheim,Kirchzarten,Konigsfeld,Krozingen,Kuppenheim,Kussaberg,Lahr,Lauchringen,Lauf,Laufenburg,Lautenbach,Lauterbach,Lenzkirch,Liebenzell,Loffenau,Loffingen,Lorrach,Lossburg,Mahlberg,Malsburg,Malsch,March,Marxzell,Marzell,Maulburg,Monchweiler,Muhlenbach,Mullheim,Munstertal,Murg,Nagold,Neubulach,Neuenburg,Neuhausen,Neuried,Neuweiler,Niedereschach,Nordrach,Oberharmersbach,Oberkirch,Oberndorf,Oberbach,Oberried,Oberwolfach,Offenburg,Ohlsbach,Oppenau,Ortenberg,otigheim,Ottenhofen,Ottersweier,Peterstal,Pfaffenweiler,Pfalzgrafenweiler,Pforzheim,Rastatt,Renchen,Rheinau,Rheinfelden,Rheinmunster,Rickenbach,Rippoldsau,Rohrdorf,Rottweil,Rummingen,Rust,Sackingen,Sasbach,Sasbachwalden,Schallbach,Schallstadt,Schapbach,Schenkenzell,Schiltach,Schliengen,Schluchsee,Schomberg,Schonach,Schonau,Schonenberg,Schonwald,Schopfheim,Schopfloch,Schramberg,Schuttertal,Schwenningen,Schworstadt,Seebach,Seelbach,Seewald,Sexau,Simmersfeld,Simonswald,Sinzheim,Solden,Staufen,Stegen,Steinach,Steinen,Steinmauern,Straubenhardt,Stuhlingen,Sulz,Sulzburg,Teinach,Tiefenbronn,Tiengen,Titisee,Todtmoos,Todtnau,Todtnauberg,Triberg,Tunau,Tuningen,uhlingen,Unterkirnach,Reichenbach,Utzenfeld,Villingen,Villingendorf,Vogtsburg,Vohrenbach,Waldachtal,Waldbronn,Waldkirch,Waldshut,Wehr,Weil,Weilheim,Weisenbach,Wembach,Wieden,Wiesental,Wildberg,Winzeln,Wittlingen,Wittnau,Wolfach,Wutach,Wutoschingen,Wyhlen,Zavelstein"},
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
if (outline === "none") return;
|
||||
console.time("drawOceanLayers");
|
||||
|
||||
lineGen.curve(d3.curveBasisClosed);
|
||||
cells = grid.cells, pointsN = grid.cells.i.length, vertices = grid.vertices;
|
||||
const limits = outline === "random" ? randomizeOutline() : outline.split(",").map(s => +s);
|
||||
markupOcean(limits);
|
||||
|
|
@ -27,15 +28,21 @@
|
|||
if (!start) continue;
|
||||
used[i] = 1;
|
||||
const chain = connectVertices(start, t); // vertices chain to form a path
|
||||
const relaxation = 1 + t * -2; // select only n-th point
|
||||
const relaxed = chain.filter((v, i) => i % relaxation === 0 || vertices.c[v].some(c => c >= pointsN));
|
||||
if (relaxed.length >= 3) chains.push([t, relaxed.map(v => vertices.p[v])]);
|
||||
if (chain.length < 4) continue;
|
||||
const relax = 1 + t * -2; // select only n-th point
|
||||
const relaxed = chain.filter((v, i) => !(i%relax) || vertices.c[v].some(c => c >= pointsN));
|
||||
if (relaxed.length < 4) continue;
|
||||
const points = clipPoly(relaxed.map(v => vertices.p[v]), 1);
|
||||
//const inside = d3.polygonContains(points, grid.points[i]);
|
||||
chains.push([t, points]); //chains.push([t, points, inside]);
|
||||
}
|
||||
|
||||
//const bbox = `M0,0h${graphWidth}v${graphHeight}h${-graphWidth}Z`;
|
||||
for (const t of limits) {
|
||||
const path = chains.filter(c => c[0] === t).map(c => round(lineGen(c[1]))).join();
|
||||
const layer = chains.filter(c => c[0] === t);
|
||||
let path = layer.map(c => round(lineGen(c[1]))).join("");
|
||||
//if (layer.every(c => !c[2])) path = bbox + path; // add outer ring if all segments are outside (works not for all cases)
|
||||
if (path) oceanLayers.append("path").attr("d", path).attr("fill", "#ecf2f9").style("opacity", opacity);
|
||||
// For each layer there should outer ring. If no, layer will be upside down. Need to fix it in the future
|
||||
}
|
||||
|
||||
// find eligible cell vertex to start path detection
|
||||
|
|
@ -57,8 +64,8 @@
|
|||
return limits;
|
||||
}
|
||||
|
||||
// Define grid ocean cells type based on distance form land
|
||||
function markupOcean(limits) {
|
||||
// Define ocean cells type based on distance form land
|
||||
for (let t = -2; t >= limits[0]-1; t--) {
|
||||
for (let i = 0; i < pointsN; i++) {
|
||||
if (cells.t[i] !== t+1) continue;
|
||||
|
|
|
|||
|
|
@ -271,10 +271,14 @@
|
|||
|
||||
// remove river and all its tributaries
|
||||
const remove = function(id) {
|
||||
const cells = pack.cells;
|
||||
const riversToRemove = pack.rivers.filter(r => r.i === id || getBasin(r.i, r.parent, id) === id).map(r => r.i);
|
||||
riversToRemove.forEach(r => rivers.select("#river"+r).remove());
|
||||
pack.cells.r.forEach((r, i) => {
|
||||
if (r && riversToRemove.includes(r)) pack.cells.r[i] = 0;
|
||||
cells.r.forEach((r, i) => {
|
||||
if (!r || !riversToRemove.includes(r)) return;
|
||||
cells.r[i] = 0;
|
||||
cells.fl[i] = grid.cells.prec[cells.g[i]];
|
||||
cells.conf[i] = 0;
|
||||
});
|
||||
pack.rivers = pack.rivers.filter(r => !riversToRemove.includes(r.i));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,52 +61,35 @@
|
|||
|
||||
const getSearoutes = function() {
|
||||
console.time("generateSearoutes");
|
||||
const cells = pack.cells, allPorts = pack.burgs.filter(b => b.port > 0 && !b.removed);
|
||||
const allPorts = pack.burgs.filter(b => b.port > 0 && !b.removed);
|
||||
if (allPorts.length < 2) return [];
|
||||
|
||||
const bodies = new Set(allPorts.map(b => b.port)); // features with ports
|
||||
let paths = []; // array to store path segments
|
||||
const connected = []; // store cell id of connected burgs
|
||||
|
||||
bodies.forEach(function(f) {
|
||||
const ports = allPorts.filter(b => b.port === f);
|
||||
const ports = allPorts.filter(b => b.port === f); // all ports on the same feature
|
||||
if (ports.length < 2) return;
|
||||
const first = ports[0].cell;
|
||||
const farthest = ports[d3.scan(ports, (a, b) => ((b.y - ports[0].y) ** 2 + (b.x - ports[0].x) ** 2) - ((a.y - ports[0].y) ** 2 + (a.x - ports[0].x) ** 2))].cell;
|
||||
|
||||
// directly connect first port with the farthest one on the same island to remove gap
|
||||
void function() {
|
||||
if (!pack.features[f] || pack.features[f].type === "lake") return;
|
||||
const portsOnIsland = ports.filter(b => cells.f[b.cell] === cells.f[first]);
|
||||
if (portsOnIsland.length < 4) return;
|
||||
const opposite = ports[d3.scan(portsOnIsland, (a, b) => ((b.y - ports[0].y) ** 2 + (b.x - ports[0].x) ** 2) - ((a.y - ports[0].y) ** 2 + (a.x - ports[0].x) ** 2))].cell;
|
||||
//debug.append("circle").attr("cx", pack.cells.p[opposite][0]).attr("cy", pack.cells.p[opposite][1]).attr("r", 1);
|
||||
//debug.append("circle").attr("cx", pack.cells.p[first][0]).attr("cy", pack.cells.p[first][1]).attr("fill", "red").attr("r", 1);
|
||||
const [from, exit, passable] = findOceanPath(opposite, first);
|
||||
if (!passable) return;
|
||||
from[first] = cells.haven[first];
|
||||
const path = restorePath(opposite, first, "ocean", from);
|
||||
paths = paths.concat(path);
|
||||
}()
|
||||
for (let s=0; s < ports.length; s++) {
|
||||
const source = ports[s].cell;
|
||||
if (connected[source]) continue;
|
||||
|
||||
// directly connect first port with the farthest one
|
||||
void function() {
|
||||
const [from, exit, passable] = findOceanPath(farthest, first);
|
||||
if (!passable) return;
|
||||
from[first] = cells.haven[first];
|
||||
const path = restorePath(farthest, first, "ocean", from);
|
||||
paths = paths.concat(path);
|
||||
}()
|
||||
for (let t=s+1; t < ports.length; t++) {
|
||||
const target = ports[t].cell;
|
||||
if (connected[target]) continue;
|
||||
|
||||
// indirectly connect first port with all other ports
|
||||
void function() {
|
||||
if (ports.length < 3) return;
|
||||
for (const p of ports) {
|
||||
if (p.cell === first || p.cell === farthest) continue;
|
||||
const [from, exit, passable] = findOceanPath(p.cell, first, true);
|
||||
const [from, exit, passable] = findOceanPath(target, source, true);
|
||||
if (!passable) continue;
|
||||
const path = restorePath(p.cell, exit, "ocean", from);
|
||||
|
||||
const path = restorePath(target, exit, "ocean", from);
|
||||
paths = paths.concat(path);
|
||||
|
||||
connected[source] = 1;
|
||||
connected[target] = 1;
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
|
@ -235,7 +218,7 @@
|
|||
|
||||
// find water paths
|
||||
function findOceanPath(start, exit = null, toRoute = null) {
|
||||
const cells = pack.cells;
|
||||
const cells = pack.cells, temp = grid.cells.temp;
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||
const cost = [], from = [];
|
||||
queue.queue({e: start, p: 0});
|
||||
|
|
@ -247,6 +230,7 @@
|
|||
for (const c of cells.c[n]) {
|
||||
if (c === exit) {from[c] = n; return [from, exit, true];}
|
||||
if (cells.h[c] >= 20) continue; // ignore land cells
|
||||
if (temp[cells.g[c]] <= -5) continue; // ignore cells with term <= -5
|
||||
const dist2 = (cells.p[c][1] - cells.p[n][1]) ** 2 + (cells.p[c][0] - cells.p[n][0]) ** 2;
|
||||
const totalCost = p + (cells.road[c] ? 1 + dist2 / 2 : dist2 + (cells.t[c] ? 1 : 100));
|
||||
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@ function getMapData() {
|
|||
const date = new Date();
|
||||
const dateString = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate();
|
||||
const license = "File can be loaded in azgaar.github.io/Fantasy-Map-Generator";
|
||||
const params = [version, license, dateString, seed, graphWidth, graphHeight].join("|");
|
||||
const params = [version, license, dateString, seed, graphWidth, graphHeight, mapId].join("|");
|
||||
const settings = [distanceUnitInput.value, distanceScaleInput.value, areaUnit.value,
|
||||
heightUnit.value, heightExponentInput.value, temperatureScale.value,
|
||||
barSize.value, barLabel.value, barBackOpacity.value, barBackColor.value,
|
||||
|
|
@ -294,19 +294,12 @@ async function saveMap() {
|
|||
link.click();
|
||||
tip(`${link.download} is saved. Open "Downloads" screen (CTRL + J) to check`, true, "success", 7000);
|
||||
window.URL.revokeObjectURL(URL);
|
||||
|
||||
// send saved files count and size to server for usage analysis (for the future Cloud storage)
|
||||
publicstorage.get("fmg").then(fmg => {
|
||||
if (!fmg) return;
|
||||
fmg.size = (fmg.size * fmg.maps + blob.size) / (fmg.maps + 1);
|
||||
fmg.maps += 1;
|
||||
publicstorage.set("fmg", fmg).then(fmg => console.log(fmg));
|
||||
});
|
||||
}
|
||||
|
||||
function saveGeoJSON_Cells() {
|
||||
let data = "{ \"type\": \"FeatureCollection\", \"features\": [\n";
|
||||
const cells = pack.cells, v = pack.vertices;
|
||||
const getPopulation = i => {const [r, u] = getCellPopulation(i); return rn(r+u)};
|
||||
|
||||
cells.i.forEach(i => {
|
||||
data += "{\n \"type\": \"Feature\",\n \"geometry\": { \"type\": \"Polygon\", \"coordinates\": [[";
|
||||
|
|
@ -321,17 +314,18 @@ function saveGeoJSON_Cells() {
|
|||
data += "["+x+","+y+"]";
|
||||
data += "]] },\n \"properties\": {\n";
|
||||
|
||||
let height = parseInt(getFriendlyHeight([cells.p[i][0],cells.p[i][1]]));
|
||||
const height = parseInt(getFriendlyHeight([cells.p[i][0],cells.p[i][1]]));
|
||||
|
||||
data += " \"id\": \""+i+"\",\n";
|
||||
data += " \"height\": \""+height+"\",\n";
|
||||
data += " \"biome\": \""+cells.biome[i]+"\",\n";
|
||||
data += " \"type\": \""+pack.features[cells.f[i]].type+"\",\n";
|
||||
data += " \"population\": \""+getFriendlyPopulation(i)+"\",\n";
|
||||
data += " \"population\": \""+getPopulation(i)+"\",\n";
|
||||
data += " \"state\": \""+cells.state[i]+"\",\n";
|
||||
data += " \"province\": \""+cells.province[i]+"\",\n";
|
||||
data += " \"culture\": \""+cells.culture[i]+"\",\n";
|
||||
data += " \"religion\": \""+cells.religion[i]+"\"\n";
|
||||
data += " \"religion\": \""+cells.religion[i]+"\",\n";
|
||||
data += " \"neighbors\": ["+cells.c[i]+"]\n";
|
||||
data +=" }\n},\n";
|
||||
});
|
||||
|
||||
|
|
@ -558,6 +552,7 @@ function parseLoadedData(data) {
|
|||
if (params[3]) {seed = params[3]; optionsSeed.value = seed;}
|
||||
if (params[4]) graphWidth = +params[4];
|
||||
if (params[5]) graphHeight = +params[5];
|
||||
mapId = params[6] ? +params[6] : Date.now();
|
||||
}()
|
||||
|
||||
console.group("Loaded Map " + seed);
|
||||
|
|
@ -625,6 +620,7 @@ function parseLoadedData(data) {
|
|||
texture = viewbox.select("#texture");
|
||||
terrs = viewbox.select("#terrs");
|
||||
biomes = viewbox.select("#biomes");
|
||||
ice = viewbox.select("#ice");
|
||||
cells = viewbox.select("#cells");
|
||||
gridOverlay = viewbox.select("#gridOverlay");
|
||||
coordinates = viewbox.select("#coordinates");
|
||||
|
|
@ -803,11 +799,9 @@ function parseLoadedData(data) {
|
|||
if (!markers.selectAll("*").size()) {addMarkers(); turnButtonOn("toggleMarkers");}
|
||||
|
||||
// 1.0 add fogging layer (state focus)
|
||||
fogging = viewbox.insert("g", "#ruler").attr("id", "fogging-cont").attr("mask", "url(#fog)")
|
||||
.append("g").attr("id", "fogging").style("display", "none");
|
||||
fogging = viewbox.insert("g", "#ruler").attr("id", "fogging-cont").attr("mask", "url(#fog)").append("g").attr("id", "fogging").style("display", "none");
|
||||
fogging.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%");
|
||||
defs.append("mask").attr("id", "fog").append("rect").attr("x", 0).attr("y", 0).attr("width", "100%")
|
||||
.attr("height", "100%").attr("fill", "white");
|
||||
defs.append("mask").attr("id", "fog").append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%").attr("fill", "white");
|
||||
|
||||
// 1.0 changes states opacity bask to regions level
|
||||
if (statesBody.attr("opacity")) {
|
||||
|
|
@ -961,12 +955,37 @@ function parseLoadedData(data) {
|
|||
Military.generate();
|
||||
}
|
||||
|
||||
if (version < 1.35) {
|
||||
if (version < 1.4) {
|
||||
// v 1.35 added dry lakes
|
||||
if (!lakes.select("#dry").size()) {
|
||||
lakes.append("g").attr("id", "dry");
|
||||
lakes.select("#dry").attr("opacity", 1).attr("fill", "#c9bfa7").attr("stroke", "#8e816f").attr("stroke-width", .7).attr("filter", null);
|
||||
}
|
||||
|
||||
// v 1.4 added ice layer
|
||||
ice = viewbox.insert("g", "#coastline").attr("id", "ice").style("display", "none");
|
||||
ice.attr("opacity", null).attr("fill", "#e8f0f6").attr("stroke", "#e8f0f6").attr("stroke-width", 1).attr("filter", "url(#dropShadow05)");
|
||||
drawIce();
|
||||
|
||||
// v 1.4 added icon and power attributes for units
|
||||
for (const unit of options.military) {
|
||||
if (!unit.icon) unit.icon = getUnitIcon(unit.type);
|
||||
if (!unit.power) unit.power = unit.crew;
|
||||
}
|
||||
|
||||
function getUnitIcon(type) {
|
||||
if (type === "naval") return "🌊";
|
||||
if (type === "ranged") return "🏹";
|
||||
if (type === "mounted") return "🐴";
|
||||
if (type === "machinery") return "💣";
|
||||
if (type === "armored") return "🐢";
|
||||
if (type === "aviation") return "🦅";
|
||||
if (type === "magical") return "🔮";
|
||||
else return "⚔️";
|
||||
}
|
||||
|
||||
// 1.4 added state reference for regiments
|
||||
pack.states.filter(s => s.military).forEach(s => s.military.forEach(r => r.state = s.i));
|
||||
}
|
||||
|
||||
}()
|
||||
|
|
|
|||
674
modules/ui/battle-screen.js
Normal file
674
modules/ui/battle-screen.js
Normal file
|
|
@ -0,0 +1,674 @@
|
|||
"use strict";
|
||||
class Battle {
|
||||
|
||||
constructor(attacker, defender) {
|
||||
if (customization) return;
|
||||
closeDialogs(".stable");
|
||||
customization = 13; // enter customization to avoid unwanted dialog closing
|
||||
|
||||
Battle.prototype.context = this; // store context
|
||||
this.iteration = 0;
|
||||
this.x = defender.x;
|
||||
this.y = defender.y;
|
||||
this.cell = findCell(this.x, this.y);
|
||||
this.attackers = {regiments:[], distances:[], morale:100, casualties:0, power:0};
|
||||
this.defenders = {regiments:[], distances:[], morale:100, casualties:0, power:0};
|
||||
|
||||
this.addHeaders();
|
||||
this.addRegiment("attackers", attacker);
|
||||
this.addRegiment("defenders", defender);
|
||||
this.place = this.definePlace();
|
||||
this.defineType();
|
||||
this.name = this.defineName();
|
||||
this.randomize();
|
||||
this.calculateStrength("attackers");
|
||||
this.calculateStrength("defenders");
|
||||
this.getInitialMorale();
|
||||
|
||||
$("#battleScreen").dialog({
|
||||
title: this.name, resizable: false, width: fitContent(),
|
||||
position: {my: "center", at: "center", of: "#map"},
|
||||
close: () => Battle.prototype.context.cancelResults()
|
||||
});
|
||||
|
||||
if (modules.Battle) return;
|
||||
modules.Battle = true;
|
||||
|
||||
// add listeners
|
||||
document.getElementById("battleType").addEventListener("click", ev => this.toggleChange(ev));
|
||||
document.getElementById("battleType").nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changeType(ev));
|
||||
document.getElementById("battleNameShow").addEventListener("click", () => Battle.prototype.context.showNameSection());
|
||||
document.getElementById("battleNamePlace").addEventListener("change", ev => Battle.prototype.context.place = ev.target.value);
|
||||
document.getElementById("battleNameFull").addEventListener("change", ev => Battle.prototype.context.changeName(ev));
|
||||
document.getElementById("battleNameCulture").addEventListener("click", () => Battle.prototype.context.generateName("culture"));
|
||||
document.getElementById("battleNameRandom").addEventListener("click", () => Battle.prototype.context.generateName("random"));
|
||||
document.getElementById("battleNameHide").addEventListener("click", this.hideNameSection);
|
||||
document.getElementById("battleAddRegiment").addEventListener("click", this.addSide);
|
||||
document.getElementById("battleRoll").addEventListener("click", () => Battle.prototype.context.randomize());
|
||||
document.getElementById("battleRun").addEventListener("click", () => Battle.prototype.context.run());
|
||||
document.getElementById("battleApply").addEventListener("click", () => Battle.prototype.context.applyResults());
|
||||
document.getElementById("battleCancel").addEventListener("click", () => Battle.prototype.context.cancelResults());
|
||||
document.getElementById("battleWiki").addEventListener("click", () => wiki("Battle-Simulator"));
|
||||
|
||||
document.getElementById("battlePhase_attackers").addEventListener("click", ev => this.toggleChange(ev));
|
||||
document.getElementById("battlePhase_attackers").nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changePhase(ev, "attackers"));
|
||||
document.getElementById("battlePhase_defenders").addEventListener("click", ev => this.toggleChange(ev));
|
||||
document.getElementById("battlePhase_defenders").nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changePhase(ev, "defenders"));
|
||||
document.getElementById("battleDie_attackers").addEventListener("click", () => Battle.prototype.context.rollDie("attackers"));
|
||||
document.getElementById("battleDie_defenders").addEventListener("click", () => Battle.prototype.context.rollDie("defenders"));
|
||||
}
|
||||
|
||||
defineType() {
|
||||
const attacker = this.attackers.regiments[0];
|
||||
const defender = this.defenders.regiments[0];
|
||||
const getType = () => {
|
||||
const typesA = Object.keys(attacker.u).map(name => options.military.find(u => u.name === name).type);
|
||||
const typesD = Object.keys(defender.u).map(name => options.military.find(u => u.name === name).type);
|
||||
|
||||
if (attacker.n && defender.n) return "naval"; // attacker and defender are navals
|
||||
if (typesA.every(t => t === "aviation") && typesD.every(t => t === "aviation")) return "air"; // if attacked and defender have only aviation units
|
||||
if (attacker.n && !defender.n && typesA.some(t => t !== "naval")) return "landing"; // if attacked is naval with non-naval units and defender is not naval
|
||||
if (!defender.n && pack.burgs[pack.cells.burg[this.cell]].walls) return "siege"; // defender is in walled town
|
||||
if (P(.1) && [5,6,7,8,9,12].includes(pack.cells.biome[this.cell])) return "ambush"; // 20% if defenders are in forest or marshes
|
||||
return "field";
|
||||
}
|
||||
|
||||
this.type = getType();
|
||||
this.setType();
|
||||
}
|
||||
|
||||
setType() {
|
||||
document.getElementById("battleType").className = "icon-button-" + this.type;
|
||||
|
||||
const sideSpecific = document.getElementById("battlePhases_"+this.type+"_attackers");
|
||||
const attackers = sideSpecific ? sideSpecific.content : document.getElementById("battlePhases_"+this.type).content;
|
||||
const defenders = sideSpecific ? document.getElementById("battlePhases_"+this.type+"_defenders").content : attackers;
|
||||
|
||||
document.getElementById("battlePhase_attackers").nextElementSibling.innerHTML = "";
|
||||
document.getElementById("battlePhase_defenders").nextElementSibling.innerHTML = "";
|
||||
document.getElementById("battlePhase_attackers").nextElementSibling.append(attackers.cloneNode(true));
|
||||
document.getElementById("battlePhase_defenders").nextElementSibling.append(defenders.cloneNode(true));
|
||||
}
|
||||
|
||||
definePlace() {
|
||||
const cells = pack.cells, i = this.cell;
|
||||
const burg = cells.burg[i] ? pack.burgs[cells.burg[i]].name : null;
|
||||
const getRiver = i => {const river = pack.rivers.find(r => r.i === i); return river.name + " " + river.type};
|
||||
const river = !burg && cells.r[i] ? getRiver(cells.r[i]) : null;
|
||||
const proper = burg || river ? null : Names.getCulture(cells.culture[this.cell]);
|
||||
return burg ? burg : river ? river : proper;
|
||||
}
|
||||
|
||||
defineName() {
|
||||
if (this.type === "field") return "Battle of " + this.place;
|
||||
if (this.type === "naval") return "Naval Battle of " + this.place;
|
||||
if (this.type === "siege") return "Siege of "+ this.place;
|
||||
if (this.type === "ambush") return this.place + " Ambush";
|
||||
if (this.type === "landing") return this.place + " Landing";
|
||||
if (this.type === "air") return `${this.place} ${P(.8) ? "Air Battle" : "Dogfight"}`;
|
||||
}
|
||||
|
||||
addHeaders() {
|
||||
let headers = "<thead><tr><th></th><th></th>";
|
||||
|
||||
for (const u of options.military) {
|
||||
const label = capitalize(u.name.replace(/_/g, ' '));
|
||||
headers += `<th data-tip="${label}">${u.icon}</th>`;
|
||||
}
|
||||
|
||||
headers += "<th data-tip='Total military''>Total</th></tr></thead>";
|
||||
battleAttackers.innerHTML = battleDefenders.innerHTML = headers;
|
||||
}
|
||||
|
||||
addRegiment(side, regiment) {
|
||||
regiment.casualties = Object.keys(regiment.u).reduce((a,b) => (a[b]=0,a), {});
|
||||
regiment.survivors = Object.assign({}, regiment.u);
|
||||
|
||||
const state = pack.states[regiment.state];
|
||||
const distance = Math.hypot(this.y-regiment.by, this.x-regiment.bx) * distanceScaleInput.value | 0; // distance between regiment and its base
|
||||
const color = state.color[0] === "#" ? state.color : "#999";
|
||||
const icon = `<svg width="1.4em" height="1.4em" style="margin-bottom: -.6em">
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="${color}" class="fillRect"></rect>
|
||||
<text x="0" y="1.04em" style="">${regiment.icon}</text></svg>`;
|
||||
const body = `<tbody id="battle${state.i}-${regiment.i}">`;
|
||||
|
||||
let initial = `<tr class="battleInitial"><td>${icon}</td><td class="regiment" data-tip="${regiment.name}">${regiment.name.slice(0, 24)}</td>`;
|
||||
let casualties = `<tr class="battleCasualties"><td></td><td data-tip="${state.fullName}">${state.fullName.slice(0, 26)}</td>`;
|
||||
let survivors = `<tr class="battleSurvivors"><td></td><td data-tip="Supply line length, affects morale">Distance to base: ${distance} ${distanceUnitInput.value}</td>`;
|
||||
|
||||
for (const u of options.military) {
|
||||
initial += `<td data-tip="Initial forces" style="width: 2.5em; text-align: center">${regiment.u[u.name]||0}</td>`;
|
||||
casualties += `<td data-tip="Casualties" style="width: 2.5em; text-align: center; color: red">0</td>`;
|
||||
survivors += `<td data-tip="Survivors" style="width: 2.5em; text-align: center; color: green">${regiment.u[u.name]||0}</td>`;
|
||||
}
|
||||
|
||||
initial += `<td data-tip="Initial forces" style="width: 2.5em; text-align: center">${regiment.a||0}</td></tr>`;
|
||||
casualties += `<td data-tip="Casualties" style="width: 2.5em; text-align: center; color: red">0</td></tr>`;
|
||||
survivors += `<td data-tip="Survivors" style="width: 2.5em; text-align: center; color: green">${regiment.a||0}</td></tr>`;
|
||||
|
||||
const div = side === "attackers" ? battleAttackers : battleDefenders;
|
||||
div.innerHTML += body + initial + casualties + survivors + "</tbody>";
|
||||
this[side].regiments.push(regiment);
|
||||
this[side].distances.push(distance);
|
||||
}
|
||||
|
||||
addSide() {
|
||||
const body = document.getElementById("regimentSelectorBody");
|
||||
const context = Battle.prototype.context;
|
||||
const regiments = pack.states.filter(s => s.military && !s.removed).map(s => s.military).flat();
|
||||
const distance = reg => rn(Math.hypot(context.y-reg.y, context.x-reg.x) * distanceScaleInput.value) + " " + distanceUnitInput.value;
|
||||
const isAdded = reg => context.defenders.regiments.some(r => r === reg) || context.attackers.regiments.some(r => r === reg);
|
||||
|
||||
body.innerHTML = regiments.map(r => {
|
||||
const s = pack.states[r.state], added = isAdded(r), dist = added ? "0 " + distanceUnitInput.value : distance(r);
|
||||
return `<div ${added ? "class='inactive'" : ""} data-s=${s.i} data-i=${r.i} data-state=${s.name} data-regiment=${r.name}
|
||||
data-total=${r.a} data-distance=${dist} data-tip="Click to select regiment">
|
||||
<svg width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${s.color}" class="fillRect"></svg>
|
||||
<div style="width:6em">${s.name.slice(0, 11)}</div>
|
||||
<div style="width:1.2em">${r.icon}</div>
|
||||
<div style="width:13em">${r.name.slice(0, 24)}</div>
|
||||
<div style="width:4em">${r.a}</div>
|
||||
<div style="width:4em">${dist}</div>
|
||||
</div>`;
|
||||
}).join("");
|
||||
|
||||
$("#regimentSelectorScreen").dialog({
|
||||
resizable: false, width: fitContent(), title: "Add regiment to the battle",
|
||||
position: {my: "left center", at: "right+10 center", of: "#battleScreen"}, close: addSideClosed,
|
||||
buttons: {
|
||||
"Add to attackers": () => addSideClicked("attackers"),
|
||||
"Add to defenders": () => addSideClicked("defenders"),
|
||||
Cancel: () => $("#regimentSelectorScreen").dialog("close")
|
||||
}
|
||||
});
|
||||
|
||||
applySorting(regimentSelectorHeader);
|
||||
body.addEventListener("click", selectLine);
|
||||
|
||||
function selectLine(ev) {
|
||||
if (ev.target.className === "inactive") {tip("Regiment is already in the battle", false, "error"); return};
|
||||
ev.target.classList.toggle("selected");
|
||||
}
|
||||
|
||||
function addSideClicked(side) {
|
||||
const selected = body.querySelectorAll(".selected");
|
||||
if (!selected.length) {tip("Please select a regiment first", false, "error"); return}
|
||||
|
||||
$("#regimentSelectorScreen").dialog("close");
|
||||
selected.forEach(line => {
|
||||
const state = pack.states[line.dataset.s];
|
||||
const regiment = state.military.find(r => r.i == +line.dataset.i);
|
||||
Battle.prototype.addRegiment.call(context, side, regiment);
|
||||
Battle.prototype.calculateStrength.call(context, side);
|
||||
Battle.prototype.getInitialMorale.call(context);
|
||||
|
||||
// move regiment
|
||||
const defenders = context.defenders.regiments, attackers = context.attackers.regiments;
|
||||
const shift = side === "attackers" ? attackers.length * -8 : (defenders.length-1) * 8;
|
||||
regiment.px = regiment.x;
|
||||
regiment.py = regiment.y;
|
||||
Military.moveRegiment(regiment, defenders[0].x, defenders[0].y + shift);
|
||||
});
|
||||
}
|
||||
|
||||
function addSideClosed() {
|
||||
body.innerHTML = "";
|
||||
body.removeEventListener("click", selectLine);
|
||||
}
|
||||
}
|
||||
|
||||
showNameSection() {
|
||||
document.querySelectorAll("#battleBottom > button").forEach(el => el.style.display = "none");
|
||||
document.getElementById("battleNameSection").style.display = "inline-block";
|
||||
|
||||
document.getElementById("battleNamePlace").value = this.place;
|
||||
document.getElementById("battleNameFull").value = this.name;
|
||||
}
|
||||
|
||||
hideNameSection() {
|
||||
document.querySelectorAll("#battleBottom > button").forEach(el => el.style.display = "inline-block");
|
||||
document.getElementById("battleNameSection").style.display = "none";
|
||||
}
|
||||
|
||||
changeName(ev) {
|
||||
this.name = ev.target.value;
|
||||
$("#battleScreen").dialog({"title":this.name});
|
||||
}
|
||||
|
||||
generateName(type) {
|
||||
const place = type === "culture"
|
||||
? Names.getCulture(pack.cells.culture[this.cell], null, null, "")
|
||||
: Names.getBase(rand(nameBases.length-1));
|
||||
document.getElementById("battleNamePlace").value = this.place = place;
|
||||
document.getElementById("battleNameFull").value = this.name = this.defineName();
|
||||
$("#battleScreen").dialog({"title":this.name});
|
||||
}
|
||||
|
||||
getJoinedForces(regiments) {
|
||||
return regiments.reduce((a, b) => {
|
||||
for (let k in b.survivors) {
|
||||
if (!b.survivors.hasOwnProperty(k)) continue;
|
||||
a[k] = (a[k] || 0) + b.survivors[k];
|
||||
}
|
||||
return a;
|
||||
}, {});
|
||||
}
|
||||
|
||||
calculateStrength(side) {
|
||||
const scheme = {
|
||||
// field battle phases
|
||||
"skirmish": {"melee":.2, "ranged":2.4, "mounted":.1, "machinery":3, "naval":1, "armored":.2, "aviation":1.8, "magical":1.8}, // ranged excel
|
||||
"melee": {"melee":2, "ranged":1.2, "mounted":1.5, "machinery":.5, "naval":.2, "armored":2, "aviation":.8, "magical":.8}, // melee excel
|
||||
"pursue": {"melee":1, "ranged":1, "mounted":4, "machinery":.05, "naval":1, "armored":1, "aviation":1.5, "magical":.6}, // mounted excel
|
||||
"retreat": {"melee":.1, "ranged":.01, "mounted":.5, "machinery":.01, "naval":.2, "armored":.1, "aviation":.8, "magical":.05}, // reduced
|
||||
|
||||
// naval battle phases
|
||||
"shelling": {"melee":0, "ranged":.2, "mounted":0, "machinery":2, "naval":2, "armored":0, "aviation":.1, "magical":.5}, // naval and machinery excel
|
||||
"boarding": {"melee":1, "ranged":.5, "mounted":.5, "machinery":0, "naval":.5, "armored":.4, "aviation":0, "magical":.2}, // melee excel
|
||||
"chase": {"melee":0, "ranged":.15, "mounted":0, "machinery":1, "naval":1, "armored":0, "aviation":.15, "magical":.5}, // reduced
|
||||
"withdrawal": {"melee":0, "ranged":.02, "mounted":0, "machinery":.5, "naval":.1, "armored":0, "aviation":.1, "magical":.3}, // reduced
|
||||
|
||||
// siege phases
|
||||
"blockade": {"melee":.25, "ranged":.25, "mounted":.2, "machinery":.5, "naval":.2, "armored":.1, "aviation":.25, "magical":.25}, // no active actions
|
||||
"sheltering": {"melee":.3, "ranged":.5, "mounted":.2, "machinery":.5, "naval":.2, "armored":.1, "aviation":.25, "magical":.25}, // no active actions
|
||||
"sortie": {"melee":2, "ranged":.5, "mounted":1.2, "machinery":.2, "naval":.1, "armored":.5, "aviation":1, "magical":1}, // melee excel
|
||||
"bombardment": {"melee":.2, "ranged":.5, "mounted":.2, "machinery":3, "naval":1, "armored":.5, "aviation":1, "magical":1}, // machinery excel
|
||||
"storming": {"melee":1, "ranged":.6, "mounted":.5, "machinery":1, "naval":.1, "armored":.1, "aviation":.5, "magical":.5}, // melee excel
|
||||
"defense": {"melee":2, "ranged":3, "mounted":1, "machinery":1, "naval":.1, "armored":1, "aviation":.5, "magical":1}, // ranged excel
|
||||
"looting": {"melee":1.6, "ranged":1.6, "mounted":.5, "machinery":.2, "naval":.02, "armored":.2, "aviation":.1, "magical":.3}, // melee excel
|
||||
"surrendering": {"melee":.1, "ranged":.1, "mounted":.05, "machinery":.01, "naval":.01, "armored":.02, "aviation":.01, "magical":.03}, // reduced
|
||||
|
||||
// ambush phases
|
||||
"surprise": {"melee":2, "ranged":2.4, "mounted":1, "machinery":1, "naval":1, "armored":1, "aviation":.8, "magical":1.2}, // increased
|
||||
"shock": {"melee":.5, "ranged":.5, "mounted":.5, "machinery":.4, "naval":.3, "armored":.1, "aviation":.4, "magical":.5}, // reduced
|
||||
|
||||
// langing phases
|
||||
"landing": {"melee":.8, "ranged":.6, "mounted":.6, "machinery":.5, "naval":.5, "armored":.5, "aviation":.5, "magical":.6}, // reduced
|
||||
"flee": {"melee":.1, "ranged":.01, "mounted":.5, "machinery":.01, "naval":.5, "armored":.1, "aviation":.2, "magical":.05}, // reduced
|
||||
"waiting": {"melee":.05, "ranged":.5, "mounted":.05, "machinery":.5, "naval":2, "armored":.05, "aviation":.5, "magical":.5}, // reduced
|
||||
|
||||
// air battle phases
|
||||
"maneuvering": {"melee":0, "ranged":.1, "mounted":0, "machinery":.2, "naval":0, "armored":0, "aviation":1, "magical":.2}, // aviation
|
||||
"dogfight": {"melee":0, "ranged":.1, "mounted":0, "machinery":.1, "naval":0, "armored":0, "aviation":2, "magical":.1} // aviation
|
||||
};
|
||||
|
||||
const forces = this.getJoinedForces(this[side].regiments);
|
||||
const phase = this[side].phase;
|
||||
const adjuster = Math.max(populationRate.value / 10, 10); // population adjuster, by default 100
|
||||
this[side].power = d3.sum(options.military.map(u => (forces[u.name] || 0) * u.power * scheme[phase][u.type])) / adjuster;
|
||||
const UIvalue = this[side].power ? Math.max(this[side].power|0, 1) : 0;
|
||||
document.getElementById("battlePower_"+side).innerHTML = UIvalue;
|
||||
}
|
||||
|
||||
getInitialMorale() {
|
||||
const powerFee = diff => Math.min(Math.max(100 - diff ** 1.5 * 10 + 10, 50), 100);
|
||||
const distanceFee = dist => Math.min(d3.mean(dist) / 50, 15);
|
||||
const powerDiff = this.defenders.power / this.attackers.power;
|
||||
this.attackers.morale = powerFee(powerDiff) - distanceFee(this.attackers.distances);
|
||||
this.defenders.morale = powerFee(1 / powerDiff) - distanceFee(this.defenders.distances);
|
||||
this.updateMorale("attackers");
|
||||
this.updateMorale("defenders");
|
||||
}
|
||||
|
||||
updateMorale(side) {
|
||||
const morale = document.getElementById("battleMorale_"+side);
|
||||
morale.dataset.tip = morale.dataset.tip.replace(morale.value, "");
|
||||
morale.value = this[side].morale | 0;
|
||||
morale.dataset.tip += morale.value;
|
||||
}
|
||||
|
||||
randomize() {
|
||||
this.rollDie("attackers");
|
||||
this.rollDie("defenders");
|
||||
this.selectPhase();
|
||||
this.calculateStrength("attackers");
|
||||
this.calculateStrength("defenders");
|
||||
}
|
||||
|
||||
rollDie(side) {
|
||||
const el = document.getElementById("battleDie_"+side);
|
||||
const prev = +el.innerHTML;
|
||||
do {el.innerHTML = rand(1, 6)} while (el.innerHTML == prev)
|
||||
this[side].die = +el.innerHTML;
|
||||
}
|
||||
|
||||
selectPhase() {
|
||||
const i = this.iteration;
|
||||
const morale = [this.attackers.morale, this.defenders.morale];
|
||||
const powerRatio = this.attackers.power / this.defenders.power;
|
||||
|
||||
const getFieldBattlePhase = () => {
|
||||
const prev = [this.attackers.phase || "skirmish", this.defenders.phase || "skirmish"]; // previous phase
|
||||
|
||||
// chance if moral < 25
|
||||
if (P(1 - morale[0] / 25)) return ["retreat", "pursue"];
|
||||
if (P(1 - morale[1] / 25)) return ["pursue", "retreat"];
|
||||
|
||||
// skirmish phase continuation depends on ranged forces number
|
||||
if (prev[0] === "skirmish" && prev[1] === "skirmish") {
|
||||
const forces = this.getJoinedForces(this.attackers.regiments.concat(this.defenders.regiments));
|
||||
const total = d3.sum(Object.values(forces)); // total forces
|
||||
const ranged = d3.sum(options.military.filter(u => u.type === "ranged").map(u => u.name).map(u => forces[u])) / total; // ranged units
|
||||
if (P(ranged) || P(.8-i/10)) return ["skirmish", "skirmish"];
|
||||
}
|
||||
|
||||
return ["melee", "melee"]; // default option
|
||||
}
|
||||
|
||||
const getNavalBattlePhase = () => {
|
||||
const prev = [this.attackers.phase || "shelling", this.defenders.phase || "shelling"]; // previous phase
|
||||
|
||||
if (prev[0] === "withdrawal") return ["withdrawal", "chase"];
|
||||
if (prev[0] === "chase") return ["chase", "withdrawal"];
|
||||
|
||||
// withdrawal phase when power imbalanced
|
||||
if (!prev[0] === "boarding") {
|
||||
if (powerRatio < .5 || P(this.attackers.casualties) && powerRatio < 1) return ["withdrawal", "chase"];
|
||||
if (powerRatio > 2 || P(this.defenders.casualties) && powerRatio > 1) return ["chase", "withdrawal"];
|
||||
}
|
||||
|
||||
// boarding phase can start from 2nd iteration
|
||||
if (prev[0] === "boarding" || P(i/10 - .1)) return ["boarding", "boarding"];
|
||||
|
||||
return ["shelling", "shelling"]; // default option
|
||||
}
|
||||
|
||||
const getSiegePhase = () => {
|
||||
const prev = [this.attackers.phase || "blockade", this.defenders.phase || "sheltering"]; // previous phase
|
||||
let phase = ["blockade", "sheltering"] // default phase
|
||||
|
||||
if (prev[0] === "retreat" || prev[0] === "looting") return prev;
|
||||
|
||||
if (P(1 - morale[0] / 30) && powerRatio < 1) return ["retreat", "pursue"]; // attackers retreat chance if moral < 30
|
||||
if (P(1 - morale[1] / 15)) return ["looting", "surrendering"]; // defenders surrendering chance if moral < 15
|
||||
|
||||
if (P((powerRatio-1) / 2)) return ["storming", "defense"]; // start storm
|
||||
|
||||
if (prev[0] !== "storming") {
|
||||
const machinery = options.military.filter(u => u.type === "machinery").map(u => u.name); // machinery units
|
||||
|
||||
const attackers = this.getJoinedForces(this.attackers.regiments);
|
||||
const machineryA = d3.sum(machinery.map(u => attackers[u]));
|
||||
if (i && machineryA && P(.9)) phase[0] = "bombardment";
|
||||
|
||||
const defenders = this.getJoinedForces(this.defenders.regiments);
|
||||
const machineryD = d3.sum(machinery.map(u => defenders[u]));
|
||||
if (machineryD && P(.9)) phase[1] = "bombardment";
|
||||
|
||||
if (i && prev[1] !== "sortie" && machineryD < machineryA && P(.25) && P(morale[1]/70)) phase[1] = "sortie"; // defenders sortie
|
||||
}
|
||||
|
||||
return phase;
|
||||
}
|
||||
|
||||
const getAmbushPhase = () => {
|
||||
const prev = [this.attackers.phase || "shock", this.defenders.phase || "surprise"]; // previous phase
|
||||
|
||||
if (prev[1] === "surprise" && P(1-powerRatio*i/5)) return ["shock", "surprise"];
|
||||
|
||||
// chance if moral < 25
|
||||
if (P(1 - morale[0] / 25)) return ["retreat", "pursue"];
|
||||
if (P(1 - morale[1] / 25)) return ["pursue", "retreat"];
|
||||
|
||||
return ["melee", "melee"]; // default option
|
||||
}
|
||||
|
||||
const getLandingPhase = () => {
|
||||
const prev = [this.attackers.phase || "landing", this.defenders.phase || "defense"]; // previous phase
|
||||
|
||||
if (prev[1] === "waiting") return ["flee", "waiting"];
|
||||
if (prev[1] === "pursue") return ["flee", P(.3) ? "pursue" : "waiting"];
|
||||
if (prev[1] === "retreat") return ["pursue", "retreat"];
|
||||
|
||||
if (prev[0] === "landing") {
|
||||
const attackers = P(i/2) ? "melee" : "landing";
|
||||
const defenders = i ? prev[1] : P(.5) ? "defense" : "shock";
|
||||
return [attackers, defenders];
|
||||
}
|
||||
|
||||
if (P(1 - morale[0] / 40)) return ["flee", "pursue"]; // chance if moral < 40
|
||||
if (P(1 - morale[1] / 25)) return ["pursue", "retreat"]; // chance if moral < 25
|
||||
|
||||
return ["melee", "melee"]; // default option
|
||||
}
|
||||
|
||||
const getAirBattlePhase = () => {
|
||||
const prev = [this.attackers.phase || "maneuvering", this.defenders.phase || "maneuvering"]; // previous phase
|
||||
|
||||
// chance if moral < 25
|
||||
if (P(1 - morale[0] / 25)) return ["retreat", "pursue"];
|
||||
if (P(1 - morale[1] / 25)) return ["pursue", "retreat"];
|
||||
|
||||
if (prev[0] === "maneuvering" && P(1-i/10)) return ["maneuvering", "maneuvering"];
|
||||
|
||||
return ["dogfight", "dogfight"]; // default option
|
||||
}
|
||||
|
||||
const phase = function(type) {
|
||||
switch (type) {
|
||||
case "field": return getFieldBattlePhase();
|
||||
case "naval": return getNavalBattlePhase();
|
||||
case "siege": return getSiegePhase();
|
||||
case "ambush": return getAmbushPhase();
|
||||
case "landing": return getLandingPhase();
|
||||
case "air": return getAirBattlePhase();
|
||||
default: getFieldBattlePhase();
|
||||
}
|
||||
}(this.type);
|
||||
|
||||
this.attackers.phase = phase[0];
|
||||
this.defenders.phase = phase[1];
|
||||
|
||||
const buttonA = document.getElementById("battlePhase_attackers");
|
||||
buttonA.className = "icon-button-" + this.attackers.phase;
|
||||
buttonA.dataset.tip = buttonA.nextElementSibling.querySelector("[data-phase='"+phase[0]+"']").dataset.tip;
|
||||
|
||||
const buttonD = document.getElementById("battlePhase_defenders");
|
||||
buttonD.className = "icon-button-" + this.defenders.phase;
|
||||
buttonD.dataset.tip = buttonD.nextElementSibling.querySelector("[data-phase='"+phase[1]+"']").dataset.tip;
|
||||
}
|
||||
|
||||
run() {
|
||||
// validations
|
||||
if (!this.attackers.power) {tip("Attackers army destroyed", false, "warn"); return}
|
||||
if (!this.defenders.power) {tip("Defenders army destroyed", false, "warn"); return}
|
||||
|
||||
// calculate casualties
|
||||
const attack = this.attackers.power * (this.attackers.die / 10 + .4);
|
||||
const defense = this.defenders.power * (this.defenders.die / 10 + .4);
|
||||
|
||||
// casualties modifier for phase
|
||||
const phase = {
|
||||
"skirmish":.1, "melee":.2, "pursue":.3, "retreat":.3, "boarding":.2, "shelling":.1, "chase":.03, "withdrawal": .03,
|
||||
"blockade":0, "sheltering":0, "sortie":.1, "bombardment":.05, "storming":.2, "defense":.2, "looting":.5, "surrendering":.5,
|
||||
"surprise":.3, "shock":.3, "landing":.3, "flee":0, "waiting":0, "maneuvering":.1, "dogfight":.2};
|
||||
|
||||
const casualties = Math.random() * (Math.max(phase[this.attackers.phase], phase[this.defenders.phase])); // total casualties, ~10% per iteration
|
||||
const casualtiesA = casualties * defense / (attack + defense); // attackers casualties, ~5% per iteration
|
||||
const casualtiesD = casualties * attack / (attack + defense); // defenders casualties, ~5% per iteration
|
||||
|
||||
this.calculateCasualties("attackers", casualtiesA);
|
||||
this.calculateCasualties("defenders", casualtiesD);
|
||||
this.attackers.casualties += casualtiesA;
|
||||
this.defenders.casualties += casualtiesD;
|
||||
|
||||
// change morale
|
||||
this.attackers.morale = Math.max(this.attackers.morale - casualtiesA * 100 - 1, 0);
|
||||
this.defenders.morale = Math.max(this.defenders.morale - casualtiesD * 100 - 1, 0);
|
||||
|
||||
// update table values
|
||||
this.updateTable("attackers");
|
||||
this.updateTable("defenders");
|
||||
|
||||
// prepare for next iteration
|
||||
this.iteration += 1;
|
||||
this.selectPhase();
|
||||
this.calculateStrength("attackers");
|
||||
this.calculateStrength("defenders");
|
||||
}
|
||||
|
||||
calculateCasualties(side, casualties) {
|
||||
for (const r of this[side].regiments) {
|
||||
for (const unit in r.u) {
|
||||
const rand = .8 + Math.random() * .4;
|
||||
const died = Math.min(Pint(r.u[unit] * casualties * rand), r.survivors[unit]);
|
||||
r.casualties[unit] -= died;
|
||||
r.survivors[unit] -= died;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateTable(side) {
|
||||
for (const r of this[side].regiments) {
|
||||
const tbody = document.getElementById("battle" + r.state + "-" + r.i);
|
||||
const battleCasualties = tbody.querySelector(".battleCasualties");
|
||||
const battleSurvivors = tbody.querySelector(".battleSurvivors");
|
||||
|
||||
let index = 3; // index to find table element easily
|
||||
for (const u of options.military) {
|
||||
battleCasualties.querySelector(`td:nth-child(${index})`).innerHTML = r.casualties[u.name] || 0;
|
||||
battleSurvivors.querySelector(`td:nth-child(${index})`).innerHTML = r.survivors[u.name] || 0;
|
||||
index++;
|
||||
}
|
||||
|
||||
battleCasualties.querySelector(`td:nth-child(${index})`).innerHTML = d3.sum(Object.values(r.casualties));
|
||||
battleSurvivors.querySelector(`td:nth-child(${index})`).innerHTML = d3.sum(Object.values(r.survivors));
|
||||
}
|
||||
this.updateMorale(side);
|
||||
}
|
||||
|
||||
toggleChange(ev) {
|
||||
ev.stopPropagation();
|
||||
const button = ev.target;
|
||||
const div = button.nextElementSibling;
|
||||
|
||||
const hideSection = function() {button.style.opacity = 1; div.style.display = "none"}
|
||||
if (div.style.display === "block") {hideSection(); return}
|
||||
|
||||
button.style.opacity = .5;
|
||||
div.style.display = "block";
|
||||
|
||||
document.getElementsByTagName("body")[0].addEventListener("click", hideSection, {once: true});
|
||||
}
|
||||
|
||||
changeType(ev) {
|
||||
if (ev.target.tagName !== "BUTTON") return;
|
||||
this.type = ev.target.dataset.type;
|
||||
this.setType();
|
||||
this.selectPhase();
|
||||
this.calculateStrength("attackers");
|
||||
this.calculateStrength("defenders");
|
||||
this.name = this.defineName();
|
||||
$("#battleScreen").dialog({"title":this.name});
|
||||
}
|
||||
|
||||
changePhase(ev, side) {
|
||||
if (ev.target.tagName !== "BUTTON") return;
|
||||
const phase = this[side].phase = ev.target.dataset.phase;
|
||||
const button = document.getElementById("battlePhase_"+side);
|
||||
button.className = "icon-button-" + phase;
|
||||
button.dataset.tip = ev.target.dataset.tip;
|
||||
this.calculateStrength(side);
|
||||
}
|
||||
|
||||
applyResults() {
|
||||
const battleName = this.name;
|
||||
const maxCasualties = Math.max(this.attackers.casualties, this.attackers.casualties);
|
||||
const relativeCasualties = this.defenders.casualties / (this.attackers.casualties + this.attackers.casualties);
|
||||
const battleStatus = getBattleStatus(relativeCasualties, maxCasualties);
|
||||
function getBattleStatus(relative, max) {
|
||||
if (isNaN(relative)) return ["standoff", "standoff"]; // if no casualties at all
|
||||
if (max < .05) return ["minor skirmishes", "minor skirmishes"];
|
||||
if (relative > 95) return ["attackers flawless victory", "disorderly retreat of defenders"];
|
||||
if (relative > .7) return ["attackers decisive victory", "defenders disastrous defeat"];
|
||||
if (relative > .6) return ["attackers victory", "defenders defeat"];
|
||||
if (relative > .4) return ["stalemate", "stalemate"];
|
||||
if (relative > .3) return ["attackers defeat", "defenders victory"];
|
||||
if (relative > 0.5) return ["attackers disastrous defeat", "decisive victory of defenders"];
|
||||
if (relative >= 0) return ["attackers disorderly retreat", "flawless victory of defenders"];
|
||||
return ["stalemate", "stalemate"]; // exception
|
||||
}
|
||||
|
||||
this.attackers.regiments.forEach(r => applyResultForSide(r, "attackers"));
|
||||
this.defenders.regiments.forEach(r => applyResultForSide(r, "defenders"));
|
||||
|
||||
function applyResultForSide(r, side) {
|
||||
const id = "regiment" + r.state + "-" + r.i;
|
||||
|
||||
// add result to regiment note
|
||||
const note = notes.find(n => n.id === id);
|
||||
if (note) {
|
||||
const status = side === "attackers" ? battleStatus[0] : battleStatus[1];
|
||||
const losses = r.a ? Math.abs(d3.sum(Object.values(r.casualties))) / r.a : 1;
|
||||
const regStatus =
|
||||
losses === 1 ? "is destroyed" :
|
||||
losses > .8 ? "is almost completely destroyed" :
|
||||
losses > .5 ? "suffered terrible losses" :
|
||||
losses > .3 ? "suffered severe losses" :
|
||||
losses > .2 ? "suffered heavy losses" :
|
||||
losses > .05 ? "suffered significant losses" :
|
||||
losses > 0 ? "suffered unsignificant losses" :
|
||||
"left the battle without loss";
|
||||
const casualties = Object.keys(r.casualties).map(t => r.casualties[t] ? `${Math.abs(r.casualties[t])} ${t}` : null).filter(c => c);
|
||||
const casualtiesText = casualties.length ? " Casualties: " + list(casualties) + "." : "";
|
||||
const legend = `\r\n\r\n${battleName} (${options.year} ${options.eraShort}): ${status}. The regiment ${regStatus}.${casualtiesText}`;
|
||||
note.legend += legend;
|
||||
}
|
||||
|
||||
r.u = Object.assign({}, r.survivors);
|
||||
r.a = d3.sum(Object.values(r.u)); // reg total
|
||||
armies.select(`g#${id} > text`).text(Military.getTotal(r)); // update reg box
|
||||
Military.moveRegiment(r, r.x + rand(20) - 10, r.y + rand(20) - 10);
|
||||
}
|
||||
|
||||
// append battlefield marker
|
||||
void function addMarkerSymbol() {
|
||||
if (svg.select("#defs-markers").select("#marker_battlefield").size()) return;
|
||||
const symbol = svg.select("#defs-markers").append("symbol").attr("id", "marker_battlefield").attr("viewBox", "0 0 30 30");
|
||||
symbol.append("path").attr("d", "M6,19 l9,10 L24,19").attr("fill", "#000000").attr("stroke", "none");
|
||||
symbol.append("circle").attr("cx", 15).attr("cy", 15).attr("r", 10).attr("fill", "#ffffff").attr("stroke", "#000000").attr("stroke-width", 1);
|
||||
symbol.append("text").attr("x", "50%").attr("y", "52%").attr("fill", "#000000").attr("stroke", "#3200ff").attr("stroke-width", 0)
|
||||
.attr("font-size", "12px").attr("dominant-baseline", "central").text("⚔️");
|
||||
}()
|
||||
|
||||
const getSide = (regs, n) => regs.length > 1 ?
|
||||
`${n ? "regiments" : "forces"} of ${list([... new Set(regs.map(r => pack.states[r.state].name))])}` :
|
||||
getAdjective(pack.states[regs[0].state].name) + " " + regs[0].name;
|
||||
const getLosses = casualties => Math.min(rn(casualties * 100), 100);
|
||||
|
||||
const legend = `${this.name} took place in ${options.year} ${options.eraShort}. It was fought between ${getSide(this.attackers.regiments, 1)} and ${getSide(this.defenders.regiments, 0)}. The ${this.type} ended in ${battleStatus[+P(.7)]}.
|
||||
\r\nAttackers losses: ${getLosses(this.attackers.casualties)}%, defenders losses: ${getLosses(this.defenders.casualties)}%`;
|
||||
const id = getNextId("markerElement");
|
||||
notes.push({id, name:this.name, legend});
|
||||
|
||||
markers.append("use").attr("id", id)
|
||||
.attr("xlink:href", "#marker_battlefield").attr("data-id", "#marker_battlefield")
|
||||
.attr("data-x", this.x).attr("data-y", this.y).attr("x", this.x - 15).attr("y", this.y - 30)
|
||||
.attr("data-size", 1).attr("width", 30).attr("height", 30);
|
||||
|
||||
$("#battleScreen").dialog("destroy");
|
||||
this.cleanData();
|
||||
}
|
||||
|
||||
cancelResults() {
|
||||
// move regiments back to initial positions
|
||||
this.attackers.regiments.concat(this.defenders.regiments).forEach(r => Military.moveRegiment(r, r.px, r.py));
|
||||
$("#battleScreen").dialog("close");
|
||||
this.cleanData();
|
||||
}
|
||||
|
||||
cleanData() {
|
||||
battleAttackers.innerHTML = battleDefenders.innerHTML = ""; // clean DOM
|
||||
customization = 0; // exit edit mode
|
||||
|
||||
// clean temp data
|
||||
this.attackers.regiments.concat(this.defenders.regiments).forEach(r => {
|
||||
delete r.px;
|
||||
delete r.py;
|
||||
delete r.casualties;
|
||||
delete r.survivors;
|
||||
});
|
||||
delete Battle.prototype.context;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -215,6 +215,8 @@ function editBiomes() {
|
|||
|
||||
function addCustomBiome() {
|
||||
const b = biomesData, i = biomesData.i.length;
|
||||
if (i > 254) {tip("Maximum number of biomes reached (255), data cleansing is required", false, "error"); return;}
|
||||
|
||||
b.i.push(i);
|
||||
b.color.push(getRandomColor());
|
||||
b.habitability.push(50);
|
||||
|
|
|
|||
|
|
@ -294,13 +294,14 @@ function editBurg(id) {
|
|||
|
||||
function openMFCG(seed) {
|
||||
if (!seed && burg.MFCGlink) {openURL(burg.MFCGlink); return;}
|
||||
const cells = pack.cells;
|
||||
const name = elSelected.text();
|
||||
const size = Math.max(Math.min(rn(burg.population), 65), 6);
|
||||
|
||||
const s = burg.MFCG || defSeed;
|
||||
const cell = burg.cell;
|
||||
const hub = +pack.cells.road[cell] > 50;
|
||||
const river = pack.cells.r[cell] ? 1 : 0;
|
||||
const hub = +cells.road[cell] > 50;
|
||||
const river = cells.r[cell] ? 1 : 0;
|
||||
|
||||
const coast = +burg.port;
|
||||
const citadel = +burg.citadel;
|
||||
|
|
@ -309,8 +310,18 @@ function editBurg(id) {
|
|||
const temple = +burg.temple;
|
||||
const shanty = +burg.shanty;
|
||||
|
||||
const site = "http://fantasycities.watabou.ru/";
|
||||
const url = `${site}?name=${name}&size=${size}&seed=${s}&hub=${hub}&random=0&continuous=0&river=${river}&coast=${coast}&citadel=${citadel}&plaza=${plaza}&temple=${temple}&walls=${walls}&shantytown=${shanty}`;
|
||||
const sea = coast && cells.haven[burg.cell] ? getSeaDirections(burg.cell) : "";
|
||||
function getSeaDirections(i) {
|
||||
const p1 = cells.p[i];
|
||||
const p2 = cells.p[cells.haven[i]];
|
||||
let deg = Math.atan2(p2[1] - p1[1], p2[0] - p1[0]) * 180 / Math.PI - 90;
|
||||
if (deg < 0) deg += 360;
|
||||
const norm = rn(normalize(deg, 0, 360) * 2, 2); // 0 = south, 0.5 = west, 1 = north, 1.5 = east
|
||||
return "&sea="+norm;
|
||||
}
|
||||
|
||||
const site = "http://fantasycities.watabou.ru/?random=0&continuous=0";
|
||||
const url = `${site}&name=${name}&size=${size}&seed=${s}&hub=${hub}&river=${river}&coast=${coast}&citadel=${citadel}&plaza=${plaza}&temple=${temple}&walls=${walls}&shantytown=${shanty}${sea}`;
|
||||
openURL(url);
|
||||
}
|
||||
}
|
||||
|
|
@ -426,4 +437,4 @@ function editBurg(id) {
|
|||
unselect();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ function editCoastline(node = d3.event.target) {
|
|||
lineGen.curve(d3.curveBasisClosed);
|
||||
const f = +elSelected.attr("data-f");
|
||||
const vertices = pack.features[f].vertices;
|
||||
const points = vertices.map(v => pack.vertices.p[v]);
|
||||
const points = clipPoly(vertices.map(v => pack.vertices.p[v]), 1);
|
||||
const d = round(lineGen(points));
|
||||
elSelected.attr("d", d);
|
||||
defs.select("mask#land > path#land_"+f).attr("d", d); // update land mask
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ function clicked() {
|
|||
else if (el.tagName === "tspan" && grand.parentNode.parentNode.id === "labels") editLabel();
|
||||
else if (grand.id === "burgLabels") editBurg();
|
||||
else if (grand.id === "burgIcons") editBurg();
|
||||
else if (parent.id === "ice") editIce();
|
||||
else if (parent.id === "terrain") editReliefIcon();
|
||||
else if (parent.id === "markers") editMarker();
|
||||
else if (grand.id === "coastline") editCoastline();
|
||||
|
|
@ -110,12 +111,6 @@ function applySorting(headers) {
|
|||
}).forEach(line => list.appendChild(line));
|
||||
}
|
||||
|
||||
// trigger trash button click on "Delete" keypress
|
||||
function removeElementOnKey() {
|
||||
$(".dialog:visible .icon-trash").click();
|
||||
$("button:visible:contains('Remove')").click();
|
||||
}
|
||||
|
||||
function addBurg(point) {
|
||||
const cells = pack.cells;
|
||||
const x = rn(point[0], 2), y = rn(point[1], 2);
|
||||
|
|
@ -376,7 +371,7 @@ function createPicker() {
|
|||
const height = bbox.height + 9;
|
||||
|
||||
picker.insert("rect", ":first-child").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height).attr("fill", "#ffffff").attr("stroke", "#5d4651").on("mousemove", pos);
|
||||
picker.insert("text", ":first-child").attr("x", 291).attr("y", -11).attr("id", "pickerCloseText").text("✖");
|
||||
picker.insert("text", ":first-child").attr("x", 291).attr("y", -10).attr("id", "pickerCloseText").text("✕");
|
||||
picker.insert("rect", ":first-child").attr("x", 288).attr("y", -21).attr("id", "pickerCloseRect").attr("width", 14).attr("height", 14).on("mousemove", cl).on("click", closePicker);
|
||||
picker.insert("text", ":first-child").attr("x", 12).attr("y", -10).attr("id", "pickerLabel").text("Color Picker").on("mousemove", pos);
|
||||
picker.insert("rect", ":first-child").attr("x", 0).attr("y", -30).attr("width", width).attr("height", 30).attr("id", "pickerHeader").on("mousemove", pos);
|
||||
|
|
@ -526,11 +521,26 @@ function changePickerSpace() {
|
|||
openPicker.updateFill();
|
||||
}
|
||||
|
||||
// remove all fogging
|
||||
function unfog() {
|
||||
defs.select("#fog").selectAll("path").remove();
|
||||
fogging.selectAll("path").remove();
|
||||
fogging.style("display", "none");
|
||||
// add fogging
|
||||
function fog(id, path) {
|
||||
if (defs.select("#fog #"+id).size()) return;
|
||||
const fadeIn = d3.transition().duration(2000).ease(d3.easeSinInOut);
|
||||
if (defs.select("#fog path").size()) {
|
||||
defs.select("#fog").append("path").attr("d", path).attr("id", id).attr("opacity", 0).transition(fadeIn).attr("opacity", 1);
|
||||
} else {
|
||||
defs.select("#fog").append("path").attr("d", path).attr("id", id).attr("opacity", 1);
|
||||
const opacity = fogging.attr("opacity");
|
||||
fogging.style("display", "block").attr("opacity", 0).transition(fadeIn).attr("opacity", opacity);
|
||||
}
|
||||
}
|
||||
|
||||
// remove fogging
|
||||
function unfog(id) {
|
||||
let el = defs.select("#fog #"+id);
|
||||
if (!id || !el.size()) el = defs.select("#fog").selectAll("path");
|
||||
|
||||
el.remove();
|
||||
if (!defs.selectAll("#fog path").size()) fogging.style("display", "none");
|
||||
}
|
||||
|
||||
function getFileName(dataType) {
|
||||
|
|
@ -581,4 +591,43 @@ function highlightElement(element) {
|
|||
let y = box.y + box.height / 2;
|
||||
if (tr[1]) y += tr[1];
|
||||
if (scale >= 2) zoomTo(x, y, scale, 1600);
|
||||
}
|
||||
|
||||
function selectIcon(initial, callback) {
|
||||
if (!callback) return;
|
||||
$("#iconSelector").dialog();
|
||||
|
||||
const table = document.getElementById("iconTable");
|
||||
const input = document.getElementById("iconInput");
|
||||
input.value = initial;
|
||||
|
||||
if (!table.innerHTML) {
|
||||
const icons = ["⚔️","🏹","🐴","💣","🌊","🎯","⚓","🔮","📯","⚒️","🛡️","👑","⚜️",
|
||||
"☠️","🎆","🗡️","🔪","⛏️","🔥","🩸","💧","🐾","🎪","🏰","🏯","⛓️","❤️","💘","💜","📜","🔔",
|
||||
"🔱","💎","🌈","🌠","✨","💥","☀️","🌙","⚡","❄️","♨️","🎲","🚨","🌉","🗻","🌋","🧱",
|
||||
"⚖️","✂️","🎵","👗","🎻","🎨","🎭","⛲","💉","📖","📕","🎁","💍","⏳","🕸️","⚗️","☣️","☢️",
|
||||
"🔰","🎖️","🚩","🏳️","🏴","💪","✊","👊","🤜","🤝","🙏","🧙","🧙♀️","💂","🤴","🧛","🧟","🧞","🧝","👼",
|
||||
"👻","👺","👹","🦄","🐲","🐉","🐎","🦓","🐺","🦊","🐱","🐈","🦁","🐯","🐅","🐆","🐕","🦌","🐵","🐒","🦍",
|
||||
"🦅","🕊️","🐓","🦇","🦜","🐦","🦉","🐮","🐄","🐂","🐃","🐷","🐖","🐗","🐏","🐑","🐐","🐫","🦒","🐘","🦏","🐭","🐁","🐀",
|
||||
"🐹","🐰","🐇","🦔","🐸","🐊","🐢","🦎","🐍","🐳","🐬","🦈","🐠","🐙","🦑","🐌","🦋","🐜","🐝","🐞","🦗","🕷️","🦂","🦀",
|
||||
"🌳","🌲","🎄","🌴","🍂","🍁","🌵","☘️","🍀","🌿","🌱","🌾","🍄","🌽","🌸","🌹","🌻",
|
||||
"🍒","🍏","🍇","🍉","🍅","🍓","🥔","🥕","🥩","🍗","🍞","🍻","🍺","🍲","🍷"
|
||||
];
|
||||
|
||||
let row = "";
|
||||
for (let i=0; i < icons.length; i++) {
|
||||
if (i%17 === 0) row = table.insertRow(i/17|0);
|
||||
const cell = row.insertCell(i%17);
|
||||
cell.innerHTML = icons[i];
|
||||
}
|
||||
}
|
||||
|
||||
table.onclick = e => {if (e.target.tagName === "TD") {input.value = e.target.innerHTML; callback(input.value)}};
|
||||
table.onmouseover = e => {if (e.target.tagName === "TD") tip(`Click to select ${e.target.innerHTML} icon`)};
|
||||
|
||||
$("#iconSelector").dialog({width: fitContent(), title: "Select Icon",
|
||||
buttons: {
|
||||
Apply: function() {callback(input.value||"⠀"); $(this).dialog("close")},
|
||||
Close: function() {callback(initial); $(this).dialog("close")}}
|
||||
});
|
||||
}
|
||||
274
modules/ui/elevation-profile.js
Normal file
274
modules/ui/elevation-profile.js
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
"use strict";
|
||||
|
||||
function showEPForRoute(node) {
|
||||
const points = [];
|
||||
debug.select("#controlPoints").selectAll("circle").each(function() {
|
||||
const i = findCell(this.getAttribute("cx"), this.getAttribute("cy"));
|
||||
points.push(i);
|
||||
});
|
||||
|
||||
const routeLen = node.getTotalLength() * distanceScaleInput.value;
|
||||
showElevationProfile(points, routeLen, false);
|
||||
}
|
||||
|
||||
function showEPForRiver(node) {
|
||||
const points = [];
|
||||
debug.select("#controlPoints").selectAll("circle").each(function() {
|
||||
const i = findCell(this.getAttribute("cx"), this.getAttribute("cy"));
|
||||
points.push(i);
|
||||
});
|
||||
|
||||
const riverLen = (node.getTotalLength() / 2) * distanceScaleInput.value;
|
||||
showElevationProfile(points, riverLen, true);
|
||||
}
|
||||
|
||||
function showElevationProfile(data, routeLen, isRiver) {
|
||||
// data is an array of cell indexes, routeLen is the distance (in actual metres/feet), isRiver should be true for rivers, false otherwise
|
||||
document.getElementById("epScaleRange").addEventListener("change", draw);
|
||||
document.getElementById("epCurve").addEventListener("change", draw);
|
||||
document.getElementById("epSave").addEventListener("click", downloadCSV);
|
||||
|
||||
$("#elevationProfile").dialog({
|
||||
title: "Elevation profile", resizable: false, width: window.width,
|
||||
close: closeElevationProfile,
|
||||
position: {my: "left top", at: "left+20 bottom-500", of: window, collision: "fit"}
|
||||
});
|
||||
|
||||
// prevent river graphs from showing rivers as flowing uphill - remember the general slope
|
||||
let slope = 0;
|
||||
if (isRiver) {
|
||||
if (pack.cells.h[data[0]] < pack.cells.h[data[data.length-1]]) {
|
||||
slope = 1; // up-hill
|
||||
} else if (pack.cells.h[data[0]] > pack.cells.h[data[data.length-1]]) {
|
||||
slope = -1; // down-hill
|
||||
}
|
||||
}
|
||||
|
||||
const chartWidth = window.innerWidth-180, chartHeight = 300; // height of our land/sea profile, excluding the biomes data below
|
||||
const xOffset = 80, yOffset = 80; // this is our drawing starting point from top-left (y = 0) of SVG
|
||||
const biomesHeight = 40;
|
||||
|
||||
let lastBurgIndex = 0;
|
||||
let lastBurgCell = 0;
|
||||
let burgCount = 0;
|
||||
let chartData = {biome:[], burg:[], cell:[], height:[], mi:1000000, ma:0, mih: 100, mah: 0, points:[]};
|
||||
for (let i=0, prevB=0, prevH=-1; i <data.length; i++) {
|
||||
let cell = data[i];
|
||||
let h = pack.cells.h[cell];
|
||||
if (h < 20) h = 20;
|
||||
|
||||
// check for river up-hill
|
||||
if (prevH != -1) {
|
||||
if (isRiver) {
|
||||
if (slope == 1 && h < prevH) h = prevH;
|
||||
else if (slope == 0 && h != prevH) h = prevH;
|
||||
else if (slope == -1 && h > prevH) h = prevH;
|
||||
}
|
||||
}
|
||||
prevH = h;
|
||||
|
||||
let b = pack.cells.burg[cell];
|
||||
if (b == prevB) b = 0;
|
||||
else prevB = b;
|
||||
if (b) { burgCount++; lastBurgIndex = i; lastBurgCell = cell; }
|
||||
|
||||
chartData.biome[i] = pack.cells.biome[cell];
|
||||
chartData.burg[i] = b;
|
||||
chartData.cell[i] = cell;
|
||||
let sh = getHeight(h);
|
||||
chartData.height[i] = parseInt(sh.substr(0, sh.indexOf(' ')));
|
||||
chartData.mih = Math.min(chartData.mih, h);
|
||||
chartData.mah = Math.max(chartData.mah, h);
|
||||
chartData.mi = Math.min(chartData.mi, chartData.height[i]);
|
||||
chartData.ma = Math.max(chartData.ma, chartData.height[i]);
|
||||
}
|
||||
|
||||
if (lastBurgIndex != 0 && lastBurgCell == chartData.cell[data.length-1] && lastBurgIndex < data.length) {
|
||||
chartData.burg[data.length-1] = chartData.burg[lastBurgIndex];
|
||||
chartData.burg[lastBurgIndex] = 0;
|
||||
}
|
||||
|
||||
draw();
|
||||
|
||||
function downloadCSV() {
|
||||
let data = "Point,X,Y,Cell,Height,Height value,Population,Burg,Burg population,Biome,Biome color,Culture,Culture color,Religion,Religion color,Province,Province color,State,State color\n"; // headers
|
||||
|
||||
for (let k=0; k < chartData.points.length; k++) {
|
||||
let cell = chartData.cell[k];
|
||||
let burg = pack.cells.burg[cell];
|
||||
let biome = pack.cells.biome[cell];
|
||||
let culture = pack.cells.culture[cell];
|
||||
let religion = pack.cells.religion[cell];
|
||||
let province = pack.cells.province[cell];
|
||||
let state = pack.cells.state[cell];
|
||||
let pop = pack.cells.pop[cell];
|
||||
let h = pack.cells.h[cell];
|
||||
|
||||
data += k+1 + ",";
|
||||
data += chartData.points[k][0] + ",";
|
||||
data += chartData.points[k][1] + ",";
|
||||
data += cell + ",";
|
||||
data += getHeight(h) + ",";
|
||||
data += h + ",";
|
||||
data += rn(pop * populationRate.value) + ",";
|
||||
if (burg) {
|
||||
data += pack.burgs[burg].name + ",";
|
||||
data += (pack.burgs[burg].population * populationRate.value * urbanization.value) + ",";
|
||||
} else {
|
||||
data += ",0,";
|
||||
}
|
||||
data += biomesData.name[biome] + ",";
|
||||
data += biomesData.color[biome] + ",";
|
||||
data += pack.cultures[culture].name + ",";
|
||||
data += pack.cultures[culture].color + ",";
|
||||
data += pack.religions[religion].name + ",";
|
||||
data += pack.religions[religion].color + ",";
|
||||
data += pack.provinces[province].name + ",";
|
||||
data += pack.provinces[province].color + ",";
|
||||
data += pack.states[state].name + ",";
|
||||
data += pack.states[state].color + ",";
|
||||
|
||||
data = data + "\n";
|
||||
}
|
||||
|
||||
const name = getFileName("elevation profile") + ".csv";
|
||||
downloadFile(data, name);
|
||||
}
|
||||
|
||||
function draw() {
|
||||
chartData.points = [];
|
||||
let heightScale = 100 / parseInt(epScaleRange.value);
|
||||
|
||||
heightScale *= .9; // curves cause the heights to go slightly higher, adjust here
|
||||
|
||||
const xscale = d3.scaleLinear().domain([0, data.length]).range([0, chartWidth]);
|
||||
const yscale = d3.scaleLinear().domain([0, chartData.ma * heightScale]).range([chartHeight, 0]);
|
||||
|
||||
for (let i=0; i<data.length; i++) {
|
||||
chartData.points.push([xscale(i) + xOffset, yscale(chartData.height[i]) + yOffset]);
|
||||
}
|
||||
|
||||
document.getElementById("elevationGraph").innerHTML = "";
|
||||
|
||||
const chart = d3.select("#elevationGraph").append("svg").attr("width", chartWidth+120).attr("height", chartHeight+yOffset+biomesHeight).attr("id", "elevationSVG").attr("class", "epbackground");
|
||||
// arrow-head definition
|
||||
chart.append("defs").append("marker").attr("id", "arrowhead").attr("orient", "auto").attr("markerWidth", "2").attr("markerHeight", "4").attr("refX", "0.1").attr("refY", "2").append("path").attr("d", "M0,0 V4 L2,2 Z").attr("fill", "darkgray");
|
||||
|
||||
let colors = getColorScheme();
|
||||
const landdef = chart.select("defs").append("linearGradient").attr("id", "landdef").attr("x1", "0%").attr("y1", "0%").attr("x2", "0%").attr("y2", "100%");
|
||||
|
||||
if (chartData.mah == chartData.mih) {
|
||||
landdef.append("stop").attr("offset", "0%").attr("style", "stop-color:" + getColor(chartData.mih, colors) + ";stop-opacity:1");
|
||||
landdef.append("stop").attr("offset", "100%").attr("style", "stop-color:" + getColor(chartData.mah, colors) + ";stop-opacity:1");
|
||||
} else {
|
||||
for (let k=chartData.mah; k >= chartData.mih; k--) {
|
||||
let perc = 1 - (k - chartData.mih) / (chartData.mah - chartData.mih);
|
||||
landdef.append("stop").attr("offset", perc*100 + "%").attr("style", "stop-color:" + getColor(k, colors) + ";stop-opacity:1");
|
||||
}
|
||||
}
|
||||
|
||||
// land
|
||||
let curve = d3.line().curve(d3.curveBasis); // see https://github.com/d3/d3-shape#curves
|
||||
let epCurveIndex = parseInt(epCurve.selectedIndex);
|
||||
switch(epCurveIndex) {
|
||||
case 0 : curve = d3.line().curve(d3.curveLinear); break;
|
||||
case 1 : curve = d3.line().curve(d3.curveBasis); break;
|
||||
case 2 : curve = d3.line().curve(d3.curveBundle.beta(1)); break;
|
||||
case 3 : curve = d3.line().curve(d3.curveCatmullRom.alpha(0.5)); break;
|
||||
case 4 : curve = d3.line().curve(d3.curveMonotoneX); break;
|
||||
case 5 : curve = d3.line().curve(d3.curveNatural); break;
|
||||
}
|
||||
|
||||
// copy the points so that we can add extra straight pieces, else we get curves at the ends of the chart
|
||||
let extra = chartData.points.slice();
|
||||
let path = curve(extra);
|
||||
// this completes the right-hand side and bottom of our land "polygon"
|
||||
path += " L" + parseInt(xscale(extra.length) + +xOffset) + "," + parseInt(extra[extra.length-1][1]);
|
||||
path += " L" + parseInt(xscale(extra.length) + +xOffset) + "," + parseInt(yscale(0) + +yOffset);
|
||||
path += " L" + parseInt(xscale(0) + +xOffset) +"," + parseInt(yscale(0) + +yOffset);
|
||||
path += "Z";
|
||||
chart.append("g").attr("id", "epland").append("path").attr("d", path).attr("stroke", "purple").attr("stroke-width", "0").attr("fill", "url(#landdef)");
|
||||
|
||||
// biome / heights
|
||||
let g = chart.append("g").attr("id", "epbiomes");
|
||||
const hu = heightUnit.value;
|
||||
for(let k=0; k < chartData.points.length; k++) {
|
||||
const x = chartData.points[k][0];
|
||||
const y = yOffset + chartHeight;
|
||||
const c = biomesData.color[chartData.biome[k]];
|
||||
const dataTip = biomesData.name[chartData.biome[k]]+" (" + chartData.height[k] + " " + hu + ", cell " + chartData.cell[k] + ")";
|
||||
|
||||
g.append("rect").attr("stroke", c).attr("fill", c).attr("x", x).attr("y", y).attr("width", xscale(1)).attr("height", 15).attr("data-tip", dataTip);
|
||||
}
|
||||
|
||||
const xAxis = d3.axisBottom(xscale).ticks(10).tickFormat(function(d){ return (rn(d / chartData.points.length * routeLen) + " " + distanceUnitInput.value);});
|
||||
const yAxis = d3.axisLeft(yscale).ticks(5).tickFormat(function(d) { return d + " " + hu; });
|
||||
|
||||
const xGrid = d3.axisBottom(xscale).ticks(10).tickSize(-chartHeight).tickFormat("");
|
||||
const yGrid = d3.axisLeft(yscale).ticks(5).tickSize(-chartWidth).tickFormat("");
|
||||
|
||||
chart.append("g")
|
||||
.attr("id", "epxaxis")
|
||||
.attr("transform", "translate(" + xOffset + "," + parseInt(chartHeight + +yOffset + 20) + ")")
|
||||
.call(xAxis)
|
||||
.selectAll("text")
|
||||
.style("text-anchor", "center")
|
||||
.attr("transform", function(d) {
|
||||
return "rotate(0)" // used to rotate labels, - anti-clockwise, + clockwise
|
||||
});
|
||||
|
||||
chart.append("g")
|
||||
.attr("id", "epyaxis")
|
||||
.attr("transform", "translate(" + parseInt(+xOffset-10) + "," + parseInt(+yOffset) + ")")
|
||||
.call(yAxis);
|
||||
|
||||
// add the X gridlines
|
||||
chart.append("g")
|
||||
.attr("id", "epxgrid")
|
||||
.attr("class", "epgrid")
|
||||
.attr("stroke-dasharray", "4 1")
|
||||
.attr("transform", "translate(" + xOffset + "," + parseInt(chartHeight + +yOffset) + ")")
|
||||
.call(xGrid);
|
||||
|
||||
// add the Y gridlines
|
||||
chart.append("g")
|
||||
.attr("id", "epygrid")
|
||||
.attr("class", "epgrid")
|
||||
.attr("stroke-dasharray", "4 1")
|
||||
.attr("transform", "translate(" + xOffset + "," + yOffset + ")")
|
||||
.call(yGrid);
|
||||
|
||||
// draw city labels - try to avoid putting labels over one another
|
||||
g = chart.append("g").attr("id", "epburglabels");
|
||||
let y1 = 0;
|
||||
const add = 15;
|
||||
|
||||
let xwidth = chartData.points[1][0] - chartData.points[0][0];
|
||||
for (let k=0; k<chartData.points.length; k++) {
|
||||
if (chartData.burg[k] > 0) {
|
||||
let b = chartData.burg[k];
|
||||
|
||||
let x1 = chartData.points[k][0]; // left side of graph by default
|
||||
if (k > 0) x1 += xwidth/2; // center it if not first
|
||||
if (k == chartData.points.length-1) x1 = chartWidth + xOffset; // right part of graph
|
||||
y1+=add;
|
||||
if (y1 >= yOffset) y1 = add;
|
||||
|
||||
// burg name
|
||||
g.append("text").attr("id", "ep" + b).attr("class", "epburglabel").attr("x", x1).attr("y", y1).attr("text-anchor", "middle");
|
||||
document.getElementById("ep" + b).innerHTML = pack.burgs[b].name;
|
||||
|
||||
// arrow from burg name to graph line
|
||||
g.append("path").attr("id", "eparrow" + b).attr("d", "M" + x1.toString() + "," + (y1+3).toString() + "L" + x1.toString() + "," + parseInt(chartData.points[k][1]-3).toString()).attr("stroke", "darkgray").attr("fill", "lightgray").attr("stroke-width", "1").attr("marker-end", "url(#arrowhead)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function closeElevationProfile() {
|
||||
document.getElementById("epScaleRange").removeEventListener("change", draw);
|
||||
document.getElementById("epCurve").removeEventListener("change", draw);
|
||||
document.getElementById("epSave").removeEventListener("click", downloadCSV);
|
||||
document.getElementById("elevationGraph").innerHTML = "";
|
||||
modules.elevation = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -106,6 +106,7 @@ function showMapTooltip(point, e, i, g) {
|
|||
if (group === "lakes" && !land) {tip(`${capitalize(subgroup)} lake. Click to edit`); return;}
|
||||
if (group === "coastline") {tip("Click to edit the coastline"); return;}
|
||||
if (group === "zones") {tip(path[path.length-8].dataset.description); return;}
|
||||
if (group === "ice") {tip("Click to edit the Ice"); return;}
|
||||
|
||||
// covering elements
|
||||
if (layerIsOn("togglePrec") && land) tip("Annual Precipitation: "+ getFriendlyPrecipitation(i)); else
|
||||
|
|
@ -222,16 +223,20 @@ function getRiverInfo(id) {
|
|||
return r ? `${r.name} ${r.type} (${id})` : "n/a";
|
||||
}
|
||||
|
||||
function getCellPopulation(i) {
|
||||
const rural = pack.cells.pop[i] * populationRate.value;
|
||||
const urban = pack.cells.burg[i] ? pack.burgs[pack.cells.burg[i]].population * populationRate.value * urbanization.value : 0;
|
||||
return [rural, urban];
|
||||
}
|
||||
|
||||
// get user-friendly (real-world) population value from map data
|
||||
function getFriendlyPopulation(i) {
|
||||
const rural = pack.cells.pop[i] * populationRate.value;
|
||||
const urban = pack.cells.burg[i] ? pack.burgs[pack.cells.burg[i]].population * populationRate.value * urbanization.value : 0;
|
||||
const [rural, urban] = getCellPopulation(i);
|
||||
return `${si(rural+urban)} (${si(rural)} rural, urban ${si(urban)})`;
|
||||
}
|
||||
|
||||
function getPopulationTip(i) {
|
||||
const rural = pack.cells.pop[i] * populationRate.value;
|
||||
const urban = pack.cells.burg[i] ? pack.burgs[pack.cells.burg[i]].population * populationRate.value * urbanization.value : 0;
|
||||
const [rural, urban] = getCellPopulation(i);
|
||||
return `Cell population: ${si(rural+urban)}; Rural: ${si(rural)}; Urban: ${si(urban)}`;
|
||||
}
|
||||
|
||||
|
|
@ -403,6 +408,7 @@ document.addEventListener("keyup", event => {
|
|||
else if (key === 85) toggleRoutes(); // "U" to toggle Routes layer
|
||||
else if (key === 84) toggleTemp(); // "T" to toggle Temperature layer
|
||||
else if (key === 78) togglePopulation(); // "N" to toggle Population layer
|
||||
else if (key === 74) toggleIce(); // "J" to toggle Ice layer
|
||||
else if (key === 65) togglePrec(); // "A" to toggle Precipitation layer
|
||||
else if (key === 76) toggleLabels(); // "L" to toggle Labels layer
|
||||
else if (key === 73) toggleIcons(); // "I" to toggle Icons layer
|
||||
|
|
@ -456,4 +462,10 @@ function pressControl() {
|
|||
if (zonesRemove.offsetParent) {
|
||||
zonesRemove.classList.contains("pressed") ? zonesRemove.classList.remove("pressed") : zonesRemove.classList.add("pressed");
|
||||
}
|
||||
}
|
||||
|
||||
// trigger trash button click on "Delete" keypress
|
||||
function removeElementOnKey() {
|
||||
$(".dialog:visible .fastDelete").click();
|
||||
$("button:visible:contains('Remove')").click();
|
||||
}
|
||||
|
|
@ -3,13 +3,13 @@
|
|||
|
||||
function editHeightmap() {
|
||||
void function selectEditMode() {
|
||||
alertMessage.innerHTML = `<span>Heightmap is a core element on which all other data (rivers, burgs, states etc) is based.
|
||||
So the best edit approach is to <i>erase</i> the secondary data and let the system automatically regenerate it on edit completion.</span>
|
||||
<p>You can also <i>keep</i> all the data, but you won't be able to change the coastline.</p>
|
||||
<p>If you need to change the coastline and keep the data, you may try the <i>risk</i> edit option.
|
||||
The data will be restored as much as possible, but the coastline change can cause unexpected fluctuations and errors.</p>
|
||||
<p>Check out ${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Heightmap-customization", "wiki")} for guidance.</p>
|
||||
<p>Please <span class="pseudoLink" onclick=saveMap(); editHeightmap();>save the map</span> before editing the heightmap!</p>`;
|
||||
alertMessage.innerHTML = `Heightmap is a core element on which all other data (rivers, burgs, states etc) is based.
|
||||
So the best edit approach is to <i>erase</i> the secondary data and let the system automatically regenerate it on edit completion.
|
||||
<p><i>Erase</i> mode also allows you Convert an Image into a heightmap or use Template Editor.</p>
|
||||
<p>You can <i>keep</i> the data, but you won't be able to change the coastline.</p>
|
||||
<p>Try <i>risk</i> mode to change the coastline and keep the data. The data will be restored as much as possible, but it can cause unpredictable errors.</p>
|
||||
<p>Please <span class="pseudoLink" onclick=saveMap(); editHeightmap();>save the map</span> before editing the heightmap!</p>
|
||||
<p>Check out ${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Heightmap-customization", "wiki")} for guidance.</p>`;
|
||||
|
||||
$("#alert").dialog({resizable: false, title: "Edit Heightmap", width: "28em",
|
||||
buttons: {
|
||||
|
|
@ -21,7 +21,6 @@ function editHeightmap() {
|
|||
});
|
||||
}()
|
||||
|
||||
let edits = [];
|
||||
restartHistory();
|
||||
viewbox.insert("g", "#terrs").attr("id", "heights");
|
||||
|
||||
|
|
@ -62,9 +61,9 @@ function editHeightmap() {
|
|||
changeOnlyLand.checked = false;
|
||||
}
|
||||
|
||||
// hide convert and template buttons for the Keep mode
|
||||
applyTemplate.style.display = type === "keep" ? "none" : "inline-block";
|
||||
convertImage.style.display = type === "keep" ? "none" : "inline-block";
|
||||
// show convert and template buttons for Erase mode only
|
||||
applyTemplate.style.display = type === "erase" ? "inline-block" : "none";
|
||||
convertImage.style.display = type === "erase" ? "inline-block" : "none";
|
||||
|
||||
// hide erosion checkbox if mode is Keep
|
||||
changeHeightsBox.style.display = type === "keep" ? "none" : "inline-block";
|
||||
|
|
@ -131,6 +130,10 @@ function editHeightmap() {
|
|||
return;
|
||||
}
|
||||
|
||||
delete window.edits; // remove global variable
|
||||
redo.disabled = templateRedo.disabled = true;
|
||||
undo.disabled = templateUndo.disabled = true;
|
||||
|
||||
customization = 0;
|
||||
customizationMenu.style.display = "none";
|
||||
if (document.getElementById("options").querySelector(".tab > button.active").id === "toolsTab") toolsContent.style.display = "block";
|
||||
|
|
@ -141,7 +144,6 @@ function editHeightmap() {
|
|||
closeDialogs();
|
||||
resetZoom();
|
||||
|
||||
restartHistory();
|
||||
if (document.getElementById("preview")) document.getElementById("preview").remove();
|
||||
if (document.getElementById("canvas3d")) enterStandardView();
|
||||
|
||||
|
|
@ -185,7 +187,6 @@ function editHeightmap() {
|
|||
}
|
||||
|
||||
defineBiomes();
|
||||
|
||||
rankCells();
|
||||
Cultures.generate();
|
||||
Cultures.expand();
|
||||
|
|
@ -317,9 +318,7 @@ function editHeightmap() {
|
|||
const land = pack.cells.h[i] >= 20;
|
||||
|
||||
// check biome
|
||||
if (!biome[g]) pack.cells.biome[i] = getBiomeId(grid.cells.prec[g], grid.cells.temp[g]);
|
||||
else if (!land && biome[g]) pack.cells.biome[i] = 0;
|
||||
else pack.cells.biome[i] = biome[g];
|
||||
pack.cells.biome[i] = land && biome[g] ? biome[g] : getBiomeId(grid.cells.prec[g], pack.cells.h[i]);
|
||||
|
||||
// rivers data
|
||||
if (!changeHeights.checked) {
|
||||
|
|
@ -354,6 +353,8 @@ function editHeightmap() {
|
|||
if (!b.i || b.removed) continue;
|
||||
b.cell = findBurgCell(b.x, b.y);
|
||||
b.feature = pack.cells.f[b.cell];
|
||||
//if (b.port) b.port = pack.cells.f[pack.cells.haven[b.cell]]; // water body id
|
||||
|
||||
pack.cells.burg[b.cell] = b.i;
|
||||
if (!b.capital && pack.cells.h[b.cell] < 20) removeBurg(b.i);
|
||||
if (b.capital) pack.states[b.state].center = b.cell;
|
||||
|
|
@ -481,8 +482,8 @@ function editHeightmap() {
|
|||
|
||||
// restart edits from 1st step
|
||||
function restartHistory() {
|
||||
edits = [];
|
||||
edits.n = 0;
|
||||
window.edits = []; // declare temp global variable
|
||||
window.edits.n = 0;
|
||||
redo.disabled = templateRedo.disabled = true;
|
||||
undo.disabled = templateUndo.disabled = true;
|
||||
updateHistory();
|
||||
|
|
@ -502,7 +503,7 @@ function editHeightmap() {
|
|||
document.getElementById("brushesButtons").addEventListener("click", e => toggleBrushMode(e));
|
||||
document.getElementById("changeOnlyLand").addEventListener("click", e => changeOnlyLandClick(e));
|
||||
document.getElementById("undo").addEventListener("click", () => restoreHistory(edits.n-1));
|
||||
document.getElementById("redo").addEventListener("click", () => restoreHistory(edits.n+1));
|
||||
document.getElementById("redo").addEventListener("click", () => restoreHistory(edits.n+1));
|
||||
document.getElementById("rescaleShow").addEventListener("click", () => {
|
||||
document.getElementById("modifyButtons").style.display = "none";
|
||||
document.getElementById("rescaleSection").style.display = "block";
|
||||
|
|
@ -962,10 +963,11 @@ function editHeightmap() {
|
|||
|
||||
function openImageConverter() {
|
||||
if ($("#imageConverter").is(":visible")) return;
|
||||
imageToLoad.click();
|
||||
closeDialogs("#imageConverter");
|
||||
|
||||
$("#imageConverter").dialog({
|
||||
title: "Image Converter", minHeight: "auto", width: "19.5em", resizable: false,
|
||||
title: "Image Converter", maxHeight: svgHeight*.8, minHeight: "auto", width: "20em",
|
||||
position: {my: "right top", at: "right-10 top+10", of: "svg"},
|
||||
beforeClose: closeImageConverter
|
||||
});
|
||||
|
|
@ -977,15 +979,9 @@ function editHeightmap() {
|
|||
canvas.height = graphHeight;
|
||||
document.body.insertBefore(canvas, optionsContainer);
|
||||
|
||||
const img = new Image;
|
||||
img.id = "image";
|
||||
img.style.display = "none";
|
||||
document.body.appendChild(img);
|
||||
|
||||
setOverlayOpacity(0);
|
||||
|
||||
document.getElementById("convertImageLoad").classList.add("glow"); // add glow effect
|
||||
tip('Image Converter is opened. Upload the image and assign the height for each of the colors', true, "warn"); // main tip
|
||||
clearMainTip();
|
||||
tip('Image Converter is opened. Upload image and assign height value for each color', false, "warn"); // main tip
|
||||
|
||||
// remove all heights
|
||||
grid.cells.h = new Uint8Array(grid.cells.i.length);
|
||||
|
|
@ -997,10 +993,10 @@ function editHeightmap() {
|
|||
|
||||
// add color pallete
|
||||
void function createColorPallete() {
|
||||
const container = d3.select("#colorScheme");
|
||||
container.selectAll("div").data(d3.range(101)).enter().append("div").attr("data-color", i => i)
|
||||
d3.select("#imageConverterPalette").selectAll("div").data(d3.range(101))
|
||||
.enter().append("div").attr("data-color", i => i)
|
||||
.style("background-color", i => color(1-(i < 20 ? i-5 : i) / 100))
|
||||
.style("width", i => i < 20 || i > 70 ? ".2em" : ".1em")
|
||||
.style("width", i => i < 40 || i > 68 ? ".2em" : ".1em")
|
||||
.on("touchmove mousemove", showPalleteHeight).on("click", assignHeight);
|
||||
}()
|
||||
|
||||
|
|
@ -1009,6 +1005,7 @@ function editHeightmap() {
|
|||
document.getElementById("imageToLoad").addEventListener("change", loadImage);
|
||||
document.getElementById("convertAutoLum").addEventListener("click", () => autoAssing("lum"));
|
||||
document.getElementById("convertAutoHue").addEventListener("click", () => autoAssing("hue"));
|
||||
document.getElementById("convertAutoFMG").addEventListener("click", () => autoAssing("scheme"));
|
||||
document.getElementById("convertColorsButton").addEventListener("click", setConvertColorsNumber);
|
||||
document.getElementById("convertComplete").addEventListener("click", applyConversion);
|
||||
document.getElementById("convertCancel").addEventListener("click", cancelConversion);
|
||||
|
|
@ -1019,7 +1016,7 @@ function editHeightmap() {
|
|||
const height = +this.getAttribute("data-color");
|
||||
colorsSelectValue.innerHTML = height;
|
||||
colorsSelectFriendly.innerHTML = getHeight(height);
|
||||
const former = colorScheme.querySelector(".hoveredColor")
|
||||
const former = imageConverterPalette.querySelector(".hoveredColor")
|
||||
if (former) former.className = "";
|
||||
this.className = "hoveredColor";
|
||||
}
|
||||
|
|
@ -1029,54 +1026,48 @@ function editHeightmap() {
|
|||
this.value = ""; // reset input value to get triggered if the file is re-uploaded
|
||||
const reader = new FileReader();
|
||||
|
||||
const img = new Image;
|
||||
img.onload = function() {
|
||||
const ctx = document.getElementById("canvas").getContext("2d");
|
||||
ctx.drawImage(img, 0, 0, graphWidth, graphHeight);
|
||||
heightsFromImage(+convertColors.value);
|
||||
resetZoom();
|
||||
convertImageLoad.classList.remove("glow");
|
||||
};
|
||||
|
||||
reader.onloadend = function() {img.src = reader.result;};
|
||||
reader.onloadend = () => img.src = reader.result;
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
function heightsFromImage(count) {
|
||||
const ctx = document.getElementById("canvas").getContext("2d");
|
||||
const imageData = ctx.getImageData(0, 0, graphWidth, graphHeight);
|
||||
const data = imageData.data;
|
||||
const sourceImage = document.getElementById("canvas");
|
||||
const sampleCanvas = document.createElement("canvas");
|
||||
sampleCanvas.width = grid.cellsX;
|
||||
sampleCanvas.height = grid.cellsY;
|
||||
sampleCanvas.getContext('2d').drawImage(sourceImage, 0, 0, grid.cellsX, grid.cellsY);
|
||||
|
||||
const q = new RgbQuant({colors:count});
|
||||
q.sample(sampleCanvas);
|
||||
const data = q.reduce(sampleCanvas);
|
||||
const pallete = q.palette(true);
|
||||
|
||||
viewbox.select("#heights").selectAll("*").remove();
|
||||
d3.select("#imageConverter").selectAll("div.color-div").remove();
|
||||
colorsSelect.style.display = "block";
|
||||
colorsUnassigned.style.display = "block";
|
||||
colorsAssigned.style.display = "none";
|
||||
|
||||
const gridColors = grid.points.map(p => {
|
||||
const x = Math.floor(p[0]-.01), y = Math.floor(p[1]-.01);
|
||||
const i = (x + y * graphWidth) * 4;
|
||||
const r = data[i], g = data[i+1], b = data[i+2];
|
||||
return [r, g, b];
|
||||
});
|
||||
|
||||
const cmap = MMCQ.quantize(gridColors, count);
|
||||
const usedColors = new Set();
|
||||
sampleCanvas.remove(); // no need to keep
|
||||
|
||||
viewbox.select("#heights").selectAll("polygon").data(grid.cells.i).join("polygon")
|
||||
.attr("points", d => getGridPolygon(d))
|
||||
.attr("id", d => "cell"+d).attr("fill", d => {
|
||||
const clr = `rgb(${cmap.nearest(gridColors[d])})`;
|
||||
usedColors.add(clr);
|
||||
return clr;
|
||||
}).on("click", mapClicked);
|
||||
.attr("points", d => getGridPolygon(d)).attr("id", d => "cell"+d)
|
||||
.attr("fill", d => `rgb(${data[d*4]}, ${data[d*4+1]}, ${data[d*4+2]})`)
|
||||
.on("click", mapClicked);
|
||||
|
||||
const unassigned = [...usedColors].sort((a, b) => d3.lab(a).l - d3.lab(b).l);
|
||||
const unassignedContainer = d3.select("#colorsUnassigned");
|
||||
unassignedContainer.selectAll("div").data(unassigned).enter().append("div")
|
||||
const colors = pallete.map(p => `rgb(${p[0]}, ${p[1]}, ${p[2]})`);
|
||||
d3.select("#colorsUnassigned").selectAll("div").data(colors).enter().append("div")
|
||||
.attr("data-color", i => i).style("background-color", i => i)
|
||||
.attr("class", "color-div").on("click", colorClicked);
|
||||
|
||||
convertColors.value = unassigned.length;
|
||||
document.getElementById("colorsUnassignedNumber").innerHTML = colors.length;
|
||||
}
|
||||
|
||||
function mapClicked() {
|
||||
|
|
@ -1091,7 +1082,7 @@ function editHeightmap() {
|
|||
|
||||
const selectedColor = imageConverter.querySelector("div.selectedColor");
|
||||
if (selectedColor) selectedColor.classList.remove("selectedColor");
|
||||
const hoveredColor = colorScheme.querySelector("div.hoveredColor");
|
||||
const hoveredColor = imageConverterPalette.querySelector("div.hoveredColor");
|
||||
if (hoveredColor) hoveredColor.classList.remove("hoveredColor");
|
||||
colorsSelectValue.innerHTML = colorsSelectFriendly.innerHTML = 0;
|
||||
|
||||
|
|
@ -1100,7 +1091,7 @@ function editHeightmap() {
|
|||
|
||||
if (this.dataset.height) {
|
||||
const height = +this.dataset.height;
|
||||
colorScheme.querySelector(`div[data-color="${height}"]`).classList.add("hoveredColor");
|
||||
imageConverterPalette.querySelector(`div[data-color="${height}"]`).classList.add("hoveredColor");
|
||||
colorsSelectValue.innerHTML = height;
|
||||
colorsSelectFriendly.innerHTML = getHeight(height);
|
||||
}
|
||||
|
|
@ -1126,48 +1117,75 @@ function editHeightmap() {
|
|||
if (selectedColor.parentNode.id === "colorsUnassigned") {
|
||||
colorsAssigned.appendChild(selectedColor);
|
||||
colorsAssigned.style.display = "block";
|
||||
|
||||
document.getElementById("colorsUnassignedNumber").innerHTML = colorsUnassigned.childElementCount - 2;
|
||||
document.getElementById("colorsAssignedNumber").innerHTML = colorsAssigned.childElementCount - 2;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// auto assign color based on luminosity or hue
|
||||
function autoAssing(type) {
|
||||
const unassigned = colorsUnassigned.querySelectorAll("div");
|
||||
if (!unassigned.length) {tip("No unassigned colors. Please load an image and click the button again", false, "error"); return;}
|
||||
let unassigned = colorsUnassigned.querySelectorAll("div");
|
||||
if (!unassigned.length) {
|
||||
heightsFromImage(+convertColors.value);
|
||||
unassigned = colorsUnassigned.querySelectorAll("div");
|
||||
if (!unassigned.length) {
|
||||
tip("No unassigned colors. Please load an image and click the button again", false, "error");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const assinged = []; // assigned heights
|
||||
const getHeightByHue = function(color) {
|
||||
let hue = d3.hsl(color).h;
|
||||
if (hue > 300) hue -= 360;
|
||||
if (hue > 170) return Math.abs(hue-250) / 3 |0; // water
|
||||
return Math.abs(hue-250+20) / 3 |0; // land
|
||||
}
|
||||
|
||||
const getHeightByLum = function(color) {
|
||||
let lum = d3.lab(color).l;
|
||||
if (lum < 13) return lum / 13 * 20 |0; // water
|
||||
return lum|0; // land
|
||||
}
|
||||
|
||||
const scheme = d3.range(101).map(i => getColor(i, color()));
|
||||
const hues = scheme.map(rgb => d3.hsl(rgb).h|0);
|
||||
const getHeightByScheme = function(color) {
|
||||
let height = scheme.indexOf(color);
|
||||
if (height !== -1) return height; // exact match
|
||||
const hue = d3.hsl(color).h;
|
||||
const closest = hues.reduce((prev, curr) => (Math.abs(curr - hue) < Math.abs(prev - hue) ? curr : prev));
|
||||
return hues.indexOf(closest);
|
||||
}
|
||||
|
||||
const assinged = []; // store assigned heights
|
||||
unassigned.forEach(el => {
|
||||
const colorFrom = el.dataset.color;
|
||||
const lab = d3.lab(colorFrom);
|
||||
const normalized = type === "hue" ? rn(normalize(lab.b + lab.a / 2, -50, 200), 2) : rn(normalize(lab.l, -15, 100), 2);
|
||||
let heightTo = rn(normalized * 100);
|
||||
if (assinged[heightTo] && heightTo < 100) heightTo += 1; // if height is already added, try increated one
|
||||
if (assinged[heightTo] && heightTo < 100) heightTo += 1; // if height is already added, try increated one
|
||||
if (assinged[heightTo] && heightTo > 3) heightTo -= 3; // if increased one is also added, try decreased one
|
||||
if (assinged[heightTo] && heightTo > 1) heightTo -= 1; // if increased one is also added, try decreased one
|
||||
const clr = el.dataset.color;
|
||||
const height = type === "hue" ? getHeightByHue(clr) : type === "lum" ? getHeightByLum(clr) : getHeightByScheme(clr);
|
||||
const colorTo = color(1 - (height < 20 ? (height-5) / 100 : height / 100));
|
||||
viewbox.select("#heights").selectAll("polygon[fill='" + clr + "']").attr("fill", colorTo).attr("data-height", height);
|
||||
|
||||
const colorTo = color(1 - (heightTo < 20 ? (heightTo-5)/100 : heightTo/100));
|
||||
viewbox.select("#heights").selectAll("polygon[fill='" + colorFrom + "']").attr("fill", colorTo).attr("data-height", heightTo);
|
||||
|
||||
if (assinged[heightTo]) {el.remove(); return;} // if color is already added, remove it
|
||||
if (assinged[height]) {el.remove(); return;} // if color is already added, remove it
|
||||
el.style.backgroundColor = el.dataset.color = colorTo;
|
||||
el.dataset.height = heightTo;
|
||||
el.dataset.height = height;
|
||||
colorsAssigned.appendChild(el);
|
||||
assinged[heightTo] = true;
|
||||
assinged[height] = true;
|
||||
});
|
||||
|
||||
// sort assigned colors by height
|
||||
Array.from(colorsAssigned.children).sort((a, b) => {
|
||||
return +a.dataset.height - +b.dataset.height;
|
||||
}).forEach(line => colorsAssigned.appendChild(line));
|
||||
Array.from(colorsAssigned.children)
|
||||
.sort((a, b) => +a.dataset.height - +b.dataset.height)
|
||||
.forEach(line => colorsAssigned.appendChild(line));
|
||||
|
||||
colorsAssigned.style.display = "block";
|
||||
colorsUnassigned.style.display = "none";
|
||||
document.getElementById("colorsAssignedNumber").innerHTML = colorsAssigned.childElementCount - 2;
|
||||
}
|
||||
|
||||
function setConvertColorsNumber() {
|
||||
prompt(`Please provide a desired number of colors. <br>An actual number depends on color scheme and may vary from desired`,
|
||||
{default:convertColors.value, step:1, min:3, max:255}, number => {
|
||||
prompt(`Please set maximum number of colors. <br>An actual number is usually lower and depends on color scheme`,
|
||||
{default:+convertColors.value, step:1, min:3, max:255}, number => {
|
||||
convertColors.value = number;
|
||||
heightsFromImage(number);
|
||||
});
|
||||
|
|
@ -1179,6 +1197,11 @@ function editHeightmap() {
|
|||
}
|
||||
|
||||
function applyConversion() {
|
||||
if (colorsAssigned.childElementCount < 3) {
|
||||
tip("Please do the assignment first", false, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
viewbox.select("#heights").selectAll("polygon").each(function() {
|
||||
const height = +this.dataset.height || 0;
|
||||
const i = +this.id.slice(4);
|
||||
|
|
@ -1198,9 +1221,7 @@ function editHeightmap() {
|
|||
|
||||
function restoreImageConverterState() {
|
||||
const canvas = document.getElementById("canvas");
|
||||
if (canvas) canvas.remove(); else return;
|
||||
const img = document.getElementById("image");
|
||||
if (img) img.remove(); else return;
|
||||
if (canvas) canvas.remove();
|
||||
|
||||
d3.select("#imageConverter").selectAll("div.color-div").remove();
|
||||
colorsAssigned.style.display = "none";
|
||||
|
|
@ -1209,12 +1230,18 @@ function editHeightmap() {
|
|||
viewbox.style("cursor", "default").on(".drag", null);
|
||||
tip('Heightmap edit mode is active. Click on "Exit Customization" to finalize the heightmap', true);
|
||||
$("#imageConverter").dialog("destroy");
|
||||
openBrushesPanel();
|
||||
}
|
||||
|
||||
function closeImageConverter(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
alertMessage.innerHTML = 'Are you sure you want to close the Image Converter? Click "Cancel" to geck back to convertion. Click "Complete" to apply the conversion. Click "Close" to exit conversion mode and restore previous heightmap';
|
||||
alertMessage.innerHTML = `
|
||||
Are you sure you want to close the Image Converter?
|
||||
Click "Cancel" to geck back to convertion.
|
||||
Click "Complete" to apply the conversion.
|
||||
Click "Close" to exit conversion mode and restore previous heightmap`;
|
||||
|
||||
$("#alert").dialog({resizable: false, title: "Close Image Converter",
|
||||
buttons: {
|
||||
Cancel: function() {
|
||||
|
|
|
|||
105
modules/ui/ice-editor.js
Normal file
105
modules/ui/ice-editor.js
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
"use strict";
|
||||
function editIce() {
|
||||
if (customization) return;
|
||||
closeDialogs(".stable");
|
||||
if (!layerIsOn("toggleIce")) toggleIce();
|
||||
|
||||
elSelected = d3.select(d3.event.target);
|
||||
const type = elSelected.attr("type") ? "Glacier" : "Iceberg";
|
||||
document.getElementById("iceRandomize").style.display = type === "Glacier" ? "none" : "inline-block";
|
||||
document.getElementById("iceSize").style.display = type === "Glacier" ? "none" : "inline-block";
|
||||
if (type === "Iceberg") document.getElementById("iceSize").value = +elSelected.attr("size");
|
||||
ice.selectAll("*").classed("draggable", true).call(d3.drag().on("drag", dragElement));
|
||||
|
||||
$("#iceEditor").dialog({
|
||||
title: "Edit "+type, resizable: false,
|
||||
position: {my: "center top+60", at: "top", of: d3.event, collision: "fit"},
|
||||
close: closeEditor
|
||||
});
|
||||
|
||||
if (modules.editIce) return;
|
||||
modules.editIce = true;
|
||||
|
||||
// add listeners
|
||||
document.getElementById("iceEditStyle").addEventListener("click", () => editStyle("ice"));
|
||||
document.getElementById("iceRandomize").addEventListener("click", randomizeShape);
|
||||
document.getElementById("iceSize").addEventListener("input", changeSize);
|
||||
document.getElementById("iceNew").addEventListener("click", toggleAdd);
|
||||
document.getElementById("iceRemove").addEventListener("click", removeIce);
|
||||
|
||||
function randomizeShape() {
|
||||
const c = grid.points[+elSelected.attr("cell")];
|
||||
const s = +elSelected.attr("size");
|
||||
const i = ra(grid.cells.i), cn = grid.points[i];
|
||||
const poly = getGridPolygon(i).map(p => [p[0]-cn[0], p[1]-cn[1]]);
|
||||
const points = poly.map(p => [rn(c[0] + p[0] * s, 2), rn(c[1] + p[1] * s, 2)]);
|
||||
elSelected.attr("points", points);
|
||||
}
|
||||
|
||||
function changeSize() {
|
||||
const c = grid.points[+elSelected.attr("cell")];
|
||||
const s = +elSelected.attr("size");
|
||||
const flat = elSelected.attr("points").split(",").map(el => +el);
|
||||
const pairs = [];
|
||||
while (flat.length) pairs.push(flat.splice(0,2));
|
||||
const poly = pairs.map(p => [(p[0]-c[0]) / s, (p[1]-c[1]) / s]);
|
||||
const size = +this.value;
|
||||
const points = poly.map(p => [rn(c[0] + p[0] * size, 2), rn(c[1] + p[1] * size, 2)]);
|
||||
elSelected.attr("points", points).attr("size", size);
|
||||
}
|
||||
|
||||
function toggleAdd() {
|
||||
document.getElementById("iceNew").classList.toggle("pressed");
|
||||
if (document.getElementById("iceNew").classList.contains("pressed")) {
|
||||
viewbox.style("cursor", "crosshair").on("click", addIcebergOnClick);
|
||||
tip("Click on map to create an iceberg. Hold Shift to add multiple", true);
|
||||
} else {
|
||||
clearMainTip();
|
||||
viewbox.on("click", clicked).style("cursor", "default");
|
||||
}
|
||||
}
|
||||
|
||||
function addIcebergOnClick() {
|
||||
const point = d3.mouse(this);
|
||||
const i = findGridCell(point[0], point[1]);
|
||||
const c = grid.points[i];
|
||||
const s = +document.getElementById("iceSize").value;
|
||||
|
||||
const points = getGridPolygon(i).map(p => [(p[0] + (c[0]-p[0]) / s)|0, (p[1] + (c[1]-p[1]) / s)|0]);
|
||||
const iceberg = ice.append("polygon").attr("points", points).attr("cell", i).attr("size", s);
|
||||
iceberg.call(d3.drag().on("drag", dragElement));
|
||||
if (d3.event.shiftKey === false) toggleAdd();
|
||||
}
|
||||
|
||||
function removeIce() {
|
||||
const type = elSelected.attr("type") ? "Glacier" : "Iceberg";
|
||||
alertMessage.innerHTML = `Are you sure you want to remove the ${type}?`;
|
||||
$("#alert").dialog({resizable: false, title: "Remove "+type,
|
||||
buttons: {
|
||||
Remove: function() {
|
||||
$(this).dialog("close");
|
||||
elSelected.remove();
|
||||
$("#iceEditor").dialog("close");
|
||||
},
|
||||
Cancel: function() {$(this).dialog("close");}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function dragElement() {
|
||||
const tr = parseTransform(this.getAttribute("transform"));
|
||||
const dx = +tr[0] - d3.event.x, dy = +tr[1] - d3.event.y;
|
||||
|
||||
d3.event.on("drag", function() {
|
||||
const x = d3.event.x, y = d3.event.y;
|
||||
this.setAttribute("transform", `translate(${(dx+x)},${(dy+y)})`);
|
||||
});
|
||||
}
|
||||
|
||||
function closeEditor() {
|
||||
ice.selectAll("*").classed("draggable", false).call(d3.drag().on("drag", null));
|
||||
clearMainTip();
|
||||
iceNew.classList.remove("pressed");
|
||||
unselect();
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ function restoreLayers() {
|
|||
if (layerIsOn("toggleCultures")) drawCultures();
|
||||
if (layerIsOn("toggleProvinces")) drawProvinces();
|
||||
if (layerIsOn("toggleReligions")) drawReligions();
|
||||
if (layerIsOn("toggleIce")) drawIce();
|
||||
|
||||
// states are getting rendered each time, if it's not required than layers should be hidden
|
||||
if (!layerIsOn("toggleBorders")) $('#borders').fadeOut();
|
||||
|
|
@ -28,14 +29,14 @@ restoreCustomPresets(); // run on-load
|
|||
|
||||
function getDefaultPresets() {
|
||||
return {
|
||||
"political": ["toggleBorders", "toggleIcons", "toggleLabels", "toggleRivers", "toggleRoutes", "toggleScaleBar", "toggleStates"],
|
||||
"political": ["toggleBorders", "toggleIcons", "toggleIce", "toggleLabels", "toggleRivers", "toggleRoutes", "toggleScaleBar", "toggleStates"],
|
||||
"cultural": ["toggleBorders", "toggleCultures", "toggleIcons", "toggleLabels", "toggleRivers", "toggleRoutes", "toggleScaleBar"],
|
||||
"religions": ["toggleBorders", "toggleIcons", "toggleLabels", "toggleReligions", "toggleRivers", "toggleRoutes", "toggleScaleBar"],
|
||||
"provinces": ["toggleBorders", "toggleIcons", "toggleProvinces", "toggleRivers", "toggleScaleBar"],
|
||||
"biomes": ["toggleBiomes", "toggleRivers", "toggleScaleBar"],
|
||||
"biomes": ["toggleBiomes", "toggleIce", "toggleRivers", "toggleScaleBar"],
|
||||
"heightmap": ["toggleHeight", "toggleRivers"],
|
||||
"physical": ["toggleCoordinates", "toggleHeight", "toggleRivers", "toggleScaleBar"],
|
||||
"poi": ["toggleBorders", "toggleHeight", "toggleIcons", "toggleMarkers", "toggleRivers", "toggleRoutes", "toggleScaleBar"],
|
||||
"physical": ["toggleCoordinates", "toggleHeight", "toggleIce", "toggleRivers", "toggleScaleBar"],
|
||||
"poi": ["toggleBorders", "toggleHeight", "toggleIce", "toggleIcons", "toggleMarkers", "toggleRivers", "toggleRoutes", "toggleScaleBar"],
|
||||
"military": ["toggleBorders", "toggleIcons", "toggleLabels", "toggleMilitary", "toggleRivers", "toggleRoutes", "toggleScaleBar", "toggleStates"],
|
||||
"landmass": ["toggleScaleBar"]
|
||||
}
|
||||
|
|
@ -260,7 +261,7 @@ function drawTemp() {
|
|||
temperature.append("path").attr("d", `M0,0 h${svgWidth} v${svgHeight} h${-svgWidth} Z`).attr("fill", scheme(1 - (min - tMin) / delta)).attr("stroke", "none");
|
||||
|
||||
for (const t of isolines) {
|
||||
const path = chains.filter(c => c[0] === t).map(c => round(lineGen(c[1]))).join();
|
||||
const path = chains.filter(c => c[0] === t).map(c => round(lineGen(c[1]))).join("");
|
||||
if (!path) continue;
|
||||
const fill = scheme(1 - (t - tMin) / delta), stroke = d3.color(fill).darker(.2);
|
||||
temperature.append("path").attr("d", path).attr("fill", fill).attr("stroke", stroke);
|
||||
|
|
@ -345,7 +346,7 @@ function drawBiomes() {
|
|||
const edgeVerticle = cells.v[i].find(v => vertices.c[v].some(i => cells.biome[i] !== b));
|
||||
const chain = connectVertices(edgeVerticle, b);
|
||||
if (chain.length < 3) continue;
|
||||
const points = chain.map(v => vertices.p[v]);
|
||||
const points = clipPoly(chain.map(v => vertices.p[v]), 1);
|
||||
paths[b] += "M" + points.join("L") + "Z";
|
||||
}
|
||||
|
||||
|
|
@ -456,38 +457,78 @@ function drawCells() {
|
|||
cells.append("path").attr("d", path);
|
||||
}
|
||||
|
||||
function drawSeaIce() {
|
||||
const seaIce = viewbox.append("g").attr("id", "seaIce").attr("fill", "#e8f0f6").attr("stroke", "#e8f0f6").attr("filter", "url(#dropShadow05)");//.attr("opacity", .8);
|
||||
for (const i of grid.cells.i) {
|
||||
const t = grid.cells.temp[i] ;
|
||||
if (t > 2) continue;
|
||||
if (t > -5 && grid.cells.h[i] >= 20) continue;
|
||||
if (t < -5) drawpolygon(i);
|
||||
if (P(normalize(t, -5.5, 2.5))) continue; // t[-5; 2]
|
||||
const size = t < -14 ? 0 : t > -6 ? (7 + t) / 10 : (15 + t) / 100; // [0; 1], where 0 = full size, 1 = zero size
|
||||
resizePolygon(i, rn(size * (.2 + rand() * .9), 2));
|
||||
function toggleIce() {
|
||||
if (!layerIsOn("toggleIce")) {
|
||||
turnButtonOn("toggleIce");
|
||||
$('#ice').fadeIn();
|
||||
if (!ice.selectAll("*").size()) drawIce();
|
||||
if (event && isCtrlClick(event)) editStyle("ice");
|
||||
} else {
|
||||
if (event && isCtrlClick(event)) {editStyle("ice"); return;}
|
||||
$('#ice').fadeOut();
|
||||
turnButtonOff("toggleIce");
|
||||
}
|
||||
}
|
||||
|
||||
// -9: .06
|
||||
// -8: .07
|
||||
// -7: .08
|
||||
// -6: .09
|
||||
function drawIce() {
|
||||
const cells = grid.cells, vertices = grid.vertices, n = cells.i.length, temp = cells.temp, h = cells.h;
|
||||
const used = new Uint8Array(cells.i.length);
|
||||
Math.seedrandom(seed);
|
||||
|
||||
// -5: .2
|
||||
// -4: .3
|
||||
// -3: .4
|
||||
// -2: .5
|
||||
// -1: .6
|
||||
// 0: .7
|
||||
const shieldMin = -6; // max temp to form ice shield (glacier)
|
||||
const icebergMax = 2; // max temp to form an iceberg
|
||||
|
||||
function drawpolygon(i) {
|
||||
seaIce.append("polygon").attr("points", getGridPolygon(i));
|
||||
for (const i of grid.cells.i) {
|
||||
const t = temp[i];
|
||||
if (t > icebergMax) continue; // too warm: no ice
|
||||
if (t > shieldMin && h[i] >= 20) continue; // non-glacier land: no ice
|
||||
|
||||
if (t <= shieldMin) {
|
||||
// very cold: ice shield
|
||||
if (used[i]) continue; // already rendered
|
||||
const onborder = cells.c[i].some(n => temp[n] > shieldMin);
|
||||
if (!onborder) continue; // need to start from onborder cell
|
||||
const vertex = cells.v[i].find(v => vertices.c[v].some(i => temp[i] > shieldMin));
|
||||
const chain = connectVertices(vertex);
|
||||
if (chain.length < 3) continue;
|
||||
const points = clipPoly(chain.map(v => vertices.p[v]));
|
||||
ice.append("polygon").attr("points", points).attr("type", "iceShield");
|
||||
continue;
|
||||
}
|
||||
|
||||
// mildly cold: iceberd
|
||||
if (P(normalize(t, -7, 2.5))) continue; // t[-5; 2] cold: skip some cells
|
||||
if (grid.features[cells.f[i]].type === "lake") continue; // lake: no icebers
|
||||
let size = (6.5 + t) / 10; // iceberg size: 0 = full size, 1 = zero size
|
||||
if (cells.t[i] === -1) size *= 1.3; // coasline: smaller icebers
|
||||
size = Math.min(size * (.4 + rand() * 1.2), .95); // randomize iceberd size
|
||||
resizePolygon(i, size);
|
||||
}
|
||||
|
||||
function resizePolygon(i, s) {
|
||||
const c = grid.points[i];
|
||||
const points = getGridPolygon(i).map(p => [p[0] + (c[0]-p[0]) * s, p[1] + (c[1]-p[1]) * s]);
|
||||
seaIce.append("polygon").attr("points", points);
|
||||
const points = getGridPolygon(i).map(p => [(p[0] + (c[0]-p[0]) * s)|0, (p[1] + (c[1]-p[1]) * s)|0]);
|
||||
ice.append("polygon").attr("points", points).attr("cell", i).attr("size", rn(1-s, 2));
|
||||
}
|
||||
|
||||
// connect vertices to chain
|
||||
function connectVertices(start) {
|
||||
const chain = []; // vertices chain to form a path
|
||||
for (let i=0, current = start; i === 0 || current !== start && i < 20000; i++) {
|
||||
const prev = last(chain); // previous vertex in chain
|
||||
chain.push(current); // add current vertex to sequence
|
||||
const c = vertices.c[current]; // cells adjacent to vertex
|
||||
c.filter(c => temp[c] <= shieldMin).forEach(c => used[c] = 1);
|
||||
const c0 = c[0] >= n || temp[c[0]] > shieldMin;
|
||||
const c1 = c[1] >= n || temp[c[1]] > shieldMin;
|
||||
const c2 = c[2] >= n || temp[c[2]] > shieldMin;
|
||||
const v = vertices.v[current]; // neighboring vertices
|
||||
if (v[0] !== prev && c0 !== c1) current = v[0];
|
||||
else if (v[1] !== prev && c1 !== c2) current = v[1];
|
||||
else if (v[2] !== prev && c0 !== c2) current = v[2];
|
||||
if (current === chain[chain.length - 1]) {console.error("Next vertex is not found"); break;}
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1222,6 +1263,7 @@ function getLayer(id) {
|
|||
if (id === "toggleTemp") return $("#temperature");
|
||||
if (id === "togglePrec") return $("#prec");
|
||||
if (id === "togglePopulation") return $("#population");
|
||||
if (id === "toggleIce") return $("#ice");
|
||||
if (id === "toggleTexture") return $("#texture");
|
||||
if (id === "toggleLabels") return $("#labels");
|
||||
if (id === "toggleIcons") return $("#icons");
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ function editMarker() {
|
|||
document.getElementById("markerIconSize").addEventListener("input", changeIconSize);
|
||||
document.getElementById("markerIconShiftX").addEventListener("input", changeIconShiftX);
|
||||
document.getElementById("markerIconShiftY").addEventListener("input", changeIconShiftY);
|
||||
document.getElementById("markerIconCustom").addEventListener("input", applyCustomUnicodeIcon);
|
||||
document.getElementById("markerIconSelect").addEventListener("click", selectMarkerIcon);
|
||||
|
||||
document.getElementById("markerStyle").addEventListener("click", toggleStyleSection);
|
||||
document.getElementById("markerSize").addEventListener("input", changeMarkerSize);
|
||||
|
|
@ -73,13 +73,7 @@ function editMarker() {
|
|||
markerIconFill.value = icon.attr("fill");
|
||||
|
||||
markerToggleBubble.className = symbol.select("circle").attr("opacity") === "0" ? "icon-info" : "icon-info-circled";
|
||||
|
||||
const table = document.getElementById("markerIconTable");
|
||||
let selected = table.getElementsByClassName("selected");
|
||||
if (selected.length) selected[0].removeAttribute("class");
|
||||
selected = document.querySelectorAll("#markerIcon" + icon.text().codePointAt());
|
||||
if (selected.length) selected[0].className = "selected";
|
||||
markerIconCustom.value = selected.length ? "" : icon.text();
|
||||
markerIconSelect.innerHTML = icon.text();
|
||||
}
|
||||
|
||||
function toggleGroupSection() {
|
||||
|
|
@ -141,14 +135,18 @@ function editMarker() {
|
|||
const id = elSelected.attr("data-id");
|
||||
const used = document.querySelectorAll("use[data-id='"+id+"']");
|
||||
const count = used.length === 1 ? "1 element" : used.length + " elements";
|
||||
alertMessage.innerHTML = "Are you sure you want to remove the marker (" + count + ")?";
|
||||
alertMessage.innerHTML = "Are you sure you want to remove all markers of that type (" + count + ")?";
|
||||
|
||||
$("#alert").dialog({resizable: false, title: "Remove marker",
|
||||
$("#alert").dialog({resizable: false, title: "Remove marker type",
|
||||
buttons: {
|
||||
Remove: function() {
|
||||
$(this).dialog("close");
|
||||
if (id !== "#marker0") d3.select("#defs-markers").select(id).remove();
|
||||
used.forEach(e => e.remove());
|
||||
used.forEach(e => {
|
||||
const index = notes.findIndex(n => n.id === e.id);
|
||||
if (index != -1) notes.splice(index, 1);
|
||||
e.remove();
|
||||
});
|
||||
updateGroupOptions();
|
||||
updateGroupOptions();
|
||||
$("#markerEditor").dialog("close");
|
||||
|
|
@ -162,242 +160,20 @@ function editMarker() {
|
|||
if (markerIconSection.style.display === "inline-block") {
|
||||
markerEditor.querySelectorAll("button:not(#markerIcon)").forEach(b => b.style.display = "inline-block");
|
||||
markerIconSection.style.display = "none";
|
||||
markerIconSelect.style.display = "none";
|
||||
} else {
|
||||
markerEditor.querySelectorAll("button:not(#markerIcon)").forEach(b => b.style.display = "none");
|
||||
markerIconSection.style.display = "inline-block";
|
||||
if (!markerIconTable.innerHTML) drawIconsList();
|
||||
markerIconSelect.style.display = "inline-block";
|
||||
}
|
||||
}
|
||||
|
||||
function drawIconsList() {
|
||||
const icons = [
|
||||
// emoticons in FF:
|
||||
["2693", "⚓", "Anchor"],
|
||||
["26EA", "⛪", "Church"],
|
||||
["1F3EF", "🏯", "Japanese Castle"],
|
||||
["1F3F0", "🏰", "Castle"],
|
||||
["1F5FC", "🗼", "Tower"],
|
||||
["1F3E0", "🏠", "House"],
|
||||
["1F3AA", "🎪", "Tent"],
|
||||
["1F3E8", "🏨", "Hotel"],
|
||||
["1F4B0", "💰", "Money bag"],
|
||||
["1F6A8", "🚨", "Revolving Light"],
|
||||
["1F309", "🌉", "Bridge at Night"],
|
||||
["1F5FB", "🗻", "Mountain"],
|
||||
["1F30B", "🌋", "Volcano"],
|
||||
["270A", "✊", "Raised Fist"],
|
||||
["1F44A", "👊", "Oncoming Fist"],
|
||||
["1F4AA", "💪", "Flexed Biceps"],
|
||||
["1F47C", "👼", "Baby Angel"],
|
||||
["1F40E", "🐎", "Horse"],
|
||||
["1F434", "🐴", "Horse Face"],
|
||||
["1F42E", "🐮", "Cow"],
|
||||
["1F43A", "🐺", "Wolf Face"],
|
||||
["1F435", "🐵", "Monkey face"],
|
||||
["1F437", "🐷", "Pig face"],
|
||||
["1F414", "🐔", "Chicken"],
|
||||
["1F411", "🐑", "Ewe"],
|
||||
["1F42B", "🐫", "Camel"],
|
||||
["1F418", "🐘", "Elephant"],
|
||||
["1F422", "🐢", "Turtle"],
|
||||
["1F40C", "🐌", "Snail"],
|
||||
["1F40D", "🐍", "Snake"],
|
||||
["1F41D", "🐝", "Honeybee"],
|
||||
["1F41C", "🐜", "Ant"],
|
||||
["1F41B", "🐛", "Bug"],
|
||||
["1F426", "🐦", "Bird"],
|
||||
["1F438", "🐸", "Frog Face"],
|
||||
["1F433", "🐳", "Whale"],
|
||||
["1F42C", "🐬", "Dolphin"],
|
||||
["1F420", "🐟", "Fish"],
|
||||
["1F480", "💀", "Skull"],
|
||||
["1F432", "🐲", "Dragon Head"],
|
||||
["1F479", "👹", "Ogre"],
|
||||
["1F47A", "👺", "Goblin"],
|
||||
["1F47B", "👻", "Ghost"],
|
||||
["1F47E", "👾", "Alien"],
|
||||
["1F383", "🎃", "Jack-O-Lantern"],
|
||||
["1F384", "🎄", "Christmas Tree"],
|
||||
["1F334", "🌴", "Palm"],
|
||||
["1F335", "🌵", "Cactus"],
|
||||
["2618", "☘️", "Shamrock"],
|
||||
["1F340", "🍀", "Four Leaf Clover"],
|
||||
["1F341", "🍁", "Maple Leaf"],
|
||||
["1F33F", "🌿", "Herb"],
|
||||
["1F33E", "🌾", "Sheaf"],
|
||||
["1F344", "🍄", "Mushroom"],
|
||||
["1F374", "🍴", "Fork and knife"],
|
||||
["1F372", "🍲", "Food"],
|
||||
["1F35E", "🍞", "Bread"],
|
||||
["1F357", "🍗", "Poultry leg"],
|
||||
["1F347", "🍇", "Grapes"],
|
||||
["1F34F", "🍏", "Apple"],
|
||||
["1F352", "🍒", "Cherries"],
|
||||
["1F36F", "🍯", "Honey pot"],
|
||||
["1F37A", "🍺", "Beer"],
|
||||
["1F37B", "🍻", "Beers"],
|
||||
["1F377", "🍷", "Wine glass"],
|
||||
["1F3BB", "🎻", "Violin"],
|
||||
["1F3B8", "🎸", "Guitar"],
|
||||
["1F52A", "🔪", "Knife"],
|
||||
["1F52B", "🔫", "Pistol"],
|
||||
["1F4A3", "💣", "Bomb"],
|
||||
["1F4A5", "💥", "Collision"],
|
||||
["1F4A8", "💨", "Dashing away"],
|
||||
["1F301", "🌁", "Foggy"],
|
||||
["2744", "❄️", "Snowflake"],
|
||||
["26A1", "⚡", "Electricity"],
|
||||
["1F320", "🌠", "Shooting star"],
|
||||
["1F319", "🌙", "Crescent moon"],
|
||||
["1F525", "🔥", "Fire"],
|
||||
["1F4A7", "💧", "Droplet"],
|
||||
["1F30A", "🌊", "Wave"],
|
||||
["23F0", "⏰", "Alarm Clock"],
|
||||
["231B", "⌛", "Hourglass"],
|
||||
["1F3C6", "🏆", "Goblet"],
|
||||
["26F2", "⛲", "Fountain"],
|
||||
["26F5", "⛵", "Sailboat"],
|
||||
["26FA", "⛺", "Campfire"],
|
||||
["2764", "❤", "Red Heart"],
|
||||
["1F498", "💘", "Heart With Arrow"],
|
||||
["1F489", "💉", "Syringe"],
|
||||
["1F4D5", "📕", "Closed Book"],
|
||||
["1F4D6", "📚", "Books"],
|
||||
["1F381", "🎁", "Wrapped Gift"],
|
||||
["1F3AF", "🎯", "Archery"],
|
||||
["1F52E", "🔮", "Magic ball"],
|
||||
["1F3AD", "🎭", "Performing arts"],
|
||||
["1F3A8", "🎨", "Artist palette"],
|
||||
["1F457", "👗", "Dress"],
|
||||
["1F392", "🎒", "Backpack"],
|
||||
["1F451", "👑", "Crown"],
|
||||
["1F48D", "💍", "Ring"],
|
||||
["1F48E", "💎", "Gem"],
|
||||
["1F514", "🔔", "Bell"],
|
||||
["1F3B2", "🎲", "Die"],
|
||||
// black and white icons in FF:
|
||||
["26A0", "⚠", "Alert"],
|
||||
["2317", "⌗", "Hash"],
|
||||
["2318", "⌘", "POI"],
|
||||
["2307", "⌇", "Wavy"],
|
||||
["27F1", "⟱", "Downwards Quadruple"],
|
||||
["21E6", "⇦", "Left arrow"],
|
||||
["21E7", "⇧", "Top arrow"],
|
||||
["21E8", "⇨", "Right arrow"],
|
||||
["21E9", "⇩", "Left arrow"],
|
||||
["21F6", "⇶", "Three arrows"],
|
||||
["2699", "⚙", "Gear"],
|
||||
["269B", "⚛", "Atom"],
|
||||
["2680", "⚀", "Die1"],
|
||||
["2681", "⚁", "Die2"],
|
||||
["2682", "⚂", "Die3"],
|
||||
["2683", "⚃", "Die4"],
|
||||
["2684", "⚄", "Die5"],
|
||||
["2685", "⚅", "Die6"],
|
||||
["26B4", "⚴", "Pallas"],
|
||||
["26B5", "⚵", "Juno"],
|
||||
["26B6", "⚶", "Vesta"],
|
||||
["26B7", "⚷", "Chiron"],
|
||||
["26B8", "⚸", "Lilith"],
|
||||
["263F", "☿", "Mercury"],
|
||||
["2640", "♀", "Venus"],
|
||||
["2641", "♁", "Earth"],
|
||||
["2642", "♂", "Mars"],
|
||||
["2643", "♃", "Jupiter"],
|
||||
["2644", "♄", "Saturn"],
|
||||
["2645", "♅", "Uranus"],
|
||||
["2646", "♆", "Neptune"],
|
||||
["2647", "♇", "Pluto"],
|
||||
["26B3", "⚳", "Ceres"],
|
||||
["2654", "♔", "Chess king"],
|
||||
["2655", "♕", "Chess queen"],
|
||||
["2656", "♖", "Chess rook"],
|
||||
["2657", "♗", "Chess bishop"],
|
||||
["2658", "♘", "Chess knight"],
|
||||
["2659", "♙", "Chess pawn"],
|
||||
["2660", "♠", "Spade"],
|
||||
["2663", "♣", "Club"],
|
||||
["2665", "♥", "Heart"],
|
||||
["2666", "♦", "Diamond"],
|
||||
["2698", "⚘", "Flower"],
|
||||
["2625", "☥", "Ankh"],
|
||||
["2626", "☦", "Orthodox"],
|
||||
["2627", "☧", "Chi Rho"],
|
||||
["2628", "☨", "Lorraine"],
|
||||
["2629", "☩", "Jerusalem"],
|
||||
["2670", "♰", "Syriac cross"],
|
||||
["2020", "†", "Dagger"],
|
||||
["262A", "☪", "Muslim"],
|
||||
["262D", "☭", "Soviet"],
|
||||
["262E", "☮", "Peace"],
|
||||
["262F", "☯", "Yin yang"],
|
||||
["26A4", "⚤", "Heterosexuality"],
|
||||
["26A2", "⚢", "Female homosexuality"],
|
||||
["26A3", "⚣", "Male homosexuality"],
|
||||
["26A5", "⚥", "Male and female"],
|
||||
["26AD", "⚭", "Rings"],
|
||||
["2690", "⚐", "White flag"],
|
||||
["2691", "⚑", "Black flag"],
|
||||
["263C", "☼", "Sun"],
|
||||
["263E", "☾", "Moon"],
|
||||
["2668", "♨", "Hot springs"],
|
||||
["2600", "☀", "Black sun"],
|
||||
["2601", "☁", "Cloud"],
|
||||
["2602", "☂", "Umbrella"],
|
||||
["2603", "☃", "Snowman"],
|
||||
["2604", "☄", "Comet"],
|
||||
["2605", "★", "Black star"],
|
||||
["2606", "☆", "White star"],
|
||||
["269D", "⚝", "Outlined star"],
|
||||
["2618", "☘", "Shamrock"],
|
||||
["21AF", "↯", "Lightning"],
|
||||
["269C", "⚜", "FleurDeLis"],
|
||||
["2622", "☢", "Radiation"],
|
||||
["2623", "☣", "Biohazard"],
|
||||
["2620", "☠", "Skull"],
|
||||
["2638", "☸", "Dharma"],
|
||||
["2624", "☤", "Caduceus"],
|
||||
["2695", "⚕", "Aeculapius staff"],
|
||||
["269A", "⚚", "Hermes staff"],
|
||||
["2697", "⚗", "Alembic"],
|
||||
["266B", "♫", "Music"],
|
||||
["2702", "✂", "Scissors"],
|
||||
["2696", "⚖", "Scales"],
|
||||
["2692", "⚒", "Hammer and pick"],
|
||||
["2694", "⚔", "Swords"]
|
||||
];
|
||||
|
||||
const table = document.getElementById("markerIconTable");
|
||||
table.addEventListener("click", selectIcon, false);
|
||||
table.addEventListener("mouseover", hoverIcon, false);
|
||||
let row = "";
|
||||
|
||||
for (let i=0; i < icons.length; i++) {
|
||||
if (i%16 === 0) row = table.insertRow(0);
|
||||
const cell = row.insertCell(0);
|
||||
const icon = String.fromCodePoint(parseInt(icons[i][0], 16));
|
||||
cell.innerHTML = icon;
|
||||
cell.id = "markerIcon" + icon.codePointAt();
|
||||
cell.dataset.desc = icons[i][2];
|
||||
}
|
||||
}
|
||||
|
||||
function selectIcon(e) {
|
||||
if (e.target !== e.currentTarget) {
|
||||
const table = document.getElementById("markerIconTable");
|
||||
const selected = table.getElementsByClassName("selected");
|
||||
if (selected.length) selected[0].removeAttribute("class");
|
||||
e.target.className = "selected";
|
||||
function selectMarkerIcon() {
|
||||
selectIcon(this.innerHTML, v => {
|
||||
this.innerHTML = v;
|
||||
const id = elSelected.attr("data-id");
|
||||
const icon = e.target.innerHTML;
|
||||
d3.select("#defs-markers").select(id).select("text").text(icon);
|
||||
}
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
function hoverIcon(e) {
|
||||
if (e.target !== e.currentTarget) tip(e.target.innerHTML + " " + e.target.dataset.desc);
|
||||
e.stopPropagation();
|
||||
d3.select("#defs-markers").select(id).select("text").text(v);
|
||||
});
|
||||
}
|
||||
|
||||
function changeIconSize() {
|
||||
|
|
@ -415,12 +191,6 @@ function editMarker() {
|
|||
d3.select("#defs-markers").select(id).select("text").attr("y", this.value + "%");
|
||||
}
|
||||
|
||||
function applyCustomUnicodeIcon() {
|
||||
if (!this.value) return;
|
||||
const id = elSelected.attr("data-id");
|
||||
d3.select("#defs-markers").select(id).select("text").text(this.value);
|
||||
}
|
||||
|
||||
function toggleStyleSection() {
|
||||
if (markerStyleSection.style.display === "inline-block") {
|
||||
markerEditor.querySelectorAll("button:not(#markerStyle)").forEach(b => b.style.display = "inline-block");
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ function overviewMilitary() {
|
|||
document.getElementById("militaryRegimentsList").addEventListener("click", () => overviewRegiments(-1));
|
||||
document.getElementById("militaryOverviewRecalculate").addEventListener("click", militaryRecalculate);
|
||||
document.getElementById("militaryExport").addEventListener("click", downloadMilitaryData);
|
||||
document.getElementById("militaryWiki").addEventListener("click", () => wiki("Military-Forces"));
|
||||
|
||||
body.addEventListener("change", function(ev) {
|
||||
const el = ev.target, line = el.parentNode, state = +line.dataset.id;
|
||||
|
|
@ -183,7 +184,7 @@ function overviewMilitary() {
|
|||
position: {my: "center", at: "center", of: "svg"},
|
||||
buttons: {
|
||||
Apply: applyMilitaryOptions,
|
||||
Add: () => addUnitLine({name: "custom"+militaryOptionsTable.rows.length, rural: .2, urban: .5, crew: 1, type: "melee"}),
|
||||
Add: () => addUnitLine({icon: "🛡️", name: "custom"+militaryOptionsTable.rows.length, rural: .2, urban: .5, crew: 1, power: 1, type: "melee"}),
|
||||
Restore: restoreDefaultUnits,
|
||||
Cancel: function() {$(this).dialog("close");}
|
||||
}, open: function() {
|
||||
|
|
@ -200,19 +201,20 @@ function overviewMilitary() {
|
|||
}
|
||||
|
||||
function addUnitLine(u) {
|
||||
const row = `<tr>
|
||||
const row = document.createElement("tr");
|
||||
row.innerHTML = `<td><button type="button" data-tip="Click to select unit icon">${u.icon||" "}</button></td>
|
||||
<td><input data-tip="Type unit name. If name is changed for existing unit, old unit will be replaced" value="${u.name}"></td>
|
||||
<td><input data-tip="Enter conscription percentage for rural population" type="number" min=0 max=100 step=.01 value="${u.rural}"></td>
|
||||
<td><input data-tip="Enter conscription percentage for urban population" type="number" min=0 max=100 step=.01 value="${u.urban}"></td>
|
||||
<td><input data-tip="Enter average number of people in crew" type="number" min=1 step=1 value="${u.crew}"></td>
|
||||
<td><input data-tip="Enter average number of people in crew (used for total personnel calculation)" type="number" min=1 step=1 value="${u.crew}"></td>
|
||||
<td><input data-tip="Enter military power (used for battle simulation)" type="number" min=0 step=.1 value="${u.power}"></td>
|
||||
<td><select data-tip="Select unit type to apply special rules on forces recalculation">${types.map(t => `<option ${u.type === t ? "selected" : ""} value="${t}">${t}</option>`).join(" ")}</select></td>
|
||||
<td data-tip="Check if unit is separate and can be stacked only with units of the same type">
|
||||
<input id="${u.name}Separate" type="checkbox" class="checkbox" ${u.separate ? "checked" : ""}>
|
||||
<label for="${u.name}Separate" class="checkbox-label"></label>
|
||||
</td>
|
||||
<td data-tip="Remove the unit"><span data-tip="Remove unit type" class="icon-trash-empty pointer" onclick="this.parentElement.parentElement.remove();"></span></td>
|
||||
</tr>`;
|
||||
table.insertAdjacentHTML("beforeend", row);
|
||||
<label for="${u.name}Separate" class="checkbox-label"></label></td>
|
||||
<td data-tip="Remove the unit"><span data-tip="Remove unit type" class="icon-trash-empty pointer" onclick="this.parentElement.parentElement.remove();"></span></td>`;
|
||||
row.querySelector("button").addEventListener("click", function(e) {selectIcon(this.innerHTML, v => this.innerHTML = v)});
|
||||
table.appendChild(row);
|
||||
}
|
||||
|
||||
function restoreDefaultUnits() {
|
||||
|
|
@ -230,8 +232,14 @@ function overviewMilitary() {
|
|||
|
||||
$("#militaryOptions").dialog("close");
|
||||
options.military = unitLines.map((r, i) => {
|
||||
const [name, rural, urban, crew, type, separate] = Array.from(r.querySelectorAll("input, select")).map(d => d.type === "checkbox" ? d.checked : d.value);
|
||||
return {name:names[i], rural:+rural||0, urban:+urban||0, crew:+crew||0, type, separate:+separate||0};
|
||||
const [icon, name, rural, urban, crew, power, type, separate] = Array.from(r.querySelectorAll("input, select, button")).map(d => {
|
||||
let value = d.value;
|
||||
if (d.type === "number") value = +d.value || 0;
|
||||
if (d.type === "checkbox") value = +d.checked || 0;
|
||||
if (d.type === "button") value = d.innerHTML || "⠀";
|
||||
return value;
|
||||
});
|
||||
return {icon, name:names[i], rural, urban, crew, power, type, separate};
|
||||
});
|
||||
localStorage.setItem("military", JSON.stringify(options.military));
|
||||
Military.generate();
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ function editNamesbase() {
|
|||
document.getElementById("namesbaseMin").addEventListener("input", updateBaseMin);
|
||||
document.getElementById("namesbaseMax").addEventListener("input", updateBaseMax);
|
||||
document.getElementById("namesbaseDouble").addEventListener("input", updateBaseDublication);
|
||||
document.getElementById("namesbaseMulti").addEventListener("input", updateBaseMiltiwordRate);
|
||||
document.getElementById("namesbaseAdd").addEventListener("click", namesbaseAdd);
|
||||
document.getElementById("namesbaseAnalize").addEventListener("click", analizeNamesbase);
|
||||
document.getElementById("namesbaseDefault").addEventListener("click", namesbaseRestoreDefault);
|
||||
|
|
@ -46,7 +45,6 @@ function editNamesbase() {
|
|||
document.getElementById("namesbaseMin").value = nameBases[base].min;
|
||||
document.getElementById("namesbaseMax").value = nameBases[base].max;
|
||||
document.getElementById("namesbaseDouble").value = nameBases[base].d;
|
||||
document.getElementById("namesbaseMulti").value = nameBases[base].m;
|
||||
updateExamples();
|
||||
}
|
||||
|
||||
|
|
@ -67,10 +65,9 @@ function editNamesbase() {
|
|||
|
||||
function updateNamesData() {
|
||||
const base = +document.getElementById("namesbaseSelect").value;
|
||||
const b = document.getElementById("namesbaseTextarea").value.replace(/ /g, "");
|
||||
const b = document.getElementById("namesbaseTextarea").value;
|
||||
if (b.split(",").length < 3) {
|
||||
tip("The names data provided is not correct", false, "error");
|
||||
document.getElementById("namesbaseTextarea").value = nameBases[base].b;
|
||||
tip("The names data provided is too short of incorrect", false, "error");
|
||||
return;
|
||||
}
|
||||
nameBases[base].b = b;
|
||||
|
|
@ -101,12 +98,6 @@ function editNamesbase() {
|
|||
nameBases[base].d = this.value;
|
||||
}
|
||||
|
||||
function updateBaseMiltiwordRate() {
|
||||
if (isNaN(+this.value) || +this.value < 0 || +this.value > 1) {tip("Please provide a number within [0-1] range", false, "error"); return;}
|
||||
const base = +document.getElementById("namesbaseSelect").value;
|
||||
nameBases[base].m = +this.value;
|
||||
}
|
||||
|
||||
function analizeNamesbase() {
|
||||
const string = document.getElementById("namesbaseTextarea").value;
|
||||
if (!string) {tip("Names data field should not be empty", false, "error"); return;}
|
||||
|
|
@ -174,7 +165,6 @@ function editNamesbase() {
|
|||
document.getElementById("namesbaseMin").value = 5;
|
||||
document.getElementById("namesbaseMax").value = 12;
|
||||
document.getElementById("namesbaseDouble").value = "";
|
||||
document.getElementById("namesbaseMulti").value = 0;
|
||||
document.getElementById("namesbaseExamples").innerHTML = "Please provide names data";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ function editNotes(id, name) {
|
|||
document.getElementById("notesDownload").addEventListener("click", downloadLegends);
|
||||
document.getElementById("notesUpload").addEventListener("click", () => legendsToLoad.click());
|
||||
document.getElementById("legendsToLoad").addEventListener("change", function() {uploadFile(this, uploadLegends)});
|
||||
document.getElementById("notesRemove").addEventListener("click", triggernotesRemove);
|
||||
document.getElementById("notesRemove").addEventListener("click", triggerNotesRemove);
|
||||
|
||||
function changeObject() {
|
||||
const note = notes.find(note => note.id === this.value);
|
||||
|
|
@ -96,7 +96,7 @@ function editNotes(id, name) {
|
|||
editNotes(notes[0].id, notes[0].name);
|
||||
}
|
||||
|
||||
function triggernotesRemove() {
|
||||
function triggerNotesRemove() {
|
||||
alertMessage.innerHTML = "Are you sure you want to remove the selected note?";
|
||||
$("#alert").dialog({resizable: false, title: "Remove note",
|
||||
buttons: {
|
||||
|
|
|
|||
|
|
@ -69,18 +69,11 @@ document.getElementById("options").querySelector("div.tab").addEventListener("cl
|
|||
if (id === "aboutTab") aboutContent.style.display = "block";
|
||||
});
|
||||
|
||||
document.getElementById("options").querySelectorAll("i.collapsible").forEach(el => el.addEventListener("click", collapse));
|
||||
function collapse(e) {
|
||||
const trigger = e.target;
|
||||
const section = trigger.parentElement.nextElementSibling;
|
||||
|
||||
if (section.style.display === "none") {
|
||||
section.style.display = "block";
|
||||
trigger.classList.replace("icon-down-open", "icon-up-open");
|
||||
} else {
|
||||
section.style.display = "none";
|
||||
trigger.classList.replace("icon-up-open", "icon-down-open");
|
||||
}
|
||||
// show popup with a list of Patreon supportes (updated manually, to be replaced with API call)
|
||||
function showSupporters() {
|
||||
const supporters = "Aaron Meyer, Ahmad Amerih, AstralJacks, aymeric, Billy Dean Goehring, Branndon Edwards, Chase Mayers, Curt Flood, cyninge, Dino Princip, E.M. White, es, Fondue, Fritjof Olsson, Gatsu, Johan Fröberg, Jonathan Moore, Joseph Miranda, Kate, KC138, Luke Nelson, Markus Finster, Massimo Vella, Mikey, Nathan Mitchell, Paavi1, Pat, Ryan Westcott, Sasquatch, Shawn Spencer, Sizz_TV, Timothée CALLET, UTG community, Vlad Tomash, Wil Sisney, William Merriott, Xariun, Gun Metal Games, Scott Marner, Spencer Sherman, Valerii Matskevych, Alloyed Clavicle, Stewart Walsh, Ruthlyn Mollett (Javan), Benjamin Mair-Pratt, Diagonath, Alexander Thomas, Ashley Wilson-Savoury, William Henry, Preston Brooks, JOSHUA QUALTIERI, Hilton Williams, Katharina Haase, Hisham Bedri, Ian arless, Karnat, Bird, Kevin, Jessica Thomas, Steve Hyatt, Logicspren, Alfred García, Jonathan Killstring, John Ackley, Invad3r233, Norbert Žigmund, Jennifer, PoliticsBuff, _gfx_, Maggie, Connor McMartin, Jared McDaris, BlastWind, Franc Casanova Ferrer, Dead & Devil, Michael Carmody, Valerie Elise, naikibens220, Jordon Phillips, William Pucs, The Dungeon Masters, Brady R Rathbun, J, Shadow, Matthew Tiffany, Huw Williams, Joseph Hamilton, FlippantFeline, Tamashi Toh, kms, Stephen Herron, MidnightMoon, Whakomatic x, Barished, Aaron bateson, Brice Moss, Diklyquill, PatronUser, Michael Greiner, Steven Bennett, Jacob Harrington, Miguel C., Reya C., Giant Monster Games, Noirbard, Brian Drennen, Ben Craigie, Alex Smolin, Endwords, Joshua E Goodwin, SirTobit , Allen S. Rout, Allen Bull Bear, Pippa Mitchell, R K, G0atfather, Ryan Lege, Caner Oleas Pekgönenç, Bradley Edwards, Tertiary , Austin Miller, Jesse Holmes, Jan Dvořák, Marten F, Erin D. Smale, Maxwell Hill, Drunken_Legends, rob bee, Jesse Holmes, YYako, Detocroix";
|
||||
alertMessage.innerHTML = "<ul style='column-count: 3; column-gap: 2em'>" + supporters.split(", ").sort().map(n => `<li>${n}</li>`).join("") + "</ul>";
|
||||
$("#alert").dialog({resizable: false, title: "Patreon Supporters", width: "30vw", position: {my: "center", at: "center", of: "svg"}});
|
||||
}
|
||||
|
||||
// Option listeners
|
||||
|
|
@ -124,7 +117,9 @@ optionsContent.addEventListener("click", function(event) {
|
|||
else if (id === "optionsSeedGenerate") generateMapWithSeed();
|
||||
else if (id === "optionsMapHistory") showSeedHistoryDialog();
|
||||
else if (id === "optionsCopySeed") copyMapURL();
|
||||
else if (id === "optionsEraRegenerate") regenerateEra();
|
||||
else if (id === "zoomExtentDefault") restoreDefaultZoomExtent();
|
||||
else if (id === "translateExtent") toggleTranslateExtent(event.target);
|
||||
});
|
||||
|
||||
function mapSizeInputChange() {
|
||||
|
|
@ -145,7 +140,6 @@ function changeMapSize() {
|
|||
landmass.select("rect").attr("x", 0).attr("y", 0).attr("width", maxWidth).attr("height", maxHeight);
|
||||
oceanPattern.select("rect").attr("x", 0).attr("y", 0).attr("width", maxWidth).attr("height", maxHeight);
|
||||
oceanLayers.select("rect").attr("x", 0).attr("y", 0).attr("width", maxWidth).attr("height", maxHeight);
|
||||
//defs.select("#mapClip > rect").attr("width", maxWidth).attr("height", maxHeight);
|
||||
|
||||
fitScaleBar();
|
||||
if (window.fitLegendBox) fitLegendBox();
|
||||
|
|
@ -159,10 +153,7 @@ function applyMapSize() {
|
|||
svgWidth = Math.min(graphWidth, window.innerWidth);
|
||||
svgHeight = Math.min(graphHeight, window.innerHeight);
|
||||
svg.attr("width", svgWidth).attr("height", svgHeight);
|
||||
zoom.translateExtent([[0, 0],[graphWidth, graphHeight]]).scaleExtent([zoomMin, zoomMax]).scaleTo(svg, zoomMin);
|
||||
//viewbox.attr("transform", null).attr("clip-path", "url(#mapClip)");
|
||||
//defs.append("clipPath").attr("id", "mapClip").append("rect").attr("x", 0).attr("y", 0).attr("width", graphWidth).attr("height", graphHeight);
|
||||
//zoom.translateExtent([[-svgWidth*.2, -graphHeight*.2], [svgWidth*1.2, graphHeight*1.2]]);
|
||||
zoom.translateExtent([[0, 0], [graphWidth, graphHeight]]).scaleExtent([zoomMin, zoomMax]).scaleTo(svg, zoomMin);
|
||||
}
|
||||
|
||||
function toggleFullscreen() {
|
||||
|
|
@ -178,6 +169,12 @@ function toggleFullscreen() {
|
|||
changeMapSize();
|
||||
}
|
||||
|
||||
function toggleTranslateExtent(el) {
|
||||
const on = el.dataset.on = +!(+el.dataset.on);
|
||||
if (on) zoom.translateExtent([[-graphWidth/2, -graphHeight/2], [graphWidth*1.5, graphHeight*1.5]]);
|
||||
else zoom.translateExtent([[0, 0], [graphWidth, graphHeight]]);
|
||||
}
|
||||
|
||||
function generateMapWithSeed() {
|
||||
if (optionsSeed.value == seed) {
|
||||
tip("The current map already has this seed", false, "error");
|
||||
|
|
@ -358,6 +355,9 @@ function randomizeOptions() {
|
|||
if (!stored("distanceUnit")) distanceUnitInput.value = US || UK ? "mi" : "km";
|
||||
if (!stored("heightUnit")) heightUnit.value = US || UK ? "ft" : "m";
|
||||
if (!stored("temperatureScale")) temperatureScale.value = US ? "°F" : "°C";
|
||||
|
||||
// World settings
|
||||
generateEra();
|
||||
}
|
||||
|
||||
// select heightmap template pseudo-randomly
|
||||
|
|
@ -393,6 +393,21 @@ function randomizeCultureSet() {
|
|||
changeCultureSet();
|
||||
}
|
||||
|
||||
// generate current year and era name
|
||||
function generateEra() {
|
||||
if (!stored("year")) yearInput.value = rand(100, 2000); // current year
|
||||
if (!stored("era")) eraInput.value = Names.getBaseShort(P(.7) ? 1 : rand(nameBases.length)) + " Era";
|
||||
options.year = yearInput.value;
|
||||
options.era = eraInput.value;
|
||||
options.eraShort = options.era.split(" ").map(w => w[0].toUpperCase()).join(""); // short name for era
|
||||
}
|
||||
|
||||
function regenerateEra() {
|
||||
unlock("era");
|
||||
options.era = eraInput.value = Names.getBaseShort(P(.7) ? 1 : rand(nameBases.length)) + " Era";
|
||||
options.eraShort = options.era.split(" ").map(w => w[0].toUpperCase()).join("");
|
||||
}
|
||||
|
||||
// remove all saved data from LocalStorage and reload the page
|
||||
function restoreDefaultOptions() {
|
||||
localStorage.clear();
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ function editProvinces() {
|
|||
document.getElementById("provincesManuallyApply").addEventListener("click", applyProvincesManualAssignent);
|
||||
document.getElementById("provincesManuallyCancel").addEventListener("click", () => exitProvincesManualAssignment());
|
||||
document.getElementById("provincesAdd").addEventListener("click", enterAddProvinceMode);
|
||||
document.getElementById("provincesRecolor").addEventListener("click", recolorProvinces);
|
||||
|
||||
body.addEventListener("click", function(ev) {
|
||||
if (customization) return;
|
||||
|
|
@ -41,7 +42,7 @@ function editProvinces() {
|
|||
if (cl.contains("icon-star-empty")) capitalZoomIn(p); else
|
||||
if (cl.contains("icon-flag-empty")) triggerIndependencePromps(p); else
|
||||
if (cl.contains("culturePopulation")) changePopulation(p); else
|
||||
if (cl.contains("icon-pin")) focusOn(p, cl); else
|
||||
if (cl.contains("icon-pin")) toggleFog(p, cl); else
|
||||
if (cl.contains("icon-trash-empty")) removeProvince(p);
|
||||
});
|
||||
|
||||
|
|
@ -113,7 +114,7 @@ function editProvinces() {
|
|||
const capital = p.burg ? pack.burgs[p.burg].name : '';
|
||||
const separable = p.burg && p.burg !== pack.states[p.state].capital;
|
||||
const focused = defs.select("#fog #focusProvince"+p.i).size();
|
||||
lines += `<div class="states" data-id=${p.i} data-name=${p.name} data-form=${p.formName} data-color="${p.color}" data-capital="${capital}" data-state="${stateName}" data-area=${area} data-population=${population}>
|
||||
lines += `<div class="states" data-id=${p.i} data-name="${p.name}" data-form="${p.formName}" data-color="${p.color}" data-capital="${capital}" data-state="${stateName}" data-area=${area} data-population=${population}>
|
||||
<svg data-tip="Province fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${p.color}" class="fillRect pointer"></svg>
|
||||
<input data-tip="Province name. Click to change" class="name pointer" value="${p.name}" readonly>
|
||||
<span data-tip="Click to open province COA in the Iron Arachne Heraldry Generator. Ctrl + click to change the seed" class="icon-coa pointer hide"></span>
|
||||
|
|
@ -281,7 +282,7 @@ function editProvinces() {
|
|||
BurgsAndStates.drawStateLabels([newState, oldState]);
|
||||
|
||||
// remove old province
|
||||
unfocus(p);
|
||||
unfog("focusProvince"+p);
|
||||
if (states[oldState].provinces.includes(p)) states[oldState].provinces.splice(states[oldState].provinces.indexOf(p), 1);
|
||||
provinces[p].removed = true;
|
||||
|
||||
|
|
@ -346,24 +347,10 @@ function editProvinces() {
|
|||
|
||||
}
|
||||
|
||||
function focusOn(p, cl) {
|
||||
const inactive = cl.contains("inactive");
|
||||
function toggleFog(p, cl) {
|
||||
const path = provs.select("#province"+p).attr("d"), id = "focusProvince"+p;
|
||||
cl.contains("inactive") ? fog(id, path) : unfog(id);
|
||||
cl.toggle("inactive");
|
||||
|
||||
if (inactive) {
|
||||
if (defs.select("#fog #focusProvince"+p).size()) return;
|
||||
fogging.style("display", "block");
|
||||
const path = provs.select("#province"+p).attr("d");
|
||||
defs.select("#fog").append("path").attr("d", path).attr("fill", "black").attr("id", "focusProvince"+p);
|
||||
fogging.append("path").attr("d", path).attr("id", "focusProvinceHalo"+p)
|
||||
.attr("fill", "none").attr("stroke", pack.provinces[p].color).attr("filter", "url(#blur5)");
|
||||
} else unfocus(p);
|
||||
}
|
||||
|
||||
function unfocus(p) {
|
||||
defs.select("#focusProvince"+p).remove();
|
||||
fogging.select("#focusProvinceHalo"+p).remove();
|
||||
if (!defs.selectAll("#fog path").size()) fogging.style("display", "none"); // all items are de-focused
|
||||
}
|
||||
|
||||
function removeProvince(p) {
|
||||
|
|
@ -376,7 +363,7 @@ function editProvinces() {
|
|||
const state = pack.provinces[p].state;
|
||||
if (pack.states[state].provinces.includes(p)) pack.states[state].provinces.splice(pack.states[state].provinces.indexOf(p), 1);
|
||||
pack.provinces[p].removed = true;
|
||||
unfocus(p);
|
||||
unfog("focusProvince"+p);
|
||||
|
||||
const g = provs.select("#provincesBody");
|
||||
g.select("#province"+p).remove();
|
||||
|
|
@ -808,6 +795,20 @@ function editProvinces() {
|
|||
if (provincesAdd.classList.contains("pressed")) provincesAdd.classList.remove("pressed");
|
||||
}
|
||||
|
||||
function recolorProvinces() {
|
||||
const state = +document.getElementById("provincesFilterState").value;
|
||||
|
||||
pack.provinces.forEach(p => {
|
||||
if (!p || p.removed) return;
|
||||
if (state !== -1 && p.state !== state) return;
|
||||
const stateColor = pack.states[p.state].color;
|
||||
const rndColor = getRandomColor();
|
||||
p.color = stateColor[0] === "#" ? d3.color(d3.interpolate(stateColor, rndColor)(.2)).hex() : rndColor;
|
||||
});
|
||||
|
||||
if (!layerIsOn("toggleProvinces")) toggleProvinces(); else drawProvinces();
|
||||
}
|
||||
|
||||
function downloadProvincesData() {
|
||||
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
|
||||
let data = "Id,Province,Form,State,Color,Capital,Area "+unit+",Total Population,Rural Population,Urban Population\n"; // headers
|
||||
|
|
@ -838,7 +839,7 @@ function editProvinces() {
|
|||
$(this).dialog("close");
|
||||
pack.provinces.filter(p => p.i).forEach(p => {
|
||||
p.removed = true;
|
||||
unfocus(p.i);
|
||||
unfog("focusProvince"+p.i);
|
||||
});
|
||||
pack.cells.i.forEach(i => pack.cells.province[i] = 0);
|
||||
pack.states.filter(s => s.i && !s.removed).forEach(s => s.provinces = []);
|
||||
|
|
|
|||
|
|
@ -14,8 +14,7 @@ function editRegiment(selector) {
|
|||
|
||||
$("#regimentEditor").dialog({
|
||||
title: "Edit Regiment", resizable: false, close: closeEditor,
|
||||
position: {my: "left top", at: "left+10 top+10", of: "#map"},
|
||||
close: closeEditor
|
||||
position: {my: "left top", at: "left+10 top+10", of: "#map"}
|
||||
});
|
||||
|
||||
if (modules.editRegiment) return;
|
||||
|
|
@ -27,6 +26,7 @@ function editRegiment(selector) {
|
|||
document.getElementById("regimentName").addEventListener("change", changeName);
|
||||
document.getElementById("regimentEmblem").addEventListener("input", changeEmblem);
|
||||
document.getElementById("regimentEmblemSelect").addEventListener("click", selectEmblem);
|
||||
document.getElementById("regimentAttack").addEventListener("click", toggleAttack);
|
||||
document.getElementById("regimentRegenerateLegend").addEventListener("click", regenerateLegend);
|
||||
document.getElementById("regimentLegend").addEventListener("click", editLegend);
|
||||
document.getElementById("regimentSplit").addEventListener("click", splitRegiment);
|
||||
|
|
@ -92,50 +92,15 @@ function editRegiment(selector) {
|
|||
elSelected.dataset.name = reg.name = document.getElementById("regimentName").value = name;
|
||||
}
|
||||
|
||||
function selectEmblem() {
|
||||
selectIcon(regimentEmblem.value, v => {regimentEmblem.value = v; changeEmblem()});
|
||||
}
|
||||
|
||||
function changeEmblem() {
|
||||
const emblem = document.getElementById("regimentEmblem").value;
|
||||
regiment().icon = elSelected.querySelector(".regimentIcon").innerHTML = emblem;
|
||||
}
|
||||
|
||||
function selectEmblem() {
|
||||
const emblems = ["⚔️","🏹","🐴","💣","🌊","🎯","⚓","🔮","📯","🛡️","👑",
|
||||
"☠️","🎆","🗡️","⛏️","🔥","🐾","🎪","🏰","⚜️","⛓️","❤️","📜","🔱","🌈","🌠","💥","☀️","🍀",
|
||||
"🔰","🕸️","⚗️","☣️","☢️","🎖️","⚕️","☸️","✡️","🚩","🏳️","🏴","🌈","💪","✊","👊","🤜","🤝","🙏","🧙","💂","🤴","🧛","🧟","🧞","🧝",
|
||||
"🦄","🐲","🐉","🐎","🦓","🐺","🦊","🐱","🐈","🦁","🐯","🐅","🐆","🐕","🦌","🐵","🐒","🦍",
|
||||
"🦅","🕊️","🐓","🦇","🐦","🦉","🐮","🐄","🐂","🐃","🐷","🐖","🐗","🐏","🐑","🐐","🐫","🦒","🐘","🦏",
|
||||
"🐭","🐁","🐀","🐹","🐰","🐇","🦔","🐸","🐊","🐢","🦎","🐍","🐳","🐬","🦈","🐙","🦑","🐌","🦋","🐜","🐝","🐞","🦗","🕷️","🦂","🦀"];
|
||||
|
||||
alertMessage.innerHTML = "";
|
||||
const container = document.createElement("div");
|
||||
container.style.userSelect = "none";
|
||||
container.style.cursor = "pointer";
|
||||
container.style.fontSize = "2em";
|
||||
container.style.width = "47vw";
|
||||
container.innerHTML = emblems.map(i => `<span>${i}</span>`).join("");
|
||||
container.addEventListener("mouseover", e => showTip(e), false);
|
||||
container.addEventListener("click", e => clickEmblem(e), false);
|
||||
alertMessage.appendChild(container);
|
||||
|
||||
$("#alert").dialog({
|
||||
resizable: false, width: fitContent(), title: "Select emblem",
|
||||
buttons: {
|
||||
Emojipedia: function() {openURL("https://emojipedia.org/");},
|
||||
Close: function() {$(this).dialog("close");}
|
||||
}
|
||||
});
|
||||
|
||||
function showTip(e) {
|
||||
if (e.target.tagName !== "SPAN") return;
|
||||
tip(`Click to select ${e.target.innerHTML} emblem`);
|
||||
}
|
||||
|
||||
function clickEmblem(e) {
|
||||
if (e.target.tagName !== "SPAN") return;
|
||||
document.getElementById("regimentEmblem").value = e.target.innerHTML;
|
||||
changeEmblem();
|
||||
}
|
||||
}
|
||||
|
||||
function changeUnit() {
|
||||
const u = this.dataset.u;
|
||||
const reg = regiment();
|
||||
|
|
@ -148,7 +113,7 @@ function editRegiment(selector) {
|
|||
|
||||
function splitRegiment() {
|
||||
const reg = regiment(), u1 = reg.u;
|
||||
const state = elSelected.dataset.state, military = pack.states[state].military;
|
||||
const state = +elSelected.dataset.state, military = pack.states[state].military;
|
||||
const i = last(military).i + 1, u2 = Object.assign({}, u1); // u clone
|
||||
|
||||
Object.keys(u2).forEach(u => u2[u] = Math.floor(u2[u]/2)); // halved new reg
|
||||
|
|
@ -164,7 +129,7 @@ function editRegiment(selector) {
|
|||
// create new regiment
|
||||
const shift = +armies.attr("box-size") * 2;
|
||||
const y = function(x, y) {do {y+=shift} while (military.find(r => r.x === x && r.y === y)); return y;}
|
||||
const newReg = {a, cell:reg.cell, i, n:reg.n, u:u2, x:reg.x, y:y(reg.x, reg.y), bx:reg.bx, by:reg.by, icon: reg.icon};
|
||||
const newReg = {a, cell:reg.cell, i, n:reg.n, u:u2, x:reg.x, y:y(reg.x, reg.y), bx:reg.bx, by:reg.by, state, icon: reg.icon};
|
||||
newReg.name = Military.getName(newReg, military);
|
||||
military.push(newReg);
|
||||
Military.generateNote(newReg, pack.states[state]); // add legend
|
||||
|
|
@ -188,10 +153,10 @@ function editRegiment(selector) {
|
|||
const point = d3.mouse(this);
|
||||
const cell = findCell(point[0], point[1]);
|
||||
const x = pack.cells.p[cell][0], y = pack.cells.p[cell][1];
|
||||
const state = elSelected.dataset.state, military = pack.states[state].military;
|
||||
const state = +elSelected.dataset.state, military = pack.states[state].military;
|
||||
const i = military.length ? last(military).i + 1 : 0;
|
||||
const n = +(pack.cells.h[cell] < 20); // naval or land
|
||||
const reg = {a:0, cell, i, n, u:{}, x, y, bx:x, by:y, icon:"🛡️"};
|
||||
const reg = {a:0, cell, i, n, u:{}, x, y, bx:x, by:y, state, icon:"🛡️"};
|
||||
reg.name = Military.getName(reg, military);
|
||||
military.push(reg);
|
||||
Military.generateNote(reg, pack.states[state]); // add legend
|
||||
|
|
@ -200,6 +165,49 @@ function editRegiment(selector) {
|
|||
toggleAdd();
|
||||
}
|
||||
|
||||
function toggleAttack() {
|
||||
document.getElementById("regimentAttack").classList.toggle("pressed");
|
||||
if (document.getElementById("regimentAttack").classList.contains("pressed")) {
|
||||
viewbox.style("cursor", "crosshair").on("click", attackRegimentOnClick);
|
||||
tip("Click on another regiment to initiate battle", true);
|
||||
armies.selectAll(":scope > g").classed("draggable", false);
|
||||
} else {
|
||||
clearMainTip();
|
||||
armies.selectAll(":scope > g").classed("draggable", true);
|
||||
viewbox.on("click", clicked).style("cursor", "default");
|
||||
}
|
||||
}
|
||||
|
||||
function attackRegimentOnClick() {
|
||||
const target = d3.event.target, regSelected = target.parentElement, army = regSelected.parentElement;
|
||||
const oldState = +elSelected.dataset.state, newState = +regSelected.dataset.state;
|
||||
|
||||
if (army.parentElement.id !== "armies") {tip("Please click on a regiment to attack", false, "error"); return;}
|
||||
if (regSelected === elSelected) {tip("Regiment cannot attack itself", false, "error"); return;}
|
||||
if (oldState === newState) {tip("Cannot attack fraternal regiment", false, "error"); return;}
|
||||
|
||||
const attacker = regiment();
|
||||
const defender = pack.states[regSelected.dataset.state].military.find(r => r.i == regSelected.dataset.id);
|
||||
if (!attacker.a || !defender.a) {tip("Regiment has no troops to battle", false, "error"); return;}
|
||||
|
||||
// save initial position to temp attribute
|
||||
attacker.px = attacker.x, attacker.py = attacker.y;
|
||||
defender.px = defender.x, defender.py = defender.y;
|
||||
|
||||
// move attacker to defender
|
||||
Military.moveRegiment(attacker, defender.x, defender.y-8);
|
||||
|
||||
// draw battle icon
|
||||
const attack = d3.transition().delay(300).duration(700).ease(d3.easeSinInOut).on("end", () => new Battle(attacker, defender));
|
||||
svg.append("text").attr("x", window.innerWidth/2).attr("y", window.innerHeight/2)
|
||||
.text("⚔️").attr("font-size", 0).attr("opacity", 1)
|
||||
.style("dominant-baseline", "central").style("text-anchor", "middle")
|
||||
.transition(attack).attr("font-size", 1000).attr("opacity", .2).remove();
|
||||
|
||||
clearMainTip();
|
||||
$("#regimentEditor").dialog("close");
|
||||
}
|
||||
|
||||
function toggleAttach() {
|
||||
document.getElementById("regimentAttach").classList.toggle("pressed");
|
||||
if (document.getElementById("regimentAttach").classList.contains("pressed")) {
|
||||
|
|
@ -289,7 +297,7 @@ function editRegiment(selector) {
|
|||
const x1 = x => rn(x - w / 2, 2);
|
||||
const y1 = y => rn(y - size, 2);
|
||||
|
||||
const baseRect = this.querySelectorAll("rect")[0];
|
||||
const baseRect = this.querySelector("rect");
|
||||
const text = this.querySelector("text");
|
||||
const iconRect = this.querySelectorAll("rect")[1];
|
||||
const icon = this.querySelector(".regimentIcon");
|
||||
|
|
@ -330,6 +338,7 @@ function editRegiment(selector) {
|
|||
armies.selectAll("g>g").call(d3.drag().on("drag", null));
|
||||
viewbox.selectAll("g#regimentBase").remove();
|
||||
document.getElementById("regimentAdd").classList.remove("pressed");
|
||||
document.getElementById("regimentAttack").classList.remove("pressed");
|
||||
document.getElementById("regimentAttach").classList.remove("pressed");
|
||||
restoreDefaultEvents();
|
||||
elSelected = null;
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ function overviewRegiments(state) {
|
|||
const military = pack.states[state].military;
|
||||
const i = military.length ? last(military).i + 1 : 0;
|
||||
const n = +(pack.cells.h[cell] < 20); // naval or land
|
||||
const reg = {a:0, cell, i, n, u:{}, x, y, bx:x, by:y, icon:"🛡️"};
|
||||
const reg = {a:0, cell, i, n, u:{}, x, y, bx:x, by:y, state, icon:"🛡️"};
|
||||
reg.name = Military.getName(reg, military);
|
||||
military.push(reg);
|
||||
Military.generateNote(reg, pack.states[state]); // add legend
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ function editRiver(id) {
|
|||
document.getElementById("riverWidthHide").addEventListener("click", hideRiverWidth);
|
||||
document.getElementById("riverWidthInput").addEventListener("input", changeWidth);
|
||||
document.getElementById("riverIncrement").addEventListener("input", changeIncrement);
|
||||
document.getElementById("riverElevationProfile").addEventListener("click", showElevationProfile);
|
||||
|
||||
document.getElementById("riverEditStyle").addEventListener("click", () => editStyle("rivers"));
|
||||
document.getElementById("riverNew").addEventListener("click", toggleRiverCreationMode);
|
||||
|
|
@ -54,7 +55,7 @@ function editRiver(id) {
|
|||
|
||||
function drawControlPoints(node) {
|
||||
const l = node.getTotalLength() / 2;
|
||||
const segments = Math.ceil(l / 8);
|
||||
const segments = Math.ceil(l / 4);
|
||||
const increment = rn(l / segments * 1e5);
|
||||
for (let i=increment*segments, c=i; i >= 0; i -= increment, c += increment) {
|
||||
const p1 = node.getPointAtLength(i / 1e5);
|
||||
|
|
@ -94,6 +95,10 @@ function editRiver(id) {
|
|||
const [d, length] = Rivers.getPath(points, +riverWidthInput.value, +riverIncrement.value);
|
||||
elSelected.attr("d", d);
|
||||
updateRiverLength(length);
|
||||
|
||||
if (modules.elevation) {
|
||||
showEPForRiver(elSelected.node());
|
||||
}
|
||||
}
|
||||
|
||||
function updateRiverLength(l = elSelected.node().getTotalLength() / 2) {
|
||||
|
|
@ -176,6 +181,11 @@ function editRiver(id) {
|
|||
if (r) r.name = riverName.value = Names.getBase(rand(nameBases.length-1));
|
||||
}
|
||||
|
||||
function showElevationProfile() {
|
||||
modules.elevation = true;
|
||||
showEPForRiver(elSelected.node());
|
||||
}
|
||||
|
||||
function showRiverWidth() {
|
||||
document.querySelectorAll("#riverEditor > button").forEach(el => el.style.display = "none");
|
||||
document.getElementById("riverWidthSection").style.display = "inline-block";
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ function editRoute(onClick) {
|
|||
document.getElementById("routeGroupName").addEventListener("change", createNewGroup);
|
||||
document.getElementById("routeGroupRemove").addEventListener("click", removeRouteGroup);
|
||||
document.getElementById("routeGroupsHide").addEventListener("click", hideGroupSection);
|
||||
document.getElementById("routeElevationProfile").addEventListener("click", showElevationProfile);
|
||||
|
||||
document.getElementById("routeEditStyle").addEventListener("click", editGroupStyle);
|
||||
document.getElementById("routeSplit").addEventListener("click", toggleRouteSplitMode);
|
||||
|
|
@ -46,7 +47,7 @@ function editRoute(onClick) {
|
|||
|
||||
function drawControlPoints(node) {
|
||||
const l = node.getTotalLength();
|
||||
const increment = l / Math.ceil(l / 8);
|
||||
const increment = l / Math.ceil(l / 4);
|
||||
for (let i=0; i <= l; i += increment) {addControlPoint(node.getPointAtLength(i));}
|
||||
routeLength.innerHTML = rn(l * distanceScaleInput.value) + " " + distanceUnitInput.value;
|
||||
}
|
||||
|
|
@ -100,7 +101,14 @@ function editRoute(onClick) {
|
|||
|
||||
elSelected.attr("d", round(lineGen(points)));
|
||||
const l = elSelected.node().getTotalLength();
|
||||
routeLength.innerHTML = rn(l * distanceScaleInput.value) + " " + distanceUnitInput.value;
|
||||
routeLength.innerHTML = rn(l * distanceScaleInput.value) + " " + distanceUnitInput.value;
|
||||
|
||||
if (modules.elevation) showEPForRoute(elSelected.node());
|
||||
}
|
||||
|
||||
function showElevationProfile() {
|
||||
modules.elevation = true;
|
||||
showEPForRoute(elSelected.node());
|
||||
}
|
||||
|
||||
function showGroupSection() {
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ function editStates() {
|
|||
if (cl.contains("icon-coa")) stateOpenCOA(ev, state); else
|
||||
if (cl.contains("icon-star-empty")) stateCapitalZoomIn(state); else
|
||||
if (cl.contains("culturePopulation")) changePopulation(state); else
|
||||
if (cl.contains("icon-pin")) focusOnState(state, cl); else
|
||||
if (cl.contains("icon-pin")) toggleFog(state, cl); else
|
||||
if (cl.contains("icon-trash-empty")) stateRemovePrompt(state);
|
||||
});
|
||||
|
||||
|
|
@ -171,6 +171,8 @@ function editStates() {
|
|||
|
||||
function stateHighlightOn(event) {
|
||||
if (!layerIsOn("toggleStates")) return;
|
||||
if (defs.select("#fog path").size()) return;
|
||||
|
||||
const state = +event.target.dataset.id;
|
||||
if (customization || !state) return;
|
||||
const d = regions.select("#state"+state).attr("d");
|
||||
|
|
@ -184,7 +186,7 @@ function editStates() {
|
|||
path.transition().duration(dur).attrTween("stroke-dasharray", function() {return t => i(t)});
|
||||
}
|
||||
|
||||
function stateHighlightOff(event) {
|
||||
function stateHighlightOff() {
|
||||
debug.selectAll(".highlight").each(function() {
|
||||
d3.select(this).transition().duration(1000).attr("opacity", 0).remove();
|
||||
});
|
||||
|
|
@ -201,14 +203,19 @@ function editStates() {
|
|||
statesBody.select("#state-gap"+state).attr("stroke", fill);
|
||||
const halo = d3.color(fill) ? d3.color(fill).darker().hex() : "#666666";
|
||||
statesHalo.select("#state-border"+state).attr("stroke", halo);
|
||||
|
||||
// recolor regiments
|
||||
const solidColor = fill[0] === "#" ? fill : "#999";
|
||||
const darkerColor = d3.color(solidColor).darker().hex();
|
||||
armies.select("#army"+state).attr("fill", solidColor);
|
||||
armies.select("#army"+state).selectAll("g > rect:nth-of-type(2)").attr("fill", darkerColor);
|
||||
}
|
||||
|
||||
openPicker(currentFill, callback);
|
||||
}
|
||||
|
||||
function editStateName(state) {
|
||||
|
||||
//Reset input value and close add mode
|
||||
// reset input value and close add mode
|
||||
stateNameEditorCustomForm.value = "";
|
||||
const addModeActive = stateNameEditorCustomForm.style.display === "inline-block";
|
||||
if (addModeActive) {
|
||||
|
|
@ -236,6 +243,7 @@ function editStates() {
|
|||
document.getElementById("stateNameEditorShortCulture").addEventListener("click", regenerateShortNameCuture);
|
||||
document.getElementById("stateNameEditorShortRandom").addEventListener("click", regenerateShortNameRandom);
|
||||
document.getElementById("stateNameEditorAddForm").addEventListener("click", addCustomForm);
|
||||
document.getElementById("stateNameEditorCustomForm").addEventListener("change", addCustomForm);
|
||||
document.getElementById("stateNameEditorFullRegenerate").addEventListener("click", regenerateFullName);
|
||||
|
||||
function regenerateShortNameCuture() {
|
||||
|
|
@ -256,7 +264,7 @@ function editStates() {
|
|||
const addModeActive = stateNameEditorCustomForm.style.display === "inline-block";
|
||||
stateNameEditorCustomForm.style.display = addModeActive ? "none" : "inline-block";
|
||||
stateNameEditorSelectForm.style.display = addModeActive ? "inline-block" : "none";
|
||||
if (addModeActive) applyOption(stateNameEditorSelectForm, value);
|
||||
if (value && addModeActive) applyOption(stateNameEditorSelectForm, value);
|
||||
stateNameEditorCustomForm.value = "";
|
||||
}
|
||||
|
||||
|
|
@ -399,26 +407,11 @@ function editStates() {
|
|||
recalculateStates();
|
||||
}
|
||||
|
||||
function focusOnState(state, cl) {
|
||||
function toggleFog(state, cl) {
|
||||
if (customization) return;
|
||||
|
||||
const inactive = cl.contains("inactive");
|
||||
const path = statesBody.select("#state"+state).attr("d"), id = "focusState"+state;
|
||||
cl.contains("inactive") ? fog(id, path) : unfog(id);
|
||||
cl.toggle("inactive");
|
||||
|
||||
if (inactive) {
|
||||
if (defs.select("#fog #focusState"+state).size()) return;
|
||||
fogging.style("display", "block");
|
||||
const path = statesBody.select("#state"+state).attr("d");
|
||||
defs.select("#fog").append("path").attr("d", path).attr("fill", "black").attr("id", "focusState"+state);
|
||||
fogging.append("path").attr("d", path).attr("id", "focusStateHalo"+state)
|
||||
.attr("fill", "none").attr("stroke", pack.states[state].color).attr("filter", "url(#blur5)");
|
||||
} else unfocus(state);
|
||||
}
|
||||
|
||||
function unfocus(s) {
|
||||
defs.select("#focusState"+s).remove();
|
||||
fogging.select("#focusStateHalo"+s).remove();
|
||||
if (!defs.selectAll("#fog path").size()) fogging.style("display", "none"); // all items are de-focused
|
||||
}
|
||||
|
||||
function stateRemovePrompt(state) {
|
||||
|
|
@ -440,7 +433,7 @@ function editStates() {
|
|||
statesBody.select("#state"+state).remove();
|
||||
statesBody.select("#state-gap"+state).remove();
|
||||
statesHalo.select("#state-border"+state).remove();
|
||||
unfocus(state);
|
||||
unfog("focusState"+state);
|
||||
const label = document.querySelector("#stateLabel"+state);
|
||||
if (label) label.remove();
|
||||
pack.burgs.forEach(b => {if(b.state === state) b.state = 0;});
|
||||
|
|
@ -453,11 +446,31 @@ function editStates() {
|
|||
pack.cells.province.forEach((pr, i) => {if(pr === p) pack.cells.province[i] = 0;});
|
||||
});
|
||||
|
||||
// remove military
|
||||
pack.states[state].military.forEach(m => {
|
||||
const id = `regiment${state}-${m.i}`;
|
||||
const index = notes.findIndex(n => n.id === id);
|
||||
if (index != -1) notes.splice(index, 1);
|
||||
});
|
||||
armies.select("g#army"+state).remove();
|
||||
|
||||
const military = pack.states[elSelected.dataset.state].military;
|
||||
const regIndex = military.indexOf(regiment());
|
||||
if (regIndex === -1) return;
|
||||
military.splice(regIndex, 1);
|
||||
|
||||
const index = notes.findIndex(n => n.id === elSelected.id);
|
||||
if (index != -1) notes.splice(index, 1);
|
||||
elSelected.remove();
|
||||
|
||||
const capital = pack.states[state].capital;
|
||||
pack.burgs[capital].capital = 0;
|
||||
pack.burgs[capital].state = 0;
|
||||
moveBurgToGroup(capital, "towns");
|
||||
|
||||
// clean state object
|
||||
pack.states[state].military = [];
|
||||
|
||||
debug.selectAll(".highlight").remove();
|
||||
if (!layerIsOn("toggleStates")) toggleStates(); else drawStates();
|
||||
if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders();
|
||||
|
|
@ -924,5 +937,6 @@ function editStates() {
|
|||
if (customization === 2) exitStatesManualAssignment("close");
|
||||
if (customization === 3) exitAddStateMode();
|
||||
debug.selectAll(".highlight").remove();
|
||||
body.innerHTML = "";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -65,6 +65,7 @@ function processFeatureRegeneration(event, button) {
|
|||
if (button === "regenerateProvinces") regenerateProvinces(); else
|
||||
if (button === "regenerateReligions") regenerateReligions(); else
|
||||
if (button === "regenerateMilitary") regenerateMilitary(); else
|
||||
if (button === "regenerateIce") regenerateIce(); else
|
||||
if (button === "regenerateMarkers") regenerateMarkers(event); else
|
||||
if (button === "regenerateZones") regenerateZones(event);
|
||||
}
|
||||
|
|
@ -86,7 +87,7 @@ function recalculatePopulation() {
|
|||
if (!b.i || b.removed) return;
|
||||
const i = b.cell;
|
||||
|
||||
b.population = rn(Math.max((pack.cells.s[i] + pack.cells.road[i]) / 8 + b.i / 1000 + i % 100 / 1000, .1), 3);
|
||||
b.population = rn(Math.max((pack.cells.s[i] + pack.cells.road[i] / 2) / 8 + b.i / 1000 + i % 100 / 1000, .1), 3);
|
||||
if (b.capital) b.population = b.population * 1.3; // increase capital population
|
||||
if (b.port) b.population = b.population * 1.3; // increase port population
|
||||
b.population = rn(b.population * gauss(2,3,.6,20,3), 3);
|
||||
|
|
@ -157,8 +158,8 @@ function regenerateStates() {
|
|||
tip(`Not enought burgs to generate ${regionsInput.value} states. Will generate only ${burgs.length} states`, false, "warn");
|
||||
}
|
||||
|
||||
// burg ids sorted by a bit randomized population:
|
||||
const sorted = burgs.map(b => [b.i, b.population * Math.random()]).sort((a, b) => b[1] - a[1]).map(b => b[0]);
|
||||
// burg local ids sorted by a bit randomized population:
|
||||
const sorted = burgs.map((b, i) => [i, b.population * Math.random()]).sort((a, b) => b[1] - a[1]).map(b => b[0]);
|
||||
const capitalsTree = d3.quadtree();
|
||||
|
||||
// turn all old capitals into towns
|
||||
|
|
@ -193,8 +194,8 @@ function regenerateStates() {
|
|||
if (!i) return {i, name: neutral};
|
||||
|
||||
let capital = null, x = 0, y = 0;
|
||||
for (let i=0; i < sorted.length; i++) {
|
||||
capital = burgs[sorted[i]];
|
||||
for (const i of sorted) {
|
||||
capital = burgs[i];
|
||||
x = capital.x, y = capital.y;
|
||||
if (capitalsTree.find(x, y, spacing) === undefined) break;
|
||||
spacing = Math.max(spacing - 1, 1);
|
||||
|
|
@ -249,6 +250,12 @@ function regenerateMilitary() {
|
|||
if (document.getElementById("militaryOverviewRefresh").offsetParent) militaryOverviewRefresh.click();
|
||||
}
|
||||
|
||||
function regenerateIce() {
|
||||
if (!layerIsOn("toggleIce")) toggleIce();
|
||||
ice.selectAll("*").remove();
|
||||
drawIce();
|
||||
}
|
||||
|
||||
function regenerateMarkers(event) {
|
||||
if (isCtrlClick(event)) prompt("Please provide markers number multiplier", {default:1, step:.01, min:0, max:100}, v => addNumberOfMarkers(v));
|
||||
else addNumberOfMarkers(gauss(1, .5, .3, 5, 2));
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ function editZones() {
|
|||
if (cl.contains("culturePopulation")) {changePopulation(zone); return;}
|
||||
if (cl.contains("icon-trash-empty")) {zoneRemove(zone); return;}
|
||||
if (cl.contains("icon-eye")) {toggleVisibility(el); return;}
|
||||
if (cl.contains("icon-pin")) {focusOnZone(zone, cl); return;}
|
||||
if (cl.contains("icon-pin")) {toggleFog(zone, cl); return;}
|
||||
if (cl.contains("fillRect")) {changeFill(el); return;}
|
||||
if (customization) selectZone(el);
|
||||
});
|
||||
|
|
@ -194,7 +194,7 @@ function editZones() {
|
|||
zones.selectAll("g").each(function() {
|
||||
if (this.dataset.cells) return;
|
||||
// all zone cells are removed
|
||||
unfocus(this.id);
|
||||
unfog("focusZone"+this.id);
|
||||
this.style.display = "block";
|
||||
});
|
||||
|
||||
|
|
@ -253,24 +253,13 @@ function editZones() {
|
|||
el.classList.toggle("inactive");
|
||||
}
|
||||
|
||||
function focusOnZone(zone, cl) {
|
||||
const inactive = cl.contains("inactive");
|
||||
function toggleFog(z, cl) {
|
||||
const dataCells = zones.select("#"+z).attr("data-cells");
|
||||
if (!dataCells) return;
|
||||
|
||||
const path = "M" + dataCells.split(",").map(c => getPackPolygon(+c)).join("M") + "Z", id = "focusZone"+z;
|
||||
cl.contains("inactive") ? fog(id, path) : unfog(id);
|
||||
cl.toggle("inactive");
|
||||
|
||||
if (inactive) {
|
||||
if (defs.select("#fog #focus"+zone).size()) return;
|
||||
const dataCells = zones.select("#"+zone).attr("data-cells");
|
||||
if (!dataCells) return;
|
||||
const data = dataCells.split(",").map(c => +c);
|
||||
const g = defs.select("#fog").append("g").attr("fill", "black").attr("stroke", "black").attr("id", "focus"+zone);
|
||||
g.selectAll("path").data(data).enter().append("path").attr("d", d => "M" + getPackPolygon(d) + "Z");
|
||||
fogging.style("display", "block");
|
||||
} else unfocus(zone);
|
||||
}
|
||||
|
||||
function unfocus(z) {
|
||||
defs.select("#focus"+z).remove();
|
||||
if (!defs.selectAll("#fog path").size()) fogging.style("display", "none"); // all states are de-focused
|
||||
}
|
||||
|
||||
function toggleLegend() {
|
||||
|
|
@ -414,7 +403,7 @@ function editZones() {
|
|||
|
||||
function zoneRemove(zone) {
|
||||
zones.select("#"+zone).remove();
|
||||
unfocus(zone);
|
||||
unfog("focusZone"+zone);
|
||||
zonesEditorAddLines();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@ 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);
|
||||
}
|
||||
|
||||
// get integer from float as floor + P(fractional)
|
||||
/** This is a description of the foo function. */
|
||||
function Pint(float) {
|
||||
return ~~float + +P(float % 1);
|
||||
}
|
||||
|
|
@ -370,14 +370,19 @@ function biased(min, max, ex) {
|
|||
}
|
||||
|
||||
// return array of values common for both array a and array b
|
||||
function intersect(a, b) {
|
||||
function common(a, b) {
|
||||
const setB = new Set(b);
|
||||
return [...new Set(a)].filter(a => setB.has(a));
|
||||
}
|
||||
|
||||
// check if char is vowel
|
||||
// clip polygon by graph bbox
|
||||
function clipPoly(points, secure = 0) {
|
||||
return polygonclip(points, [0, 0, graphWidth, graphHeight], secure);
|
||||
}
|
||||
|
||||
// check if char is vowel or can serve as vowel
|
||||
function vowel(c) {
|
||||
return "aeiouy".includes(c);
|
||||
return `aeiouyɑ'əøɛœæɶɒɨɪɔɐʊɤɯаоиеёэыуюяàèìòùỳẁȁȅȉȍȕáéíóúýẃőűâêîôûŷŵäëïöüÿẅãẽĩõũỹąęįǫųāēīōūȳăĕĭŏŭǎěǐǒǔȧėȯẏẇạẹịọụỵẉḛḭṵṳ`.includes(c);
|
||||
}
|
||||
|
||||
// remove vowels from the end of the string
|
||||
|
|
@ -411,6 +416,13 @@ function getAdjective(string) {
|
|||
// get ordinal out of integer: 1 => 1st
|
||||
const nth = n => n+(["st","nd","rd"][((n+90)%100-10)%10-1]||"th");
|
||||
|
||||
// conjunct array: [A,B,C] => "A, B and C"
|
||||
function list(array) {
|
||||
if (!Intl.ListFormat) return array.join(", ");
|
||||
const conjunction = new Intl.ListFormat(window.lang || "en", {style:"long", type:"conjunction"});
|
||||
return conjunction.format(array);
|
||||
}
|
||||
|
||||
// split string into 2 almost equal parts not breaking words
|
||||
function splitInTwo(str) {
|
||||
const half = str.length / 2;
|
||||
|
|
@ -563,7 +575,12 @@ function getAbsolutePath(href) {
|
|||
|
||||
// open URL in a new tab or window
|
||||
function openURL(url) {
|
||||
window.open(url, '_blank');
|
||||
window.open(url, "_blank");
|
||||
}
|
||||
|
||||
// open project wiki-page
|
||||
function wiki(page) {
|
||||
window.open("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/" + page, "_blank");
|
||||
}
|
||||
|
||||
// wrap URL into html a element
|
||||
|
|
@ -610,5 +627,5 @@ void function() {
|
|||
cancel.addEventListener("click", () => prompt.style.display = "none");
|
||||
}()
|
||||
|
||||
// localStorageDB
|
||||
!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 console.error("indexDB not supported");var n,o={k:"",v:""},r=t.open("d2",1);r.onsuccess=function(e){n=this.result},r.onerror=function(e){console.error("indexedDB request error"),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)}}}();
|
||||
// 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 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){console.error("indexedDB request error"),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)}}}();
|
||||
3
run_php_server.bat
Normal file
3
run_php_server.bat
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
start chrome.exe http://localhost:8000/
|
||||
@echo off
|
||||
php -S localhost:8000
|
||||
Loading…
Add table
Add a link
Reference in a new issue