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

This commit is contained in:
Mészáros Gergely 2022-01-17 13:23:06 +01:00
commit b58674dddd
23 changed files with 850 additions and 522 deletions

View file

@ -29,16 +29,19 @@ input:read-only {
cursor: default; cursor: default;
} }
input[type="radio"] {
vertical-align: bottom;
cursor: pointer;
}
textarea { textarea {
padding: 2px; padding: 3px;
text-indent: 1px;
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;
} }
iframe { iframe {
border: 0; border: 0;
pointer-events: none;
width: 100%; width: 100%;
} }
@ -835,75 +838,79 @@ fieldset {
} }
.matrix-table { .matrix-table {
width: 100%; max-height: 80vh;
font-size: smaller; max-width: 85vw;
text-align: center; scrollbar-width: thin;
border-collapse: collapse; overflow: auto;
} }
table.matrix-table th, .matrix-table > table {
table.matrix-table td { text-align: center;
border-collapse: collapse;
font-size: smaller;
}
.matrix-table > table th,
.matrix-table > table td {
border: 1px solid var(--dark-solid); border: 1px solid var(--dark-solid);
height: 2em; height: 2em;
padding: 0.2em; padding: 0.2em;
position: relative; position: relative;
} }
table.matrix-table th { .matrix-table > table th {
background-color: #302a2a; background-color: #302a2a;
color: #ffffff; color: #ffffff;
} }
table.matrix-table tr:hover th { .matrix-table > table td:hover {
background: #3e3636;
}
table.matrix-table td:hover {
outline: 2px solid var(--dark-solid); outline: 2px solid var(--dark-solid);
outline-offset: -1px; outline-offset: -1px;
z-index: 1; z-index: 1;
cursor: pointer;
} }
table.matrix-table td.Ally { .matrix-table > table td.Ally {
background-color: #73ec73; background-color: #73ec73;
color: #000000; color: #000000;
} }
table.matrix-table td.Friendly { .matrix-table > table td.Friendly {
background-color: #d4f8aa; background-color: #d4f8aa;
} }
table.matrix-table td.Neutral { .matrix-table > table td.Neutral {
background-color: #d8d9d3; background-color: #d8d9d3;
} }
table.matrix-table td.Suspicion { .matrix-table > table td.Suspicion {
background-color: #eeafaa; background-color: #eeafaa;
} }
table.matrix-table td.Enemy { .matrix-table > table td.Enemy {
background-color: #ffa39c; background-color: #ffa39c;
color: #af0d23; color: #af0d23;
} }
table.matrix-table td.Unknown { .matrix-table > table td.Unknown {
background-color: #c1bfbf; background-color: #c1bfbf;
} }
table.matrix-table td.Rival { .matrix-table > table td.Rival {
background-color: #bd845c; background-color: #bd845c;
} }
table.matrix-table td.Vassal { .matrix-table > table td.Vassal {
background-color: #87cefa; background-color: #87cefa;
} }
table.matrix-table td.Suzerain { .matrix-table > table td.Suzerain {
background-color: #8f8fe1; background-color: #8f8fe1;
} }
table.matrix-table td.x { .matrix-table > table td.x {
background-color: #d4ca94; background-color: #d4ca94;
cursor: initial;
} }
#sizeOutput { #sizeOutput {
@ -1311,19 +1318,23 @@ div.slider .ui-slider-handle {
} }
#alertMessage::-webkit-scrollbar, #alertMessage::-webkit-scrollbar,
.table::-webkit-scrollbar { .table::-webkit-scrollbar,
.matrix-table::-webkit-scrollbar {
width: 6px; width: 6px;
height: 6px;
background-color: transparent; background-color: transparent;
} }
#alertMessage::-webkit-scrollbar-thumb, #alertMessage::-webkit-scrollbar-thumb,
.table::-webkit-scrollbar-thumb { .table::-webkit-scrollbar-thumb,
.matrix-table::-webkit-scrollbar-thumb {
background-color: #aaa; background-color: #aaa;
border-radius: 6px; border-radius: 6px;
} }
#alertMessage::-webkit-scrollbar-thumb:hover, #alertMessage::-webkit-scrollbar-thumb:hover,
.table::-webkit-scrollbar-thumb:hover { .table::-webkit-scrollbar-thumb:hover,
.matrix-table::-webkit-scrollbar-thumb:hover {
background: #666; background: #666;
} }
@ -1505,15 +1516,14 @@ div.states > .riverType {
width: 5em; width: 5em;
} }
div.states > .coaIcon { .coaIcon {
stroke-width: 3; stroke-width: 3;
width: 1.4em; width: 1.4em;
height: 1.4em; height: 1.4em;
margin: -0.3em 0; margin: -0.3em 0;
cursor: pointer;
} }
div.states > .coaIcon > use { .coaIcon > use {
pointer-events: none; pointer-events: none;
} }
@ -2001,7 +2011,7 @@ div.textual fieldset {
div.textual span, div.textual span,
.textual legend { .textual legend {
font-size: 0.8em; font-size: 0.9em;
font-weight: bold; font-weight: bold;
} }
@ -2120,8 +2130,9 @@ svg.button {
text-decoration: underline; text-decoration: underline;
} }
#info-line { .info-line {
font-size: 0.9em; font-size: 0.9em;
font-style: italic;
color: gray; color: gray;
user-select: none; user-select: none;
} }
@ -2265,10 +2276,10 @@ svg.button {
left: 50%; left: 50%;
top: 50%; top: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
max-width: 22em; max-width: 23em;
background-color: #fff;
padding: 1.2em; padding: 1.2em;
border: solid 1px #000; background-color: var(--bg-dialogs);
border: solid 1px var(--dark-solid);
font-size: 1.2em; font-size: 1.2em;
z-index: 1000; z-index: 1000;
} }

View file

@ -947,7 +947,7 @@
<tr data-tip="Map seed number. Seed produces the same map only if canvas size and options are the same"> <tr data-tip="Map seed number. Seed produces the same map only if canvas size and options are the same">
<td> <td>
<i data-tip="Show seed history to apply a previous seed" id="optionsMapHistory" class="icon-history"></i> <i data-tip="Show seed history to apply a previous seed" id="optionsMapHistory" class="icon-hourglass-1"></i>
</td> </td>
<td>Map seed</td> <td>Map seed</td>
<td> <td>
@ -1062,7 +1062,7 @@
<input id="regionsInput" data-stored="regions" type="range" min=0 max=99 value=13> <input id="regionsInput" data-stored="regions" type="range" min=0 max=99 value=13>
</td> </td>
<td> <td>
<input id="regionsOutput" data-stored="regions" type="number" min=0 max=99 value=13> <input id="regionsOutput" data-stored="regions" type="number" min=0 max=999 value=13>
</td> </td>
</tr> </tr>
@ -1451,9 +1451,21 @@
</div> </div>
<div id="aboutContent" class="tabcontent"> <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>
<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> <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.
<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> 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</a>, <a href="https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Q&A" target="_blank">Q&A</a>, <a href="https://youtube.com/playlist?list=PLtgiuDC8iVR2gIG8zMTRn7T_L0arl9h1C" target="_blank">Video tutorial</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 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"> <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"> <a href="https://www.patreon.com/azgaar" target="_blank" style="color: white; text-decoration: none; font-family: sans-serif">
<div> <div>
@ -1466,7 +1478,10 @@
</div> </div>
</a> </a>
</div> </div>
<p>Special thanks to <a data-tip="Click to see list of supporters" onclick="showSupporters()">all supporters</a> on Patreon!</p>
<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"> <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&quote=" data-tip="Share on Facebook" target="_blank"><img alt="Share on Facebook" src="images/Facebook.png" /></a></li> <li><a href="https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fazgaar.github.io%2FFantasy-Map-Generator%2F&quote=" data-tip="Share on Facebook" target="_blank"><img alt="Share on Facebook" src="images/Facebook.png" /></a></li>
@ -2038,12 +2053,15 @@
</div> </div>
</div> </div>
<div id="mfcgPreviewSection" data-tip="Burg preview in the Medieval Fantasy City Generator. Default seed is a conbimation of map seed and burg id" style="display: flex; flex-direction: column"> <div id="mfcgPreviewSection" data-tip="Burg preview in the Medieval Fantasy City Generator. Default seed is a combination of map seed and burg id" style="display: flex; flex-direction: column">
<div> <div>
See in <a id="mfcgLink" target="_blank">City Generator by Watabou</a>. See in <a id="mfcgLink" target="_blank">City Generator by Watabou</a>.
<div id="mfcgBurgSeedSection">
Seed: <input id="mfcgBurgSeed" style="width: 10em" type="number" min=1 max="1e13" step="1" /> Seed: <input id="mfcgBurgSeed" style="width: 10em" type="number" min=1 max="1e13" step="1" />
<i id="regenerateMFCGBurgSeed" data-tip="Randomize Medieval Fantasy City Generator burg seed" class="icon-arrows-cw pointer" style="margin-left: .1em"></i> <i id="regenerateMFCGBurgSeed" data-tip="Randomize Medieval Fantasy City Generator burg seed" class="icon-arrows-cw pointer" style="margin-left: .1em"></i>
</div> </div>
<i id="addCustomMFCGBurgLink" data-tip="Provide custom link to the burg map" class="icon-pencil pointer" style="margin-left: .1em"></i>
</div>
<iframe id="mfcgPreview" sandbox="allow-scripts allow-same-origin"></iframe> <iframe id="mfcgPreview" sandbox="allow-scripts allow-same-origin"></iframe>
</div> </div>
</div> </div>
@ -2623,8 +2641,10 @@
<option value="Sultanate">Sultanate</option> <option value="Sultanate">Sultanate</option>
<option value="Tsardom">Tsardom</option> <option value="Tsardom">Tsardom</option>
<option value="Ulus">Ulus</option> <option value="Ulus">Ulus</option>
<option value="Viceroyalty">Viceroyalty</option>
</optgroup> </optgroup>
<optgroup label="Republic"> <optgroup label="Republic">
<option value="Chancellery">Chancellery</option>
<option value="City-state">City-state</option> <option value="City-state">City-state</option>
<option value="Diarchy">Diarchy</option> <option value="Diarchy">Diarchy</option>
<option value="Federation">Federation</option> <option value="Federation">Federation</option>
@ -2662,8 +2682,10 @@
<option value="Divine Kingdom">Divine Kingdom</option> <option value="Divine Kingdom">Divine Kingdom</option>
<option value="Divine Empire">Divine Empire</option> <option value="Divine Empire">Divine Empire</option>
<option value="Eparchy">Eparchy</option> <option value="Eparchy">Eparchy</option>
<option value="Exarchate">Exarchate</option>
<option value="Holy State">Holy State</option> <option value="Holy State">Holy State</option>
<option value="Imamah">Imamah</option> <option value="Imamah">Imamah</option>
<option value="Patriarchate">Patriarchate</option>
<option value="Theocracy">Theocracy</option> <option value="Theocracy">Theocracy</option>
</optgroup> </optgroup>
<optgroup label="Anarchy"> <optgroup label="Anarchy">
@ -2740,33 +2762,27 @@
<div id="diplomacyEditor" class="dialog stable" style="display: none"> <div id="diplomacyEditor" class="dialog stable" style="display: none">
<div id="diplomacyHeader" class="header"> <div id="diplomacyHeader" class="header">
<div style="left:.2em" data-tip="Click to sort by state name" class="sortable alphabetically" data-sortby="name">State&nbsp;</div> <div style="left:.2em" data-tip="Click to sort by state name" class="sortable alphabetically" data-sortby="name">State&nbsp;</div>
<div style="left:13.4em" data-tip="Click to sort by diplomatical relations" class="sortable alphabetically" data-sortby="relations">Relations&nbsp;</div> <div style="left:14.4em" data-tip="Click to sort by diplomatical relations" class="sortable alphabetically" data-sortby="relations">Relations&nbsp;</div>
</div> </div>
<div id="diplomacyBodySection" class="table"></div> <div id="diplomacyBodySection" class="table"></div>
<div class="info-line">Click on state name to see relations.<br>Click on relations name to change it</div>
<div id="diplomacySelect">
<div data-tip="Ally means states formed a defensive pact and will protect each other in case of third party aggression">Ally</div>
<div data-tip="State is friendly to anouther state when they share some common interests">Friendly</div>
<div data-tip="Neutral means states relations are neither positive nor negative">Neutral</div>
<div data-tip="Suspicion means state has a cautious distrust of another state">Suspicion</div>
<div data-tip="Enemies are states at war with each other">Enemy</div>
<div data-tip="Relations are unknown if states do not have enough information about each other">Unknown</div>
<div data-tip="Rivalry is a state of competing for dominance in the region">Rival</div>
<div data-tip="Vassal is a state having obligation to its suzerain">Vassal</div>
<div data-tip="Suzerain is a state having some control over its vassals">Suzerain</div>
</div>
<div id="diplomacyBottom" style="margin-top: .1em"> <div id="diplomacyBottom" style="margin-top: .1em">
<button id="diplomacyEditorRefresh" data-tip="Refresh the Editor" class="icon-cw"></button> <button id="diplomacyEditorRefresh" data-tip="Refresh the Editor" class="icon-cw"></button>
<button id="diplomacyEditStyle" data-tip="Edit states (including diplomacy view) style in Style Editor" class="icon-adjust"></button> <button id="diplomacyEditStyle" data-tip="Edit states (including diplomacy view) style in Style Editor" class="icon-adjust"></button>
<button id="diplomacyRegenerate" data-tip="Regenerate diplomatical relations" class="icon-retweet"></button> <button id="diplomacyRegenerate" data-tip="Regenerate diplomatical relations" class="icon-retweet"></button>
<button id="diplomacyReset" data-tip="Reset diplomatical relations of selected state to Neutral" class="icon-eraser"></button>
<button id="diplomacyHistory" data-tip="Show relations history" class="icon-hourglass-1"></button> <button id="diplomacyHistory" data-tip="Show relations history" class="icon-hourglass-1"></button>
<button id="diplomacyMatrix" data-tip="Show relations matrix" class="icon-list-bullet"></button> <button id="diplomacyShowMatrix" data-tip="Show relations matrix" class="icon-list-bullet"></button>
<button id="diplomacyExport" data-tip="Save state relations matrix as a text file (.csv)" class="icon-download"></button> <button id="diplomacyExport" data-tip="Save state relations matrix as a text file (.csv)" class="icon-download"></button>
</div> </div>
</div> </div>
<div id="diplomacyMatrix" class="dialog" style="display: none">
<div id="diplomacyMatrixBody" class="matrix-table"></div>
</div>
<div id="provinceNameEditor" class="dialog" data-province="0" style="display: none"> <div id="provinceNameEditor" class="dialog" data-province="0" style="display: none">
<div> <div>
<div data-tip="Province short name" class="label">Short name:</div> <div data-tip="Province short name" class="label">Short name:</div>
@ -2784,6 +2800,8 @@
<option value="Autonomy">Autonomy</option> <option value="Autonomy">Autonomy</option>
<option value="Barony">Barony</option> <option value="Barony">Barony</option>
<option value="Canton">Canton</option> <option value="Canton">Canton</option>
<option value="Captaincy">Captaincy</option>
<option value="Chiefdom">Chiefdom</option>
<option value="Clan">Clan</option> <option value="Clan">Clan</option>
<option value="Colony">Colony</option> <option value="Colony">Colony</option>
<option value="Council">Council</option> <option value="Council">Council</option>
@ -2791,6 +2809,7 @@
<option value="Deanery">Deanery</option> <option value="Deanery">Deanery</option>
<option value="Department">Department</option> <option value="Department">Department</option>
<option value="Dependency">Dependency</option> <option value="Dependency">Dependency</option>
<option value="Diaconate">Diaconate</option>
<option value="District">District</option> <option value="District">District</option>
<option value="Earldom">Earldom</option> <option value="Earldom">Earldom</option>
<option value="Governorate">Governorate</option> <option value="Governorate">Governorate</option>
@ -2800,6 +2819,7 @@
<option value="Landgrave">Landgrave</option> <option value="Landgrave">Landgrave</option>
<option value="Mandate">Mandate</option> <option value="Mandate">Mandate</option>
<option value="Margrave">Margrave</option> <option value="Margrave">Margrave</option>
<option value="Municipality">Municipality</option>
<option value="Occupation zone">Occupation zone</option> <option value="Occupation zone">Occupation zone</option>
<option value="Parish">Parish</option> <option value="Parish">Parish</option>
<option value="Prefecture">Prefecture</option> <option value="Prefecture">Prefecture</option>
@ -2807,6 +2827,7 @@
<option value="Region">Region</option> <option value="Region">Region</option>
<option value="Republic">Republic</option> <option value="Republic">Republic</option>
<option value="Reservation">Reservation</option> <option value="Reservation">Reservation</option>
<option value="Seneschalty">Seneschalty</option>
<option value="Shire">Shire</option> <option value="Shire">Shire</option>
<option value="State">State</option> <option value="State">State</option>
<option value="Territory">Territory</option> <option value="Territory">Territory</option>
@ -2874,21 +2895,22 @@
<div id="namesbaseBasesTop"> <div id="namesbaseBasesTop">
<span>Select base: </span> <span>Select base: </span>
<select id="namesbaseSelect" data-tip="Select base to edit" style="width: 12em" value="0"></select> <select id="namesbaseSelect" data-tip="Select base to edit" style="width: 12em" value="0"></select>
<span style="margin-left: 2px">Names data: </span>
</div> </div>
<div id="namesbaseBody"> <div id="namesbaseBody" style="margin-block: 2px">
<span>Names data:</span><br> <textarea id="namesbaseTextarea" data-base="0" rows=13 data-tip="Names data: a comma separated list of source names used for names generation" placeholder="Provide a names data: a comma separated list of source names" autocorrect="off" spellcheck="false"></textarea>
<textarea id="namesbaseTextarea" data-base="0" rows=12 data-tip="Names data: a comma separated list of source names used for names generation" placeholder="Provide a names data: a comma separated list of source names" autocorrect="off" spellcheck="false"></textarea>
<br>
<div> <div>
<span>Name: </span> <span>Name: </span>
<input id="namesbaseName" data-tip="Type to change a base name" placeholder="Base name" autocorrect="off" spellcheck="false" style="width:12em"/> <input id="namesbaseName" data-tip="Type to change a base name" placeholder="Base name" autocorrect="off" spellcheck="false" style="width:12em"/>
<span>Length: </span> <span>Length: </span>
<input id="namesbaseMin" data-tip="Recommended minimum name length" type="number" min=2 max=100> <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> <input id="namesbaseMax" data-tip="Recommended maximum name length" type="number" min=2 value=10>
<span>Double: </span> <span>Doubled: </span>
<input id="namesbaseDouble" data-tip="Populate with letters that can used twice in a row (geminates)" autocorrect="off" spellcheck="false" style="width:10em"> <input id="namesbaseDouble" data-tip="Populate with letters that can used twice in a row (geminates)" autocorrect="off" spellcheck="false" style="width:10em">
</div> </div>
<fieldset> <fieldset>
<legend>Generated examples: </legend> <legend>Generated examples: </legend>
<div id="namesbaseExamples" data-tip="Examples. Click to re-generate"></div> <div id="namesbaseExamples" data-tip="Examples. Click to re-generate"></div>
@ -2897,12 +2919,12 @@
<div id="namesbaseBottom"> <div id="namesbaseBottom">
<button id="namesbaseUpdateExamples" data-tip="Re-generate examples based on provided data" class="icon-arrows-cw"></button> <button id="namesbaseUpdateExamples" data-tip="Re-generate examples based on provided data" class="icon-arrows-cw"></button>
<button id="namesbaseAnalize" data-tip="Analyze namesbase to get a validity and quality overview" class="icon-flask"></button>
<button id="namesbaseAdd" data-tip="Add new namesbase" class="icon-plus"></button> <button id="namesbaseAdd" data-tip="Add new namesbase" class="icon-plus"></button>
<button id="namesbaseDefault" data-tip="Restore default namesbase" class="icon-cancel"></button> <button id="namesbaseDefault" data-tip="Restore default namesbase" class="icon-cancel"></button>
<button id="namesbaseDownload" data-tip="Download namesbase to PC" class="icon-download"></button> <button id="namesbaseDownload" data-tip="Download namesbase to PC" class="icon-download"></button>
<button id="namesbaseUpload" data-tip="Upload a namesbase from PC" class="icon-upload"></button> <button id="namesbaseUpload" data-tip="Upload a namesbase from PC" class="icon-upload"></button>
<button id="namesbaseCA" data-tip="Find or share custom namesbase on Cartography Assets portal" class="icon-drafting-compass" onclick="openURL('https://cartographyassets.com/asset-category/specific-assets/azgaars-generator/namebases/')"></button> <button id="namesbaseCA" data-tip="Find or share custom namesbase on Cartography Assets portal" class="icon-drafting-compass"></button>
<button id="namesbaseAnalyze" data-tip="Analyze namesbase to get a validity and quality overview" class="icon-flask"></button>
<button id="namesbaseSpeak" data-tip="Speak the examples. You can change voice and language in options" class="icon-voice"></button> <button id="namesbaseSpeak" data-tip="Speak the examples. You can change voice and language in options" class="icon-voice"></button>
</div> </div>
</div> </div>
@ -3289,6 +3311,7 @@
<button id="addNewBurg" data-tip="Add a new burg. Hold Shift to add multiple" class="icon-plus"></button> <button id="addNewBurg" data-tip="Add a new burg. Hold Shift to add multiple" class="icon-plus"></button>
<button id="burgsExport" data-tip="Save burgs-related data as a text file (.csv)" class="icon-download"></button> <button id="burgsExport" data-tip="Save burgs-related data as a text file (.csv)" class="icon-download"></button>
<button id="burgNamesImport" data-tip="Rename burgs in bulk" class="icon-upload"></button> <button id="burgNamesImport" data-tip="Rename burgs in bulk" class="icon-upload"></button>
<button id="burgsLockAll" data-tip="Lock or unlock all burgs" class="icon-lock"></button>
<button id="burgsRemoveAll" data-tip="Remove all unlocked burgs except for capitals. To remove a capital remove its state first" class="icon-trash"></button> <button id="burgsRemoveAll" data-tip="Remove all unlocked burgs except for capitals. To remove a capital remove its state first" class="icon-trash"></button>
</div> </div>
</div> </div>
@ -3640,8 +3663,8 @@
</div> </div>
<div data-tip="Image scale relative to image size (e.g. 5x)" style="margin-bottom: .3em"> <div data-tip="Image scale relative to image size (e.g. 5x)" style="margin-bottom: .3em">
<div class="label">Scale:</div> <div class="label">Scale:</div>
<input id="tileScaleInput" data-stored="tileScale" type="range" min=1 max=4 value=1 style="width: 11em"> <input id="tileScaleInput" data-stored="tileScale" type="range" min=1 max=4 value=1 style="width: 10em">
<input id="tileScaleOutput" data-stored="tileScale" type="number" min=1 value=1> <input id="tileScaleOutput" data-stored="tileScale" type="number" min=1 value=1 >
</div> </div>
<div data-tip="Calculated size of image if combined" style="margin-bottom: .3em"> <div data-tip="Calculated size of image if combined" style="margin-bottom: .3em">
<div class="label">Total size:</div> <div class="label">Total size:</div>
@ -3695,7 +3718,7 @@
<div id="prompt" style="display: none" class="dialog"> <div id="prompt" style="display: none" class="dialog">
<form id="promptForm"> <form id="promptForm">
<div id="promptText"></div> <div id="promptText"></div>
<input id="promptInput" type="number" step=.01 placeholder="type value" autocomplete="off" required> <input id="promptInput" type="number" step=.01 placeholder="type value" autocomplete="off">
<button type="submit">Confirm</button> <button type="submit">Confirm</button>
<button type="button" id="promptCancel" formnovalidate>Cancel</button> <button type="button" id="promptCancel" formnovalidate>Cancel</button>
</form> </form>

37
main.js
View file

@ -109,7 +109,7 @@ scaleBar.on("mousemove", () => tip("Click to open Units Editor")).on("click", ()
legend.on("mousemove", () => tip("Drag to change the position. Click to hide the legend")).on("click", () => clearLegend()); legend.on("mousemove", () => tip("Drag to change the position. Click to hide the legend")).on("click", () => clearLegend());
// main data variables // main data variables
let grid = {}; // initial grapg based on jittered square grid and data let grid = {}; // initial graph based on jittered square grid and data
let pack = {}; // packed graph and data let pack = {}; // packed graph and data
let seed; let seed;
let mapId; let mapId;
@ -161,7 +161,7 @@ let urbanDensity = +document.getElementById("urbanDensityInput").value;
applyStoredOptions(); applyStoredOptions();
// voronoi graph extention, cannot be changed arter generation // voronoi graph extension, cannot be changed after generation
let graphWidth = +mapWidthInput.value; let graphWidth = +mapWidthInput.value;
let graphHeight = +mapHeightInput.value; let graphHeight = +mapHeightInput.value;
@ -493,7 +493,7 @@ function invokeActiveZooming() {
coastline.select("#sea_island").attr("filter", filter); coastline.select("#sea_island").attr("filter", filter);
} }
// rescale lables on zoom // rescale labels on zoom
if (labels.style("display") !== "none") { if (labels.style("display") !== "none") {
labels.selectAll("g").each(function () { labels.selectAll("g").each(function () {
if (this.id === "burgLabels") return; if (this.id === "burgLabels") return;
@ -668,7 +668,7 @@ function generate() {
const parsedError = parseError(error); const parsedError = parseError(error);
clearMainTip(); clearMainTip();
alertMessage.innerHTML = `An error is occured on map generation. Please retry. alertMessage.innerHTML = `An error has occurred on map generation. Please retry.
<br>If error is critical, clear the stored data and try again. <br>If error is critical, clear the stored data and try again.
<p id="errorBox">${parsedError}</p>`; <p id="errorBox">${parsedError}</p>`;
$("#alert").dialog({ $("#alert").dialog({
@ -909,7 +909,7 @@ function defineMapSize() {
const template = document.getElementById("templateInput").value; // heightmap template const template = document.getElementById("templateInput").value; // heightmap template
const part = grid.features.some(f => f.land && f.border); // if land goes over map borders const part = grid.features.some(f => f.land && f.border); // if land goes over map borders
const max = part ? 80 : 100; // max size const max = part ? 80 : 100; // max size
const lat = () => gauss(P(0.5) ? 40 : 60, 15, 25, 75); // latiture shift const lat = () => gauss(P(0.5) ? 40 : 60, 15, 25, 75); // latitude shift
if (!part) { if (!part) {
if (template === "Pangea") return [100, 50]; if (template === "Pangea") return [100, 50];
@ -981,7 +981,10 @@ function generatePrecipitation() {
prec.selectAll("*").remove(); prec.selectAll("*").remove();
const {cells, cellsX, cellsY} = grid; const {cells, cellsX, cellsY} = grid;
cells.prec = new Uint8Array(cells.i.length); // precipitation array cells.prec = new Uint8Array(cells.i.length); // precipitation array
const modifier = precInput.value / 100; // user's input
const cellsNumberModifier = (pointsInput.dataset.cells / 10000) ** 0.25;
const precInputModifier = precInput.value / 100;
const modifier = cellsNumberModifier * precInputModifier;
const westerly = []; const westerly = [];
const easterly = []; const easterly = [];
@ -997,14 +1000,14 @@ function generatePrecipitation() {
// x2 = 60-70 latitude: wet summer (rising zone), dry winter (sinking zone) // x2 = 60-70 latitude: wet summer (rising zone), dry winter (sinking zone)
// x1 = 70-85 latitude: dry all year (sinking zone) // x1 = 70-85 latitude: dry all year (sinking zone)
// x0.5 = 85-90 latitude: dry all year (sinking zone) // x0.5 = 85-90 latitude: dry all year (sinking zone)
const lalitudeModifier = [4, 2, 2, 2, 1, 1, 2, 2, 2, 2, 3, 3, 2, 2, 1, 1, 1, 0.5]; const latitudeModifier = [4, 2, 2, 2, 1, 1, 2, 2, 2, 2, 3, 3, 2, 2, 1, 1, 1, 0.5];
const MAX_PASSABLE_ELEVATION = 85; const MAX_PASSABLE_ELEVATION = 85;
// define wind directions based on cells latitude and prevailing winds there // define wind directions based on cells latitude and prevailing winds there
d3.range(0, cells.i.length, cellsX).forEach(function (c, i) { d3.range(0, cells.i.length, cellsX).forEach(function (c, i) {
const lat = mapCoordinates.latN - (i / cellsY) * mapCoordinates.latT; const lat = mapCoordinates.latN - (i / cellsY) * mapCoordinates.latT;
const latBand = ((Math.abs(lat) - 1) / 5) | 0; const latBand = ((Math.abs(lat) - 1) / 5) | 0;
const latMod = lalitudeModifier[latBand]; const latMod = latitudeModifier[latBand];
const windTier = (Math.abs(lat - 89) / 30) | 0; // 30d tiers from 0 to 5 from N to S const windTier = (Math.abs(lat - 89) / 30) | 0; // 30d tiers from 0 to 5 from N to S
const {isWest, isEast, isNorth, isSouth} = getWindDirections(windTier); const {isWest, isEast, isNorth, isSouth} = getWindDirections(windTier);
@ -1021,14 +1024,14 @@ function generatePrecipitation() {
const vertT = southerly + northerly; const vertT = southerly + northerly;
if (northerly) { if (northerly) {
const bandN = ((Math.abs(mapCoordinates.latN) - 1) / 5) | 0; const bandN = ((Math.abs(mapCoordinates.latN) - 1) / 5) | 0;
const latModN = mapCoordinates.latT > 60 ? d3.mean(lalitudeModifier) : lalitudeModifier[bandN]; const latModN = mapCoordinates.latT > 60 ? d3.mean(latitudeModifier) : latitudeModifier[bandN];
const maxPrecN = (northerly / vertT) * 60 * modifier * latModN; const maxPrecN = (northerly / vertT) * 60 * modifier * latModN;
passWind(d3.range(0, cellsX, 1), maxPrecN, cellsX, cellsY); passWind(d3.range(0, cellsX, 1), maxPrecN, cellsX, cellsY);
} }
if (southerly) { if (southerly) {
const bandS = ((Math.abs(mapCoordinates.latS) - 1) / 5) | 0; const bandS = ((Math.abs(mapCoordinates.latS) - 1) / 5) | 0;
const latModS = mapCoordinates.latT > 60 ? d3.mean(lalitudeModifier) : lalitudeModifier[bandS]; const latModS = mapCoordinates.latT > 60 ? d3.mean(latitudeModifier) : latitudeModifier[bandS];
const maxPrecS = (southerly / vertT) * 60 * modifier * latModS; const maxPrecS = (southerly / vertT) * 60 * modifier * latModS;
passWind(d3.range(cells.i.length - cellsX, cells.i.length, 1), maxPrecS, -cellsX, cellsY); passWind(d3.range(cells.i.length - cellsX, cells.i.length, 1), maxPrecS, -cellsX, cellsY);
} }
@ -1054,7 +1057,7 @@ function generatePrecipitation() {
} }
let humidity = maxPrec - cells.h[first]; // initial water amount let humidity = maxPrec - cells.h[first]; // initial water amount
if (humidity <= 0) continue; // if first cell in row is too elevated cosdired wind dry if (humidity <= 0) continue; // if first cell in row is too elevated consider wind dry
for (let s = 0, current = first; s < steps; s++, current += next) { for (let s = 0, current = first; s < steps; s++, current += next) {
if (cells.temp[current] < -5) continue; // no flux in permafrost if (cells.temp[current] < -5) continue; // no flux in permafrost
@ -1182,7 +1185,7 @@ function reGraph() {
TIME && console.timeEnd("reGraph"); TIME && console.timeEnd("reGraph");
} }
// Detect and draw the coasline // Detect and draw the coastline
function drawCoastline() { function drawCoastline() {
TIME && console.time("drawCoastline"); TIME && console.time("drawCoastline");
reMarkFeatures(); reMarkFeatures();
@ -1191,7 +1194,7 @@ function drawCoastline() {
vertices = pack.vertices, vertices = pack.vertices,
n = cells.i.length, n = cells.i.length,
features = pack.features; features = pack.features;
const used = new Uint8Array(features.length); // store conneted features const used = new Uint8Array(features.length); // store connected features
const largestLand = d3.scan( const largestLand = d3.scan(
features.map(f => (f.land ? f.cells : 0)), features.map(f => (f.land ? f.cells : 0)),
(a, b) => b - a (a, b) => b - a
@ -1395,6 +1398,12 @@ function reMarkFeatures() {
TIME && console.timeEnd("reMarkFeatures"); TIME && console.timeEnd("reMarkFeatures");
} }
function isWetLand(moisture, temperature, height) {
if (moisture > 40 && temperature > -2 && height < 25) return true; //near coast
if (moisture > 24 && temperature > -2 && height > 24 && height < 60) return true; //off coast
return false;
}
// assign biome id for each cell // assign biome id for each cell
function defineBiomes() { function defineBiomes() {
TIME && console.time("defineBiomes"); TIME && console.time("defineBiomes");
@ -1427,7 +1436,7 @@ function defineBiomes() {
function getBiomeId(moisture, temperature, height) { function getBiomeId(moisture, temperature, height) {
if (height < 20) return 0; // marine biome: all water cells if (height < 20) return 0; // marine biome: all water cells
if (temperature < -5) return 11; // permafrost biome if (temperature < -5) return 11; // permafrost biome
if (moisture > 40 && temperature > -2 && (height < 25 || (moisture > 24 && height > 24 && height < 60))) return 12; // wetland biome if (isWetLand(moisture, temperature, height)) return 12; // wetland biome
const moistureBand = Math.min((moisture / 5) | 0, 4); // [0-4] const moistureBand = Math.min((moisture / 5) | 0, 4); // [0-4]
const temperatureBand = Math.min(Math.max(20 - temperature, 0), 25); // [0-25] const temperatureBand = Math.min(Math.max(20 - temperature, 0), 25); // [0-25]

View file

@ -31,7 +31,7 @@ window.BurgsAndStates = (function () {
function placeCapitals() { function placeCapitals() {
TIME && console.time("placeCapitals"); TIME && console.time("placeCapitals");
let count = +regionsInput.value; let count = +regionsOutput.value;
let burgs = [0]; let burgs = [0];
const rand = () => 0.5 + Math.random() * 0.5; const rand = () => 0.5 + Math.random() * 0.5;
@ -240,7 +240,7 @@ window.BurgsAndStates = (function () {
b.citadel = b.capital || (pop > 50 && P(0.75)) || P(0.5) ? 1 : 0; b.citadel = b.capital || (pop > 50 && P(0.75)) || P(0.5) ? 1 : 0;
b.plaza = pop > 50 || (pop > 30 && P(0.75)) || (pop > 10 && P(0.5)) || P(0.25) ? 1 : 0; b.plaza = pop > 50 || (pop > 30 && P(0.75)) || (pop > 10 && P(0.5)) || P(0.25) ? 1 : 0;
b.walls = b.capital || pop > 30 || (pop > 20 && P(0.75)) || (pop > 10 && P(0.5)) || P(0.2) ? 1 : 0; b.walls = b.capital || pop > 30 || (pop > 20 && P(0.75)) || (pop > 10 && P(0.5)) || P(0.2) ? 1 : 0;
b.shanty = pop > 30 || (pop > 20 && P(0.75)) || (b.walls && P(0.75)) ? 1 : 0; b.shanty = pop > 60 || (pop > 40 && P(0.75)) || (pop > 20 && b.walls && P(0.4)) ? 1 : 0;
const religion = cells.religion[b.cell]; const religion = cells.religion[b.cell];
const theocracy = pack.states[b.state].form === "Theocracy"; const theocracy = pack.states[b.state].form === "Theocracy";
b.temple = (religion && theocracy) || pop > 50 || (pop > 35 && P(0.75)) || (pop > 20 && P(0.5)) ? 1 : 0; b.temple = (religion && theocracy) || pop > 50 || (pop > 35 && P(0.75)) || (pop > 20 && P(0.5)) ? 1 : 0;
@ -726,7 +726,7 @@ window.BurgsAndStates = (function () {
TIME && console.time("assignColors"); TIME && console.time("assignColors");
const colors = ["#66c2a5", "#fc8d62", "#8da0cb", "#e78ac3", "#a6d854", "#ffd92f"]; // d3.schemeSet2; const colors = ["#66c2a5", "#fc8d62", "#8da0cb", "#e78ac3", "#a6d854", "#ffd92f"]; // d3.schemeSet2;
// assin basic color using greedy coloring algorithm // assign basic color using greedy coloring algorithm
pack.states.forEach(s => { pack.states.forEach(s => {
if (!s.i || s.removed) return; if (!s.i || s.removed) return;
const neibs = s.neighbors; const neibs = s.neighbors;
@ -962,12 +962,12 @@ window.BurgsAndStates = (function () {
const republic = { const republic = {
Republic: 75, Republic: 75,
Federation: 4, Federation: 4,
Oligarchy: 2, "Trade Company": 4,
"Most Serene Republic": 2, "Most Serene Republic": 2,
Oligarchy: 2,
Tetrarchy: 1, Tetrarchy: 1,
Triumvirate: 1, Triumvirate: 1,
Diarchy: 1, Diarchy: 1,
"Trade Company": 4,
Junta: 1 Junta: 1
}; // weighted random }; // weighted random
const union = {Union: 3, League: 4, Confederation: 1, "United Kingdom": 1, "United Republic": 1, "United Provinces": 2, Commonwealth: 1, Heptarchy: 1}; // weighted random const union = {Union: 3, League: 4, Confederation: 1, "United Kingdom": 1, "United Republic": 1, "United Provinces": 2, Commonwealth: 1, Heptarchy: 1}; // weighted random
@ -997,7 +997,7 @@ window.BurgsAndStates = (function () {
const form = monarchy[tier]; const form = monarchy[tier];
// Default name depends on exponent tier, some culture bases have special names for tiers // Default name depends on exponent tier, some culture bases have special names for tiers
if (s.diplomacy) { if (s.diplomacy) {
if (form === "Duchy" && s.neighbors.length > 1 && rand(6) < s.neighbors.length && s.diplomacy.includes("Vassal")) return "Marches"; // some vassal dutchies on borderland if (form === "Duchy" && s.neighbors.length > 1 && rand(6) < s.neighbors.length && s.diplomacy.includes("Vassal")) return "Marches"; // some vassal duchies on borderland
if (base === 1 && P(0.3) && s.diplomacy.includes("Vassal")) return "Dominion"; // English vassals if (base === 1 && P(0.3) && s.diplomacy.includes("Vassal")) return "Dominion"; // English vassals
if (P(0.3) && s.diplomacy.includes("Vassal")) return "Protectorate"; // some vassals if (P(0.3) && s.diplomacy.includes("Vassal")) return "Protectorate"; // some vassals
} }
@ -1037,7 +1037,12 @@ window.BurgsAndStates = (function () {
if (tier < 2 && P(0.5)) return "Diocese"; if (tier < 2 && P(0.5)) return "Diocese";
if (tier < 2 && P(0.5)) return "Bishopric"; if (tier < 2 && P(0.5)) return "Bishopric";
} }
if (tier < 2 && P(0.9) && [7, 5].includes(base)) return "Eparchy"; // Greek, Ruthenian if (P(0.9) && [7, 5].includes(base)) {
// Greek, Ruthenian
if (tier < 2) return "Eparchy";
if (tier === 2) return "Exarchate";
if (tier > 2) return "Patriarchate";
}
if (P(0.9) && [21, 16].includes(base)) return "Imamah"; // Nigerian, Turkish if (P(0.9) && [21, 16].includes(base)) return "Imamah"; // Nigerian, Turkish
if (tier > 2 && P(0.8) && [18, 17, 28].includes(base)) return "Caliphate"; // Arabic, Berber, Swahili if (tier > 2 && P(0.8) && [18, 17, 28].includes(base)) return "Caliphate"; // Arabic, Berber, Swahili
return rw(theocracy); return rw(theocracy);
@ -1093,7 +1098,7 @@ window.BurgsAndStates = (function () {
const max = percentage == 100 ? 1000 : gauss(20, 5, 5, 100) * percentage ** 0.5; // max growth const max = percentage == 100 ? 1000 : gauss(20, 5, 5, 100) * percentage ** 0.5; // max growth
const forms = { const forms = {
Monarchy: {County: 11, Earldom: 3, Shire: 1, Landgrave: 1, Margrave: 1, Barony: 1}, Monarchy: {County: 22, Earldom: 6, Shire: 2, Landgrave: 2, Margrave: 2, Barony: 2, Captaincy: 1, Seneschalty: 1},
Republic: {Province: 6, Department: 2, Governorate: 2, District: 1, Canton: 1, Prefecture: 1}, Republic: {Province: 6, Department: 2, Governorate: 2, District: 1, Canton: 1, Prefecture: 1},
Theocracy: {Parish: 3, Deanery: 1}, Theocracy: {Parish: 3, Deanery: 1},
Union: {Province: 1, State: 1, Canton: 1, Republic: 1, County: 1, Council: 1}, Union: {Province: 1, State: 1, Canton: 1, Republic: 1, County: 1, Council: 1},

View file

@ -405,8 +405,8 @@ function saveGeoJSON_Cells() {
json.features.push(feature); json.features.push(feature);
}); });
const name = getFileName("Cells") + ".geojson"; const fileName = getFileName("Cells") + ".geojson";
downloadFile(JSON.stringify(json), name, "application/json"); downloadFile(JSON.stringify(json), fileName, "application/json");
} }
function saveGeoJSON_Routes() { function saveGeoJSON_Routes() {
@ -421,30 +421,25 @@ function saveGeoJSON_Routes() {
json.features.push(feature); json.features.push(feature);
}); });
const name = getFileName("Routes") + ".geojson"; const fileName = getFileName("Routes") + ".geojson";
downloadFile(JSON.stringify(json), name, "application/json"); downloadFile(JSON.stringify(json), fileName, "application/json");
} }
function saveGeoJSON_Rivers() { function saveGeoJSON_Rivers() {
const json = {type: "FeatureCollection", features: []}; const json = {type: "FeatureCollection", features: []};
rivers.selectAll("path").each(function () { rivers.selectAll("path").each(function () {
const coordinates = getRiverPoints(this); const river = pack.rivers.find(r => r.i === +this.id.slice(5));
const id = this.id; if (!river) return;
const width = +this.dataset.increment;
const increment = +this.dataset.increment;
const river = pack.rivers.find(r => r.i === +id.slice(5));
const name = river ? river.name : "";
const type = river ? river.type : "";
const i = river ? river.i : "";
const basin = river ? river.basin : "";
const feature = {type: "Feature", geometry: {type: "LineString", coordinates}, properties: {id, i, basin, name, type, width, increment}}; const coordinates = getRiverPoints(this);
const properties = {...river, id: this.id};
const feature = {type: "Feature", geometry: {type: "LineString", coordinates}, properties};
json.features.push(feature); json.features.push(feature);
}); });
const name = getFileName("Rivers") + ".geojson"; const fileName = getFileName("Rivers") + ".geojson";
downloadFile(JSON.stringify(json), name, "application/json"); downloadFile(JSON.stringify(json), fileName, "application/json");
} }
function saveGeoJSON_Markers() { function saveGeoJSON_Markers() {

View file

@ -828,6 +828,7 @@ function parseLoadedData(data) {
// v 1.65 changed rivers data // v 1.65 changed rivers data
d3.select("#rivers").attr("style", null); // remove style to unhide layer d3.select("#rivers").attr("style", null); // remove style to unhide layer
const {cells, rivers} = pack; const {cells, rivers} = pack;
const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2);
for (const river of rivers) { for (const river of rivers) {
const node = document.getElementById("river" + river.i); const node = document.getElementById("river" + river.i);
@ -856,7 +857,7 @@ function parseLoadedData(data) {
river.points = riverPoints; river.points = riverPoints;
} }
river.widthFactor = 1; river.widthFactor = defaultWidthFactor;
cells.i.forEach(i => { cells.i.forEach(i => {
const riverInWater = cells.r[i] && cells.h[i] < 20; const riverInWater = cells.r[i] && cells.h[i] < 20;
@ -1013,6 +1014,31 @@ function parseLoadedData(data) {
ERROR && console.error("Data Integrity Check. Province", p.i, "is linked to removed state", p.state); ERROR && console.error("Data Integrity Check. Province", p.i, "is linked to removed state", p.state);
p.removed = true; // remove incorrect province p.removed = true; // remove incorrect province
}); });
{
const markerIds = [];
let nextId = last(pack.markers)?.i + 1 || 0;
pack.markers.forEach(marker => {
if (markerIds[marker.i]) {
ERROR && console.error("Data Integrity Check. Marker", marker.i, "has non-unique id. Changing to", nextId);
const domElements = document.querySelectorAll("#marker" + marker.i);
if (domElements[1]) domElements[1].id = "marker" + nextId; // rename 2nd dom element
const noteElements = notes.filter(note => note.id === "marker" + marker.i);
if (noteElements[1]) noteElements[1].id = "marker" + nextId; // rename 2nd note
marker.i = nextId;
nextId += 1;
} else {
markerIds[marker.i] = true;
}
});
// sort markers by index
pack.markers.sort((a, b) => a.i - b.i);
}
})(); })();
changeMapSize(); changeMapSize();

View file

@ -97,7 +97,7 @@ window.Markers = (function () {
} }
function addMarker({cell, type, icon, dx, dy, px}) { function addMarker({cell, type, icon, dx, dy, px}) {
const i = pack.markers.length; const i = last(pack.markers)?.i + 1 || 0;
const [x, y] = getMarkerCoordinates(cell); const [x, y] = getMarkerCoordinates(cell);
const marker = {i, icon, type, x, y, cell}; const marker = {i, icon, type, x, y, cell};
if (dx) marker.dx = dx; if (dx) marker.dx = dx;

View file

@ -30,63 +30,59 @@ window.Religions = (function () {
const base = { const base = {
number: ["One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve"], number: ["One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve"],
being: [ being: [
"God",
"Goddess",
"Lord",
"Lady",
"Deity",
"Creator",
"Maker",
"Overlord",
"Ruler",
"Chief",
"Master",
"Spirit",
"Ancestor", "Ancestor",
"Ancient",
"Brother",
"Chief",
"Council",
"Creator",
"Deity",
"Elder",
"Father", "Father",
"Forebear", "Forebear",
"Forefather", "Forefather",
"Mother",
"Brother",
"Sister",
"Elder",
"Numen",
"Ancient",
"Virgin",
"Giver", "Giver",
"Council", "God",
"Goddess",
"Guardian", "Guardian",
"Reaper" "Lady",
"Lord",
"Maker",
"Master",
"Mother",
"Numen",
"Overlord",
"Reaper",
"Ruler",
"Sister",
"Spirit",
"Virgin"
], ],
animal: [ animal: [
"Dragon",
"Wyvern",
"Phoenix",
"Unicorn",
"Sphinx",
"Centaur",
"Pegasus",
"Kraken",
"Basilisk",
"Chimera",
"Cyclope",
"Antelope", "Antelope",
"Ape", "Ape",
"Badger", "Badger",
"Basilisk",
"Bear", "Bear",
"Beaver", "Beaver",
"Bison", "Bison",
"Boar", "Boar",
"Buffalo", "Buffalo",
"Camel",
"Cat", "Cat",
"Centaur",
"Chimera",
"Cobra", "Cobra",
"Crane", "Crane",
"Crocodile", "Crocodile",
"Crow", "Crow",
"Cyclope",
"Deer", "Deer",
"Dog", "Dog",
"Dragon",
"Eagle", "Eagle",
"Elk", "Elk",
"Falcon",
"Fox", "Fox",
"Goat", "Goat",
"Goose", "Goose",
@ -94,10 +90,12 @@ window.Religions = (function () {
"Hawk", "Hawk",
"Heron", "Heron",
"Horse", "Horse",
"Hound",
"Hyena", "Hyena",
"Ibis", "Ibis",
"Jackal", "Jackal",
"Jaguar", "Jaguar",
"Kraken",
"Lark", "Lark",
"Leopard", "Leopard",
"Lion", "Lion",
@ -107,177 +105,179 @@ window.Religions = (function () {
"Mule", "Mule",
"Narwhal", "Narwhal",
"Owl", "Owl",
"Ox",
"Panther", "Panther",
"Pegasus",
"Phoenix",
"Rat", "Rat",
"Raven", "Raven",
"Rook", "Rook",
"Scorpion", "Scorpion",
"Serpent",
"Shark", "Shark",
"Sheep", "Sheep",
"Snake", "Snake",
"Sphinx",
"Spider", "Spider",
"Swan", "Swan",
"Tiger", "Tiger",
"Turtle", "Turtle",
"Unicorn",
"Viper", "Viper",
"Vulture", "Vulture",
"Walrus", "Walrus",
"Wolf", "Wolf",
"Wolverine", "Wolverine",
"Worm", "Worm",
"Camel", "Wyvern"
"Falcon",
"Hound",
"Ox",
"Serpent"
], ],
adjective: [ adjective: [
"New",
"Good",
"High",
"Old",
"Great",
"Big",
"Young",
"Major",
"Strong",
"Happy",
"Last",
"Main",
"Huge",
"Far",
"Beautiful",
"Wild",
"Fair",
"Prime",
"Crazy",
"Ancient",
"Proud",
"Secret",
"Lucky",
"Sad",
"Silent",
"Latter",
"Severe",
"Fat",
"Holy",
"Pure",
"Aggressive", "Aggressive",
"Honest", "Almighty",
"Giant", "Ancient",
"Mad", "Beautiful",
"Pregnant", "Benevolent",
"Distant", "Big",
"Lost",
"Broken",
"Blind", "Blind",
"Friendly", "Blond",
"Unknown", "Bloody",
"Sleeping", "Brave",
"Slumbering", "Broken",
"Loud", "Brutal",
"Hungry", "Burning",
"Wise", "Calm",
"Worried", "Cheerful",
"Sacred", "Crazy",
"Magical", "Cruel",
"Superior",
"Patient",
"Dead", "Dead",
"Deadly", "Deadly",
"Peaceful",
"Grateful",
"Frozen",
"Evil",
"Scary",
"Burning",
"Divine",
"Bloody",
"Dying",
"Waking",
"Brutal",
"Unhappy",
"Calm",
"Cruel",
"Favorable",
"Blond",
"Explicit",
"Disturbing",
"Devastating", "Devastating",
"Brave", "Distant",
"Sunny", "Disturbing",
"Troubled", "Divine",
"Flying", "Dying",
"Sustainable",
"Marine",
"Fatal",
"Inherent",
"Selected",
"Naval",
"Cheerful",
"Almighty",
"Benevolent",
"Eternal", "Eternal",
"Evil",
"Explicit",
"Fair",
"Far",
"Fat",
"Fatal",
"Favorable",
"Flying",
"Friendly",
"Frozen",
"Giant",
"Good",
"Grateful",
"Great",
"Happy",
"High",
"Holy",
"Honest",
"Huge",
"Hungry",
"Immutable", "Immutable",
"Infallible" "Infallible",
"Inherent",
"Last",
"Latter",
"Lost",
"Loud",
"Lucky",
"Mad",
"Magical",
"Main",
"Major",
"Marine",
"Naval",
"New",
"Old",
"Patient",
"Peaceful",
"Pregnant",
"Prime",
"Proud",
"Pure",
"Sacred",
"Sad",
"Scary",
"Secret",
"Selected",
"Severe",
"Silent",
"Sleeping",
"Slumbering",
"Strong",
"Sunny",
"Superior",
"Sustainable",
"Troubled",
"Unhappy",
"Unknown",
"Waking",
"Wild",
"Wise",
"Worried",
"Young"
], ],
genitive: [ genitive: [
"Day",
"Life",
"Death",
"Night",
"Home",
"Fog",
"Snow",
"Winter",
"Summer",
"Cold", "Cold",
"Springs", "Day",
"Gates", "Death",
"Nature",
"Thunder",
"Lightning",
"War",
"Ice",
"Frost",
"Fire",
"Doom", "Doom",
"Fate", "Fate",
"Pain", "Fire",
"Fog",
"Frost",
"Gates",
"Heaven", "Heaven",
"Home",
"Ice",
"Justice", "Justice",
"Life",
"Light", "Light",
"Lightning",
"Love", "Love",
"Nature",
"Night",
"Pain",
"Snow",
"Springs",
"Summer",
"Thunder",
"Time", "Time",
"Victory" "Victory",
"War",
"Winter"
], ],
theGenitive: [ theGenitive: [
"World",
"Word",
"South",
"West",
"North",
"East",
"Sun",
"Moon",
"Peak",
"Fall",
"Dawn",
"Eclipse",
"Abyss", "Abyss",
"Blood", "Blood",
"Tree", "Dawn",
"Earth", "Earth",
"East",
"Eclipse",
"Fall",
"Harvest", "Harvest",
"Moon",
"North",
"Peak",
"Rainbow", "Rainbow",
"Sea", "Sea",
"Sky", "Sky",
"South",
"Stars", "Stars",
"Storm", "Storm",
"Sun",
"Tree",
"Underworld", "Underworld",
"Wild" "West",
"Wild",
"Word",
"World"
], ],
color: ["Dark", "Light", "Bright", "Golden", "White", "Black", "Red", "Pink", "Purple", "Blue", "Green", "Yellow", "Amber", "Orange", "Brown", "Grey"] color: ["Amber", "Black", "Blue", "Bright", "Brown", "Dark", "Golden", "Green", "Grey", "Light", "Orange", "Pink", "Purple", "Red", "White", "Yellow"]
}; };
const forms = { const forms = {
@ -308,10 +308,10 @@ window.Religions = (function () {
Monotheism: {Religion: 1, Church: 1}, Monotheism: {Religion: 1, Church: 1},
"Non-theism": {Beliefs: 3, Spirits: 1}, "Non-theism": {Beliefs: 3, Spirits: 1},
Cult: {Cult: 4, Sect: 4, Worship: 1, Orden: 1, Coterie: 1, Arcanum: 1}, Cult: {Cult: 4, Sect: 4, Arcanum: 1, Coterie: 1, Order: 1, Worship: 1},
"Dark Cult": {Cult: 2, Sect: 2, Occultism: 1, Idols: 1, Coven: 1, Circle: 1, Blasphemy: 1}, "Dark Cult": {Cult: 2, Sect: 2, Blasphemy: 1, Circle: 1, Coven: 1, Idols: 1, Occultism: 1},
Heresy: {Heresy: 3, Sect: 2, Schism: 1, Dissenters: 1, Circle: 1, Brotherhood: 1, Society: 1, Iconoclasm: 1, Dissent: 1, Apostates: 1} Heresy: {Heresy: 3, Sect: 2, Apostates: 1, Brotherhood: 1, Circle: 1, Dissent: 1, Dissenters: 1, Iconoclasm: 1, Schism: 1, Society: 1}
}; };
const generate = function () { const generate = function () {

View file

@ -23,10 +23,14 @@ window.Rivers = (function () {
resolveDepressions(h); resolveDepressions(h);
drainWater(); drainWater();
defineRivers(); defineRivers();
calculateConfluenceFlux(); calculateConfluenceFlux();
Lakes.cleanupLakeData(); Lakes.cleanupLakeData();
if (allowErosion) cells.h = Uint8Array.from(h); // apply changed heights as basic one if (allowErosion) {
cells.h = Uint8Array.from(h); // apply gradient
downcutRivers(); // downcut river beds
}
TIME && console.timeEnd("generateRivers"); TIME && console.timeEnd("generateRivers");
@ -34,6 +38,8 @@ window.Rivers = (function () {
const pixel2 = distanceScale * distanceScale const pixel2 = distanceScale * distanceScale
//const MIN_FLUX_TO_FORM_RIVER = 10 * distanceScale; //const MIN_FLUX_TO_FORM_RIVER = 10 * distanceScale;
const MIN_FLUX_TO_FORM_RIVER = 30; const MIN_FLUX_TO_FORM_RIVER = 30;
const cellsNumberModifier = (pointsInput.dataset.cells / 10000) ** 0.25;
const prec = grid.cells.prec; const prec = grid.cells.prec;
// const area = c => pack.cells.area[c] * pixel2; // const area = c => pack.cells.area[c] * pixel2;
const area = pack.cells.area; const area = pack.cells.area;
@ -41,7 +47,7 @@ window.Rivers = (function () {
const lakeOutCells = Lakes.setClimateData(h); const lakeOutCells = Lakes.setClimateData(h);
land.forEach(function (i) { land.forEach(function (i) {
cells.fl[i] += (prec[cells.g[i]] * area[i]) / 100; // add flux from precipitation cells.fl[i] += prec[cells.g[i]] / cellsNumberModifier; // add flux from precipitation
// create lake outlet if lake is not in deep depression and flux > evaporation // create lake outlet if lake is not in deep depression and flux > evaporation
const lakes = lakeOutCells[i] ? features.filter(feature => i === feature.outCell && feature.flux > feature.evaporation) : []; const lakes = lakeOutCells[i] ? features.filter(feature => i === feature.outCell && feature.flux > feature.evaporation) : [];
@ -93,6 +99,15 @@ window.Rivers = (function () {
// cells is depressed // cells is depressed
if (h[i] <= h[min]) return; if (h[i] <= h[min]) return;
// debug
// .append("line")
// .attr("x1", pack.cells.p[i][0])
// .attr("y1", pack.cells.p[i][1])
// .attr("x2", pack.cells.p[min][0])
// .attr("y2", pack.cells.p[min][1])
// .attr("stroke", "#333")
// .attr("stroke-width", 0.2);
if (cells.fl[i] < MIN_FLUX_TO_FORM_RIVER) { if (cells.fl[i] < MIN_FLUX_TO_FORM_RIVER) {
// flux is too small to operate as a river // flux is too small to operate as a river
if (h[min] >= 20) cells.fl[min] += cells.fl[i]; if (h[min] >= 20) cells.fl[min] += cells.fl[i];
@ -152,6 +167,9 @@ window.Rivers = (function () {
cells.conf = new Uint16Array(cells.i.length); cells.conf = new Uint16Array(cells.i.length);
pack.rivers = []; pack.rivers = [];
const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2);
const mainStemWidthFactor = defaultWidthFactor * 1.2;
for (const key in riversData) { for (const key in riversData) {
const riverCells = riversData[key]; const riverCells = riversData[key];
if (riverCells.length < 3) continue; // exclude tiny rivers if (riverCells.length < 3) continue; // exclude tiny rivers
@ -169,7 +187,7 @@ window.Rivers = (function () {
const mouth = riverCells[riverCells.length - 2]; const mouth = riverCells[riverCells.length - 2];
const parent = riverParents[key] || 0; const parent = riverParents[key] || 0;
const widthFactor = (!parent || parent === riverId ? 1.2 : 1); const widthFactor = !parent || parent === riverId ? mainStemWidthFactor : defaultWidthFactor;
const meanderedPoints = addMeandering(riverCells); const meanderedPoints = addMeandering(riverCells);
const discharge = cells.fl[mouth]; // m3 in second const discharge = cells.fl[mouth]; // m3 in second
const length = getApproximateLength(meanderedPoints); const length = getApproximateLength(meanderedPoints);
@ -179,6 +197,22 @@ window.Rivers = (function () {
} }
} }
function downcutRivers() {
const MAX_DOWNCUT = 5;
for (const i of pack.cells.i) {
if (cells.h[i] < 35) continue; // don't donwcut lowlands
if (!cells.fl[i]) continue;
const higherCells = cells.c[i].filter(c => cells.h[c] > cells.h[i]);
const higherFlux = higherCells.reduce((acc, c) => acc + cells.fl[c], 0) / higherCells.length;
if (!higherFlux) continue;
const downcut = Math.floor(cells.fl[i] / higherFlux);
if (downcut) cells.h[i] -= Math.min(downcut, MAX_DOWNCUT);
}
}
function calculateConfluenceFlux() { function calculateConfluenceFlux() {
for (const i of cells.i) { for (const i of cells.i) {
if (!cells.conf[i]) continue; if (!cells.conf[i]) continue;
@ -347,14 +381,14 @@ window.Rivers = (function () {
const LENGTH_PROGRESSION = [1, 1, 2, 3, 5, 8, 13, 21, 34].map(n => n / LENGTH_FACTOR); const LENGTH_PROGRESSION = [1, 1, 2, 3, 5, 8, 13, 21, 34].map(n => n / LENGTH_FACTOR);
const MAX_PROGRESSION = last(LENGTH_PROGRESSION); const MAX_PROGRESSION = last(LENGTH_PROGRESSION);
const getOffset = (flux, pointNumber, widthFactor = 1, startingWidth = 0) => { const getOffset = (flux, pointNumber, widthFactor, startingWidth = 0) => {
const fluxWidth = Math.min(flux ** 0.9 / FLUX_FACTOR, MAX_FLUX_WIDTH); const fluxWidth = Math.min(flux ** 0.9 / FLUX_FACTOR, MAX_FLUX_WIDTH);
const lengthWidth = pointNumber * STEP_WIDTH + (LENGTH_PROGRESSION[pointNumber] || MAX_PROGRESSION); const lengthWidth = pointNumber * STEP_WIDTH + (LENGTH_PROGRESSION[pointNumber] || MAX_PROGRESSION);
return widthFactor * (lengthWidth + fluxWidth) + startingWidth; return widthFactor * (lengthWidth + fluxWidth) + startingWidth;
}; };
// build polygon from a list of points and calculated offset (width) // build polygon from a list of points and calculated offset (width)
const getRiverPath = function (points, widthFactor = 1, startingWidth = 0) { const getRiverPath = function (points, widthFactor, startingWidth = 0) {
const riverPointsLeft = []; const riverPointsLeft = [];
const riverPointsRight = []; const riverPointsRight = [];
@ -447,5 +481,20 @@ window.Rivers = (function () {
return getBasin(parent); return getBasin(parent);
}; };
return {generate, alterHeights, resolveDepressions, addMeandering, getRiverPath, specify, getName, getType, getBasin, getWidth, getOffset, getApproximateLength, getRiverPoints, remove}; return {
generate,
alterHeights,
resolveDepressions,
addMeandering,
getRiverPath,
specify,
getName,
getType,
getBasin,
getWidth,
getOffset,
getApproximateLength,
getRiverPoints,
remove
};
})(); })();

View file

@ -37,6 +37,7 @@ function editBurg(id) {
burgBody.querySelectorAll(".burgFeature").forEach(el => el.addEventListener("click", toggleFeature)); burgBody.querySelectorAll(".burgFeature").forEach(el => el.addEventListener("click", toggleFeature));
document.getElementById("mfcgBurgSeed").addEventListener("change", changeSeed); document.getElementById("mfcgBurgSeed").addEventListener("change", changeSeed);
document.getElementById("regenerateMFCGBurgSeed").addEventListener("click", randomizeSeed); document.getElementById("regenerateMFCGBurgSeed").addEventListener("click", randomizeSeed);
document.getElementById("addCustomMFCGBurgLink").addEventListener("click", addCustomMfcgLink);
document.getElementById("burgStyleShow").addEventListener("click", showStyleSection); document.getElementById("burgStyleShow").addEventListener("click", showStyleSection);
document.getElementById("burgStyleHide").addEventListener("click", hideStyleSection); document.getElementById("burgStyleHide").addEventListener("click", hideStyleSection);
@ -112,7 +113,13 @@ function editBurg(id) {
if (options.showMFCGMap) { if (options.showMFCGMap) {
document.getElementById("mfcgPreviewSection").style.display = "block"; document.getElementById("mfcgPreviewSection").style.display = "block";
updateMFCGFrame(b); updateMFCGFrame(b);
if (b.link) {
document.getElementById("mfcgBurgSeedSection").style.display = "none";
} else {
document.getElementById("mfcgBurgSeedSection").style.display = "inline-block";
document.getElementById("mfcgBurgSeed").value = getBurgSeed(b); document.getElementById("mfcgBurgSeed").value = getBurgSeed(b);
}
} else { } else {
document.getElementById("mfcgPreviewSection").style.display = "none"; document.getElementById("mfcgPreviewSection").style.display = "none";
} }
@ -347,22 +354,25 @@ function editBurg(id) {
function toggleFeature() { function toggleFeature() {
const id = +elSelected.attr("data-id"); const id = +elSelected.attr("data-id");
const b = pack.burgs[id]; const burg = pack.burgs[id];
const feature = this.dataset.feature; const feature = this.dataset.feature;
const turnOn = this.classList.contains("inactive"); const turnOn = this.classList.contains("inactive");
if (feature === "port") togglePort(id); if (feature === "port") togglePort(id);
else if (feature === "capital") toggleCapital(id); else if (feature === "capital") toggleCapital(id);
else b[feature] = +turnOn; else burg[feature] = +turnOn;
if (b[feature]) this.classList.remove("inactive"); if (burg[feature]) this.classList.remove("inactive");
else if (!b[feature]) this.classList.add("inactive"); else if (!burg[feature]) this.classList.add("inactive");
if (b.port) document.getElementById("burgEditAnchorStyle").style.display = "inline-block"; if (burg.port) document.getElementById("burgEditAnchorStyle").style.display = "inline-block";
else document.getElementById("burgEditAnchorStyle").style.display = "none"; else document.getElementById("burgEditAnchorStyle").style.display = "none";
updateMFCGFrame(burg);
} }
function toggleBurgLockButton() { function toggleBurgLockButton() {
const id = +elSelected.attr("data-id"); const id = +elSelected.attr("data-id");
toggleBurgLock(id); const burg = pack.burgs[id];
burg.lock = !burg.lock;
updateBurgLockIcon(); updateBurgLockIcon();
} }
@ -405,7 +415,7 @@ function editBurg(id) {
function updateMFCGFrame(burg) { function updateMFCGFrame(burg) {
const mfcgURL = getMFCGlink(burg); const mfcgURL = getMFCGlink(burg);
document.getElementById("mfcgPreview").setAttribute("src", mfcgURL); document.getElementById("mfcgPreview").setAttribute("src", mfcgURL + "&preview=1");
document.getElementById("mfcgLink").setAttribute("href", mfcgURL); document.getElementById("mfcgLink").setAttribute("href", mfcgURL);
} }
@ -426,6 +436,17 @@ function editBurg(id) {
document.getElementById("mfcgBurgSeed").value = burgSeed; document.getElementById("mfcgBurgSeed").value = burgSeed;
} }
function addCustomMfcgLink() {
const id = +elSelected.attr("data-id");
const burg = pack.burgs[id];
const message = "Enter custom link to the burg map. It can be a link to Medieval Fantasy City Generator or other tool. Keep empty to use MFCG seed";
prompt(message, {default: burg.link || "", required: false}, link => {
if (link) burg.link = link;
else delete burg.link;
updateMFCGFrame(burg);
});
}
function openEmblemEdit() { function openEmblemEdit() {
const id = +elSelected.attr("data-id"), const id = +elSelected.attr("data-id"),
burg = pack.burgs[id]; burg = pack.burgs[id];

View file

@ -7,6 +7,7 @@ function overviewBurgs() {
const body = document.getElementById("burgsBody"); const body = document.getElementById("burgsBody");
updateFilter(); updateFilter();
updateLockAllIcon();
burgsOverviewAddLines(); burgsOverviewAddLines();
$("#burgsOverview").dialog(); $("#burgsOverview").dialog();
@ -33,6 +34,7 @@ function overviewBurgs() {
document.getElementById("burgsListToLoad").addEventListener("change", function () { document.getElementById("burgsListToLoad").addEventListener("change", function () {
uploadFile(this, importBurgNames); uploadFile(this, importBurgNames);
}); });
document.getElementById("burgsLockAll").addEventListener("click", toggleLockAll);
document.getElementById("burgsRemoveAll").addEventListener("click", triggerAllBurgsRemove); document.getElementById("burgsRemoveAll").addEventListener("click", triggerAllBurgsRemove);
document.getElementById("burgsInvertLock").addEventListener("click", invertLock); document.getElementById("burgsInvertLock").addEventListener("click", invertLock);
@ -87,7 +89,7 @@ function overviewBurgs() {
<input data-tip="Burg name. Click and type to change" class="burgName" value="${b.name}" autocorrect="off" spellcheck="false"> <input data-tip="Burg name. Click and type to change" class="burgName" value="${b.name}" autocorrect="off" spellcheck="false">
<input data-tip="Burg province" class="burgState" value="${province}" disabled> <input data-tip="Burg province" class="burgState" value="${province}" disabled>
<input data-tip="Burg state" class="burgState" value="${state}" disabled> <input data-tip="Burg state" class="burgState" value="${state}" disabled>
<select data-tip="Dominant culture. Click to change burg culture (to change cell cultrure use Cultures Editor)" class="stateCulture">${getCultureOptions( <select data-tip="Dominant culture. Click to change burg culture (to change cell culture use Cultures Editor)" class="stateCulture">${getCultureOptions(
b.culture b.culture
)}</select> )}</select>
<span data-tip="Burg population" class="icon-male"></span> <span data-tip="Burg population" class="icon-male"></span>
@ -195,8 +197,11 @@ function overviewBurgs() {
} }
function toggleBurgLockStatus() { function toggleBurgLockStatus() {
const burg = +this.parentNode.dataset.id; const burgId = +this.parentNode.dataset.id;
toggleBurgLock(burg);
const burg = pack.burgs[burgId];
burg.lock = !burg.lock;
if (this.classList.contains("icon-lock")) { if (this.classList.contains("icon-lock")) {
this.classList.remove("icon-lock"); this.classList.remove("icon-lock");
this.classList.add("icon-lock-open"); this.classList.add("icon-lock-open");
@ -478,9 +483,9 @@ function overviewBurgs() {
} }
function renameBurgsInBulk() { function renameBurgsInBulk() {
const message = `Download burgs list as a text file, make changes and re-upload the file. alertMessage.innerHTML = `Download burgs list as a text file, make changes and re-upload the file.
Make sure the file is a plain text document with each name on its own line (the dilimiter is CRLF).
If you do not want to change the name, just leave it as is`; If you do not want to change the name, just leave it as is`;
alertMessage.innerHTML = message;
$("#alert").dialog({ $("#alert").dialog({
title: "Burgs bulk renaming", title: "Burgs bulk renaming",
@ -562,4 +567,21 @@ function overviewBurgs() {
pack.burgs = pack.burgs.map(burg => ({...burg, lock: !burg.lock})); pack.burgs = pack.burgs.map(burg => ({...burg, lock: !burg.lock}));
burgsOverviewAddLines(); burgsOverviewAddLines();
} }
function toggleLockAll() {
const activeBurgs = pack.burgs.filter(b => b.i && !b.removed);
const allLocked = activeBurgs.every(burg => burg.lock);
pack.burgs.forEach(burg => {
burg.lock = !allLocked;
});
burgsOverviewAddLines();
document.getElementById("burgsLockAll").className = allLocked ? "icon-lock" : "icon-lock-open";
}
function updateLockAllIcon() {
const allLocked = pack.burgs.every(({lock, i, removed}) => lock || !i || removed);
document.getElementById("burgsLockAll").className = allLocked ? "icon-lock-open" : "icon-lock";
}
} }

View file

@ -1,10 +1,9 @@
"use strict"; "use strict";
function editDiplomacy() { function editDiplomacy() {
if (customization) return; if (customization) return;
if (pack.states.filter(s => s.i && !s.removed).length < 2) { if (pack.states.filter(s => s.i && !s.removed).length < 2) return tip("There should be at least 2 states to edit the diplomacy", false, "error");
tip("There should be at least 2 states to edit the diplomacy", false, "error");
return; const body = document.getElementById("diplomacyBodySection");
}
closeDialogs("#diplomacyEditor, .stable"); closeDialogs("#diplomacyEditor, .stable");
if (!layerIsOn("toggleStates")) toggleStates(); if (!layerIsOn("toggleStates")) toggleStates();
@ -14,21 +13,29 @@ function editDiplomacy() {
if (layerIsOn("toggleBiomes")) toggleBiomes(); if (layerIsOn("toggleBiomes")) toggleBiomes();
if (layerIsOn("toggleReligions")) toggleReligions(); if (layerIsOn("toggleReligions")) toggleReligions();
const body = document.getElementById("diplomacyBodySection"); const relations = {
const statuses = ["Ally", "Friendly", "Neutral", "Suspicion", "Enemy", "Unknown", "Rival", "Vassal", "Suzerain"]; Ally: {inText: "is an ally of", color: "#00b300", tip: "Allies formed a defensive pact and protect each other in case of third party aggression"},
const description = [" is an ally of ", " is friendly to ", " is neutral to ", " is suspicious of ", Friendly: {inText: "is friendly to", color: "#d4f8aa", tip: "State is friendly to anouther state when they share some common interests"},
" is at war with ", " does not know about ", " is a rival of ", " is a vassal of ", " is suzerain to "]; Neutral: {inText: "is neutral to", color: "#edeee8", tip: "Neutral means states relations are neither positive nor negative"},
const colors = ["#00b300", "#d4f8aa", "#edeee8", "#eeafaa", "#e64b40", "#a9a9a9", "#ad5a1f", "#87CEFA", "#00008B"]; Suspicion: {inText: "is suspicious of", color: "#eeafaa", tip: "Suspicion means state has a cautious distrust of another state"},
refreshDiplomacyEditor(); Enemy: {inText: "is at war with", color: "#e64b40", tip: "Enemies are states at war with each other"},
Unknown: {inText: "does not know about", color: "#a9a9a9", tip: "Relations are unknown if states do not have enough information about each other"},
Rival: {inText: "is a rival of", color: "#ad5a1f", tip: "Rivalry is a state of competing for dominance in the region"},
Vassal: {inText: "is a vassal of", color: "#87CEFA", tip: "Vassal is a state having obligation to its suzerain"},
Suzerain: {inText: "is suzerain to", color: "#00008B", tip: "Suzerain is a state having some control over its vassals"}
};
tip("Click on a state to see its diplomatic relations", false, "warning"); refreshDiplomacyEditor();
viewbox.style("cursor", "crosshair").on("click", selectStateOnMapClick); viewbox.style("cursor", "crosshair").on("click", selectStateOnMapClick);
if (modules.editDiplomacy) return; if (modules.editDiplomacy) return;
modules.editDiplomacy = true; modules.editDiplomacy = true;
$("#diplomacyEditor").dialog({ $("#diplomacyEditor").dialog({
title: "Diplomacy Editor", resizable: false, width: fitContent(), close: closeDiplomacyEditor, title: "Diplomacy Editor",
resizable: false,
width: fitContent(),
close: closeDiplomacyEditor,
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"} position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
}); });
@ -36,10 +43,30 @@ function editDiplomacy() {
document.getElementById("diplomacyEditorRefresh").addEventListener("click", refreshDiplomacyEditor); document.getElementById("diplomacyEditorRefresh").addEventListener("click", refreshDiplomacyEditor);
document.getElementById("diplomacyEditStyle").addEventListener("click", () => editStyle("regions")); document.getElementById("diplomacyEditStyle").addEventListener("click", () => editStyle("regions"));
document.getElementById("diplomacyRegenerate").addEventListener("click", regenerateRelations); document.getElementById("diplomacyRegenerate").addEventListener("click", regenerateRelations);
document.getElementById("diplomacyMatrix").addEventListener("click", showRelationsMatrix); document.getElementById("diplomacyReset").addEventListener("click", resetRelations);
document.getElementById("diplomacyShowMatrix").addEventListener("click", showRelationsMatrix);
document.getElementById("diplomacyHistory").addEventListener("click", showRelationsHistory); document.getElementById("diplomacyHistory").addEventListener("click", showRelationsHistory);
document.getElementById("diplomacyExport").addEventListener("click", downloadDiplomacyData); document.getElementById("diplomacyExport").addEventListener("click", downloadDiplomacyData);
document.getElementById("diplomacySelect").addEventListener("mouseup", diplomacyChangeRelations);
body.addEventListener("click", function (ev) {
const el = ev.target;
if (el.parentElement.classList.contains("Self")) return;
if (el.classList.contains("changeRelations")) {
const line = el.parentElement;
const subjectId = +line.dataset.id;
const objectId = +body.querySelector("div.Self").dataset.id;
const currentRelation = line.dataset.relations;
selectRelation(subjectId, objectId, currentRelation);
return;
}
// select state of clicked line
body.querySelector("div.Self").classList.remove("Self");
el.parentElement.classList.add("Self");
refreshDiplomacyEditor();
});
function refreshDiplomacyEditor() { function refreshDiplomacyEditor() {
diplomacyEditorAddLines(); diplomacyEditorAddLines();
@ -50,33 +77,36 @@ function editDiplomacy() {
function diplomacyEditorAddLines() { function diplomacyEditorAddLines() {
const states = pack.states; const states = pack.states;
const selectedLine = body.querySelector("div.Self"); const selectedLine = body.querySelector("div.Self");
const sel = selectedLine ? +selectedLine.dataset.id : states.find(s => s.i && !s.removed).i; const selectedId = selectedLine ? +selectedLine.dataset.id : states.find(s => s.i && !s.removed).i;
const selName = states[sel].fullName; const selectedName = states[selectedId].name;
diplomacySelect.style.display = "none";
COArenderer.trigger("stateCOA"+sel, states[sel].coa); COArenderer.trigger("stateCOA" + selectedId, states[selectedId].coa);
let lines = `<div class="states Self" data-id=${sel} data-tip="List below shows relations to ${selName}"> let lines = `<div class="states Self" data-id=${selectedId} data-tip="List below shows relations to ${selectedName}">
<div style="width: max-content">${selName}</div> <div style="width: max-content">${states[selectedId].fullName}</div>
<svg class="coaIcon" viewBox="0 0 200 200"><use href="#stateCOA${sel}"></use></svg> <svg class="coaIcon" viewBox="0 0 200 200"><use href="#stateCOA${selectedId}"></use></svg>
</div>`; </div>`;
for (const s of states) { for (const state of states) {
if (!s.i || s.removed || s.i === sel) continue; if (!state.i || state.removed || state.i === selectedId) continue;
const relation = s.diplomacy[sel]; const relation = state.diplomacy[selectedId];
const index = statuses.indexOf(relation); const {color, inText} = relations[relation];
const color = colors[index];
const tip = s.fullName + description[index] + selName;
const tipSelect = `${tip}. Click to see relations to ${s.name}`;
const tipChange = `${tip}. Click to change relations to ${selName}`;
COArenderer.trigger("stateCOA"+s.i, s.coa);
lines += `<div class="states" data-id=${s.i} data-name="${s.fullName}" data-relations="${relation}"> const tip = `${state.name} ${inText} ${selectedName}`;
<svg data-tip="${tipSelect}" class="coaIcon" viewBox="0 0 200 200"><use href="#stateCOA${s.i}"></use></svg> const tipSelect = `${tip}. Click to see relations to ${state.name}`;
<div data-tip="${tipSelect}" style="width:12em">${s.fullName}</div> const tipChange = `Click to change relations. ${tip}`;
<svg data-tip="${tipChange}" width=".9em" height=".9em" style="margin-bottom:-1px" class="changeRelations">
<rect x="0" y="0" width="100%" height="100%" fill="${color}" class="fillRect pointer" style="pointer-events: none"></rect> const name = state.fullName.length < 23 ? state.fullName : state.name;
COArenderer.trigger("stateCOA" + state.i, state.coa);
lines += `<div class="states" data-id=${state.i} data-name="${name}" data-relations="${relation}">
<svg data-tip="${tipSelect}" class="coaIcon" viewBox="0 0 200 200"><use href="#stateCOA${state.i}"></use></svg>
<div data-tip="${tipSelect}" style="width: 12em">${name}</div>
<div data-tip="${tipChange}" class="changeRelations pointer" style="width: 6em">
<svg width=".9em" height=".9em" style="margin-bottom:-1px">
<rect x="0" y="0" width="100%" height="100%" fill="${color}" class="fillRect"></rect>
</svg> </svg>
<input data-tip="${tipChange}" class="changeRelations diplomacyRelations" value="${relation}" readonly/> ${relation}
</div>
</div>`; </div>`;
} }
body.innerHTML = lines; body.innerHTML = lines;
@ -84,8 +114,6 @@ function editDiplomacy() {
// add listeners // add listeners
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => stateHighlightOn(ev))); body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => stateHighlightOn(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => stateHighlightOff(ev))); body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => stateHighlightOff(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("click", selectStateOnLineClick));
body.querySelectorAll(".changeRelations").forEach(el => el.addEventListener("click", toggleDiplomacySelect));
applySorting(diplomacyHeader); applySorting(diplomacyHeader);
$("#diplomacyEditor").dialog(); $("#diplomacyEditor").dialog();
@ -95,19 +123,31 @@ function editDiplomacy() {
if (!layerIsOn("toggleStates")) return; if (!layerIsOn("toggleStates")) return;
const state = +event.target.dataset.id; const state = +event.target.dataset.id;
if (customization || !state) return; if (customization || !state) return;
const d = regions.select("#state"+state).attr("d"); const d = regions.select("#state" + state).attr("d");
const path = debug.append("path").attr("class", "highlight").attr("d", d) const path = debug
.attr("fill", "none").attr("stroke", "red").attr("stroke-width", 1).attr("opacity", 1) .append("path")
.attr("class", "highlight")
.attr("d", d)
.attr("fill", "none")
.attr("stroke", "red")
.attr("stroke-width", 1)
.attr("opacity", 1)
.attr("filter", "url(#blur1)"); .attr("filter", "url(#blur1)");
const l = path.node().getTotalLength(), dur = (l + 5000) / 2; const l = path.node().getTotalLength(),
dur = (l + 5000) / 2;
const i = d3.interpolateString("0," + l, l + "," + l); const i = d3.interpolateString("0," + l, l + "," + l);
path.transition().duration(dur).attrTween("stroke-dasharray", function() {return t => i(t)}); path
.transition()
.duration(dur)
.attrTween("stroke-dasharray", function () {
return t => i(t);
});
} }
function stateHighlightOff(event) { function stateHighlightOff(event) {
debug.selectAll(".highlight").each(function() { debug.selectAll(".highlight").each(function () {
d3.select(this).transition().duration(1000).attr("opacity", 0).remove(); d3.select(this).transition().duration(1000).attr("opacity", 0).remove();
}); });
} }
@ -118,22 +158,17 @@ function editDiplomacy() {
if (!sel) return; if (!sel) return;
if (!layerIsOn("toggleStates")) toggleStates(); if (!layerIsOn("toggleStates")) toggleStates();
statesBody.selectAll("path").each(function() { statesBody.selectAll("path").each(function () {
if (this.id.slice(0, 9) === "state-gap") return; // exclude state gap element if (this.id.slice(0, 9) === "state-gap") return; // exclude state gap element
const id = +this.id.slice(5); // state id const id = +this.id.slice(5); // state id
const index = statuses.indexOf(pack.states[id].diplomacy[sel]); // status index
const clr = index !== -1 ? colors[index] : "#4682b4"; // Self (bluish)
this.setAttribute("fill", clr);
statesBody.select("#state-gap"+id).attr("stroke", clr);
statesHalo.select("#state-border"+id).attr("stroke", d3.color(clr).darker().hex());
});
}
function selectStateOnLineClick() { const relation = pack.states[id].diplomacy[sel];
if (this.classList.contains("Self")) return; const color = relations[relation]?.color || "#4682b4";
body.querySelector("div.Self").classList.remove("Self");
this.classList.add("Self"); this.setAttribute("fill", color);
refreshDiplomacyEditor(); statesBody.select("#state-gap" + id).attr("stroke", color);
statesHalo.select("#state-border" + id).attr("stroke", d3.color(color).darker().hex());
});
} }
function selectStateOnMapClick() { function selectStateOnMapClick() {
@ -145,42 +180,63 @@ function editDiplomacy() {
if (+selectedLine.dataset.id === state) return; if (+selectedLine.dataset.id === state) return;
selectedLine.classList.remove("Self"); selectedLine.classList.remove("Self");
body.querySelector("div[data-id='"+state+"']").classList.add("Self"); body.querySelector("div[data-id='" + state + "']").classList.add("Self");
refreshDiplomacyEditor(); refreshDiplomacyEditor();
} }
function toggleDiplomacySelect(event) { function selectRelation(subjectId, objectId, currentRelation) {
event.stopPropagation(); const states = pack.states;
const select = document.getElementById("diplomacySelect");
const show = select.style.display === "none"; const subject = states[subjectId];
if (!show) {select.style.display = "none"; return;} const header = `<div style="margin-bottom: 0.3em"><svg class="coaIcon" viewBox="0 0 200 200"><use href="#stateCOA${subject.i}"></use></svg> <b>${subject.fullName}</b></div>`;
select.style.display = "block";
const input = event.target.closest("div").querySelector("input"); const options = Object.entries(relations)
select.style.left = input.getBoundingClientRect().left + "px"; .map(
select.style.top = input.getBoundingClientRect().bottom + "px"; ([relation, {color, inText, tip}]) =>
body.dataset.state = event.target.closest("div.states").dataset.id; `<div style="margin-block: 0.2em" data-tip="${tip}"><label class="pointer">
<input type="radio" name="relationSelect" value="${relation}" ${currentRelation === relation && "checked"} >
<svg width=".9em" height=".9em" style="margin-bottom:-1px">
<rect x="0" y="0" width="100%" height="100%" fill="${color}" class="fillRect" />
</svg>
${inText}
</label></div>`
)
.join("");
const object = states[objectId];
const footer = `<div style="margin-top: 0.7em"><svg class="coaIcon" viewBox="0 0 200 200"><use href="#stateCOA${object.i}"></use></svg> <b>${object.fullName}</b></div>`;
alertMessage.innerHTML = `<div style="overflow: hidden">${header} ${options} ${footer}</div>`;
$("#alert").dialog({
width: fitContent(),
title: `Change relations`,
buttons: {
Apply: function () {
const newRelation = document.querySelector('input[name="relationSelect"]:checked')?.value;
changeRelation(subjectId, objectId, currentRelation, newRelation);
$(this).dialog("close");
},
Cancel: function () {
$(this).dialog("close");
}
}
});
} }
function diplomacyChangeRelations(event) { function changeRelation(subjectId, objectId, oldRelation, newRelation) {
event.stopPropagation(); if (newRelation === oldRelation) return;
diplomacySelect.style.display = "none"; const states = pack.states;
const subject = +body.dataset.state; const chronicle = states[0].diplomacy;
const rel = event.target.innerHTML;
const states = pack.states, chronicle = states[0].diplomacy; const subjectName = states[subjectId].name;
const selectedLine = body.querySelector("div.Self"); const objectName = states[objectId].name;
const object = selectedLine ? +selectedLine.dataset.id : states.find(s => s.i && !s.removed).i;
if (!object) return;
const objectName = states[object].name; // object of relations change
const subjectName = states[subject].name; // subject of relations change - actor
const oldRel = states[subject].diplomacy[object]; states[subjectId].diplomacy[objectId] = newRelation;
if (rel === oldRel) return; states[objectId].diplomacy[subjectId] = newRelation === "Vassal" ? "Suzerain" : newRelation === "Suzerain" ? "Vassal" : newRelation;
states[subject].diplomacy[object] = rel;
states[object].diplomacy[subject] = rel === "Vassal" ? "Suzerain" : rel === "Suzerain" ? "Vassal" : rel;
// update relation history // update relation history
const change = () => [`Relations change`, `${subjectName}-${getAdjective(objectName)} relations changed to ${rel.toLowerCase()}`]; const change = () => [`Relations change`, `${subjectName}-${getAdjective(objectName)} relations changed to ${newRelation.toLowerCase()}`];
const ally = () => [`Defence pact`, `${subjectName} entered into defensive pact with ${objectName}`]; const ally = () => [`Defence pact`, `${subjectName} entered into defensive pact with ${objectName}`];
const vassal = () => [`Vassalization`, `${subjectName} became a vassal of ${objectName}`]; const vassal = () => [`Vassalization`, `${subjectName} became a vassal of ${objectName}`];
const suzerain = () => [`Vassalization`, `${subjectName} vassalized ${objectName}`]; const suzerain = () => [`Vassalization`, `${subjectName} vassalized ${objectName}`];
@ -189,24 +245,33 @@ function editDiplomacy() {
const war = () => [`War declaration`, `${subjectName} declared a war on its enemy ${objectName}`]; const war = () => [`War declaration`, `${subjectName} declared a war on its enemy ${objectName}`];
const peace = () => { const peace = () => {
const treaty = `${subjectName} and ${objectName} agreed to cease fire and signed a peace treaty`; const treaty = `${subjectName} and ${objectName} agreed to cease fire and signed a peace treaty`;
const changed = rel === "Ally" ? ally() const changed =
: rel === "Vassal" ? vassal() newRelation === "Ally"
: rel === "Suzerain" ? suzerain() ? ally()
: rel === "Unknown" ? unknown() : newRelation === "Vassal"
? vassal()
: newRelation === "Suzerain"
? suzerain()
: newRelation === "Unknown"
? unknown()
: change(); : change();
return [`War termination`, treaty, changed[1]]; return [`War termination`, treaty, changed[1]];
} };
if (oldRel === "Enemy") chronicle.push(peace()); else if (oldRelation === "Enemy") chronicle.push(peace());
if (rel === "Enemy") chronicle.push(war()); else else if (newRelation === "Enemy") chronicle.push(war());
if (rel === "Vassal") chronicle.push(vassal()); else else if (newRelation === "Vassal") chronicle.push(vassal());
if (rel === "Suzerain") chronicle.push(suzerain()); else else if (newRelation === "Suzerain") chronicle.push(suzerain());
if (rel === "Ally") chronicle.push(ally()); else else if (newRelation === "Ally") chronicle.push(ally());
if (rel === "Unknown") chronicle.push(unknown()); else else if (newRelation === "Unknown") chronicle.push(unknown());
if (rel === "Rival") chronicle.push(rival()); else else if (newRelation === "Rival") chronicle.push(rival());
chronicle.push(change()); else chronicle.push(change());
refreshDiplomacyEditor(); refreshDiplomacyEditor();
if (diplomacyMatrix.offsetParent) {
document.getElementById("diplomacyMatrixBody").innerHTML = "";
showRelationsMatrix();
}
} }
function regenerateRelations() { function regenerateRelations() {
@ -214,28 +279,52 @@ function editDiplomacy() {
refreshDiplomacyEditor(); refreshDiplomacyEditor();
} }
function resetRelations() {
const selectedId = +body.querySelector("div.Self")?.dataset?.id;
if (!selectedId) return;
const states = pack.states;
states[selectedId].diplomacy.forEach((relations, index) => {
if (relations !== "x") {
states[selectedId].diplomacy[index] = "Neutral";
states[index].diplomacy[selectedId] = "Neutral";
}
});
refreshDiplomacyEditor();
}
function showRelationsHistory() { function showRelationsHistory() {
const chronicle = pack.states[0].diplomacy; const chronicle = pack.states[0].diplomacy;
if (!chronicle.length) {tip("Relations history is blank", false, "error"); return;} if (!chronicle.length) return tip("Relations history is blank", false, "error");
let message = `<div autocorrect="off" spellcheck="false">`; let message = `<div autocorrect="off" spellcheck="false">`;
chronicle.forEach((e, d) => { chronicle.forEach((entry, d) => {
message += `<div>`; message += `<div>`;
e.forEach((l, i) => message += `<div contenteditable="true" data-id="${d}-${i}"${i ? "" : " style='font-weight:bold'"}>${l}</div>`); entry.forEach((l, i) => {
message += `<div contenteditable="true" data-id="${d}-${i}"${i ? "" : " style='font-weight:bold'"}>${l}</div>`;
});
message += `&#8205;</div>`; message += `&#8205;</div>`;
}); });
alertMessage.innerHTML = message + `</div><i id="info-line">Type to edit. Press Enter to add a new line, empty the element to remove it</i>`; alertMessage.innerHTML = message + `</div><div class="info-line">Type to edit. Press Enter to add a new line, empty the element to remove it</div>`;
alertMessage.querySelectorAll("div[contenteditable='true']").forEach(el => el.addEventListener("input", changeReliationsHistory)); alertMessage.querySelectorAll("div[contenteditable='true']").forEach(el => el.addEventListener("input", changeReliationsHistory));
$("#alert").dialog({title: "Relations history", position: {my: "center", at: "center", of: "svg"}, $("#alert").dialog({
title: "Relations history",
position: {my: "center", at: "center", of: "svg"},
buttons: { buttons: {
Save: function() { Save: function () {
const data = this.querySelector("div").innerText.split("\n").join("\r\n"); const data = this.querySelector("div").innerText.split("\n").join("\r\n");
const name = getFileName("Relations history") + ".txt"; const name = getFileName("Relations history") + ".txt";
downloadFile(data, name); downloadFile(data, name);
}, },
Clear: function() {pack.states[0].diplomacy = []; $(this).dialog("close");}, Clear: function () {
Close: function() {$(this).dialog("close");} pack.states[0].diplomacy = [];
$(this).dialog("close");
},
Close: function () {
$(this).dialog("close");
}
} }
}); });
} }
@ -251,22 +340,48 @@ function editDiplomacy() {
function showRelationsMatrix() { function showRelationsMatrix() {
const states = pack.states.filter(s => s.i && !s.removed); const states = pack.states.filter(s => s.i && !s.removed);
const valid = states.map(s => s.i); const valid = states.map(state => state.i);
const diplomacyMatrixBody = document.getElementById("diplomacyMatrixBody");
let message = `<table class="matrix-table"><tr><th data-tip='&#8205;'></th>`; let table = `<table><thead><tr><th data-tip='&#8205;'></th>`;
message += states.map(s => `<th data-tip='See relations to ${s.fullName}'>${s.name}</th>`).join("") + `</tr>`; // headers table += states.map(state => `<th data-tip='Relations to ${state.fullName}'>${state.name}</th>`).join("") + `</tr>`;
states.forEach(s => { table += `<tbody>`;
message += `<tr><th data-tip='See relations of ${s.fullName}'>${s.name}</th>` + s.diplomacy
.filter((v, i) => valid.includes(i)).map((r, i) => { states.forEach(state => {
const desc = description[statuses.indexOf(r)]; table +=
const tip = desc ? s.fullName + desc + pack.states[valid[i]].fullName : '&#8205;'; `<tr data-id=${state.i}><th data-tip='Relations of ${state.fullName}'>${state.name}</th>` +
return `<td data-tip='${tip}' class='${r}'>${r}</td>` state.diplomacy
}).join("") + "</tr>"; .filter((v, i) => valid.includes(i))
.map((relation, index) => {
const relationObj = relations[relation];
if (!relationObj) return `<td class='${relation}'>${relation}</td>`;
const objectState = pack.states[valid[index]];
const tip = `${state.fullName} ${relationObj.inText} ${objectState.fullName}`;
return `<td data-id=${objectState.i} data-tip='${tip}' class='${relation}'>${relation}</td>`;
})
.join("") +
"</tr>";
}); });
message += `</table>`;
alertMessage.innerHTML = message;
$("#alert").dialog({title: "Relations matrix", width: fitContent(), position: {my: "center", at: "center", of: "svg"}, buttons: {}}); table += `</tbody></table>`;
diplomacyMatrixBody.innerHTML = table;
const tableEl = diplomacyMatrixBody.querySelector("table");
tableEl.addEventListener("click", function (event) {
const el = event.target;
if (el.tagName !== "TD") return;
const currentRelation = el.innerText;
if (!relations[currentRelation]) return;
const subjectId = +el.closest("tr")?.dataset?.id;
const objectId = +el?.dataset?.id;
selectRelation(subjectId, objectId, currentRelation);
});
$("#diplomacyMatrix").dialog({title: "Relations matrix", position: {my: "center", at: "center", of: "svg"}, buttons: {}});
} }
function downloadDiplomacyData() { function downloadDiplomacyData() {
@ -288,7 +403,8 @@ function editDiplomacy() {
clearMainTip(); clearMainTip();
const selected = body.querySelector("div.Self"); const selected = body.querySelector("div.Self");
if (selected) selected.classList.remove("Self"); if (selected) selected.classList.remove("Self");
if (layerIsOn("toggleStates")) drawStates(); else toggleStates(); if (layerIsOn("toggleStates")) drawStates();
else toggleStates();
debug.selectAll(".highlight").remove(); debug.selectAll(".highlight").remove();
} }
} }

View file

@ -265,41 +265,48 @@ function getBurgSeed(burg) {
} }
function getMFCGlink(burg) { function getMFCGlink(burg) {
if (burg.link) return burg.link;
const {cells} = pack; const {cells} = pack;
const {name, population, cell} = burg; const {i, name, population: burgPopulation, cell} = burg;
const burgSeed = getBurgSeed(burg); const seed = getBurgSeed(burg);
const sizeRaw = 2.13 * Math.pow((population * populationRate) / urbanDensity, 0.385);
const sizeRaw = 2.13 * Math.pow((burgPopulation * populationRate) / urbanDensity, 0.385);
const size = minmax(Math.ceil(sizeRaw), 6, 100); const size = minmax(Math.ceil(sizeRaw), 6, 100);
const people = rn(population * populationRate * urbanization); const population = rn(burgPopulation * populationRate * urbanization);
const river = cells.r[cell] ? 1 : 0;
const coast = Number(burg.port > 0);
const sea = coast && cells.haven[cell] ? getSeaDirections(cell) : null;
const biome = cells.biome[cell];
const arableBiomes = river ? [1, 2, 3, 4, 5, 6, 7, 8] : [5, 6, 7, 8];
const farms = +arableBiomes.includes(biome);
const citadel = +burg.citadel;
const urban_castle = +(citadel && each(2)(i));
const hub = +cells.road[cell] > 50; const hub = +cells.road[cell] > 50;
const river = cells.r[cell] ? 1 : 0;
const coast = +burg.port;
const citadel = +burg.citadel;
const walls = +burg.walls; const walls = +burg.walls;
const plaza = +burg.plaza; const plaza = +burg.plaza;
const temple = +burg.temple; const temple = +burg.temple;
const shanty = +burg.shanty; const shantytown = +burg.shanty;
const sea = coast && cells.haven[cell] ? getSeaDirections(cell) : "";
function getSeaDirections(i) { function getSeaDirections(i) {
const p1 = cells.p[i]; const p1 = cells.p[i];
const p2 = cells.p[cells.haven[i]]; const p2 = cells.p[cells.haven[i]];
let deg = (Math.atan2(p2[1] - p1[1], p2[0] - p1[0]) * 180) / Math.PI - 90; let deg = (Math.atan2(p2[1] - p1[1], p2[0] - p1[0]) * 180) / Math.PI - 90;
if (deg < 0) deg += 360; 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 rn(normalize(deg, 0, 360) * 2, 2); // 0 = south, 0.5 = west, 1 = north, 1.5 = east
return "&sea=" + norm;
} }
const baseURL = "https://watabou.github.io/city-generator/?random=0&continuous=0"; const parameters = {name, population, size, seed, river, coast, farms, citadel, urban_castle, hub, plaza, temple, walls, shantytown, gates: -1};
const url = `${baseURL}&name=${name}&population=${people}&size=${size}&seed=${burgSeed}&hub=${hub}&river=${river}&coast=${coast}&citadel=${citadel}&plaza=${plaza}&temple=${temple}&walls=${walls}&shantytown=${shanty}${sea}`; const url = new URL("https://watabou.github.io/city-generator");
return url; url.search = new URLSearchParams(parameters);
} if (sea) url.searchParams.append("sea", sea);
function toggleBurgLock(burg) { return url.toString();
const b = pack.burgs[burg];
b.lock = b.lock ? 0 : 1;
} }
// draw legend box // draw legend box

View file

@ -460,32 +460,31 @@ function showInfo() {
const Discord = link("https://discordapp.com/invite/X7E84HU", "Discord"); const Discord = link("https://discordapp.com/invite/X7E84HU", "Discord");
const Reddit = link("https://www.reddit.com/r/FantasyMapGenerator", "Reddit"); const Reddit = link("https://www.reddit.com/r/FantasyMapGenerator", "Reddit");
const Patreon = link("https://www.patreon.com/azgaar", "Patreon"); const Patreon = link("https://www.patreon.com/azgaar", "Patreon");
const Trello = link("https://trello.com/b/7x832DG4/fantasy-map-generator", "Trello");
const Armoria = link("https://azgaar.github.io/Armoria", "Armoria"); const Armoria = link("https://azgaar.github.io/Armoria", "Armoria");
const QuickStart = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Quick-Start-Tutorial", "Quick start tutorial"); const QuickStart = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Quick-Start-Tutorial", "Quick start tutorial");
const QAA = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Q&A", "Q&A page"); const QAA = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Q&A", "Q&A page");
const VideoTutorial = link("https://youtube.com/playlist?list=PLtgiuDC8iVR2gIG8zMTRn7T_L0arl9h1C", "Video tutorial");
alertMessage.innerHTML = ` alertMessage.innerHTML = `
<b>Fantasy Map Generator</b> (FMG) is an open-source application, it means the code is published an anyone can use it. <b>Fantasy Map Generator</b> (FMG) is a free open-source application.
In case of FMG is also means that you own all created maps and can use them as you wish, you can even sell them. It means that you own all created maps and can use them as you wish.
<p>The development is supported by community, you can donate on ${Patreon}. <p>The development is community-backed, you can donate on ${Patreon}.
You can also help creating overviews, tutorials and spreding the word about the Generator.</p> You can also help creating overviews, tutorials and spreding the word about the Generator.</p>
<p>The best way to get help is to contact the community on ${Discord} and ${Reddit}. <p>The best way to get help is to contact the community on ${Discord} and ${Reddit}.
Before asking questions, please check out the ${QuickStart} and the ${QAA}.</p> Before asking questions, please check out the ${QuickStart}, the ${QAA}, and ${VideoTutorial}.</p>
<p>Track the development process on ${Trello}.</p> <p>Check out our another project: ${Armoria} heraldry generator and editor.</p>
<p>Check out our new project: ${Armoria}, heraldry generator and editor.</p>
<b>Links:</b>
<ul style="columns:2"> <ul style="columns:2">
<li>${link("https://github.com/Azgaar/Fantasy-Map-Generator", "GitHub repository")}</li> <li>${link("https://github.com/Azgaar/Fantasy-Map-Generator", "GitHub repository")}</li>
<li>${link("https://github.com/Azgaar/Fantasy-Map-Generator/blob/master/LICENSE", "License")}</li> <li>${link("https://github.com/Azgaar/Fantasy-Map-Generator/blob/master/LICENSE", "License")}</li>
<li>${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "Changelog")}</li> <li>${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "Changelog")}</li>
<li>${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys", "Hotkeys")}</li> <li>${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys", "Hotkeys")}</li>
<li>${link("https://trello.com/b/7x832DG4/fantasy-map-generator", "Devboard")}</li>
<li><a href="mailto:azgaar.fmg@yandex.by" target="_blank">Contact Azgaar</a></li>
</ul>`; </ul>`;
$("#alert").dialog({ $("#alert").dialog({

View file

@ -470,23 +470,26 @@ function togglePrec(event) {
function drawPrec() { function drawPrec() {
prec.selectAll("circle").remove(); prec.selectAll("circle").remove();
const cells = grid.cells, const {cells, points} = grid;
p = grid.points;
prec.style("display", "block"); prec.style("display", "block");
const show = d3.transition().duration(800).ease(d3.easeSinIn); const show = d3.transition().duration(800).ease(d3.easeSinIn);
prec.selectAll("text").attr("opacity", 0).transition(show).attr("opacity", 1); prec.selectAll("text").attr("opacity", 0).transition(show).attr("opacity", 1);
const cellsNumberModifier = (pointsInput.dataset.cells / 10000) ** 0.25;
const data = cells.i.filter(i => cells.h[i] >= 20 && cells.prec[i]); const data = cells.i.filter(i => cells.h[i] >= 20 && cells.prec[i]);
const getRadius = prec => rn(Math.sqrt(prec / 4) / cellsNumberModifier, 2);
prec prec
.selectAll("circle") .selectAll("circle")
.data(data) .data(data)
.enter() .enter()
.append("circle") .append("circle")
.attr("cx", d => p[d][0]) .attr("cx", d => points[d][0])
.attr("cy", d => p[d][1]) .attr("cy", d => points[d][1])
.attr("r", 0) .attr("r", 0)
.transition(show) .transition(show)
.attr("r", d => rn(Math.max(Math.sqrt(cells.prec[d] * 0.5), 0.8), 2)); .attr("r", d => getRadius(cells.prec[d]));
} }
function togglePopulation(event) { function togglePopulation(event) {

View file

@ -17,18 +17,24 @@ function editNamesbase() {
document.getElementById("namesbaseMax").addEventListener("input", updateBaseMax); document.getElementById("namesbaseMax").addEventListener("input", updateBaseMax);
document.getElementById("namesbaseDouble").addEventListener("input", updateBaseDublication); document.getElementById("namesbaseDouble").addEventListener("input", updateBaseDublication);
document.getElementById("namesbaseAdd").addEventListener("click", namesbaseAdd); document.getElementById("namesbaseAdd").addEventListener("click", namesbaseAdd);
document.getElementById("namesbaseAnalize").addEventListener("click", analizeNamesbase); document.getElementById("namesbaseAnalyze").addEventListener("click", analyzeNamesbase);
document.getElementById("namesbaseDefault").addEventListener("click", namesbaseRestoreDefault); document.getElementById("namesbaseDefault").addEventListener("click", namesbaseRestoreDefault);
document.getElementById("namesbaseDownload").addEventListener("click", namesbaseDownload); document.getElementById("namesbaseDownload").addEventListener("click", namesbaseDownload);
document.getElementById("namesbaseUpload").addEventListener("click", () => namesbaseToLoad.click()); document.getElementById("namesbaseUpload").addEventListener("click", () => document.getElementById("namesbaseToLoad").click());
document.getElementById("namesbaseToLoad").addEventListener("change", function() {uploadFile(this, namesbaseUpload)}); document.getElementById("namesbaseToLoad").addEventListener("change", function () {
uploadFile(this, namesbaseUpload);
});
document.getElementById("namesbaseCA").addEventListener("click", () => {
openURL("https://cartographyassets.com/asset-category/specific-assets/azgaars-generator/namebases/");
});
document.getElementById("namesbaseSpeak").addEventListener("click", () => speak(namesbaseExamples.textContent)); document.getElementById("namesbaseSpeak").addEventListener("click", () => speak(namesbaseExamples.textContent));
createBasesList(); createBasesList();
updateInputs(); updateInputs();
$("#namesbaseEditor").dialog({ $("#namesbaseEditor").dialog({
title: "Namesbase Editor", width: "42.5em", title: "Namesbase Editor",
width: "auto",
position: {my: "center", at: "center", of: "svg"} position: {my: "center", at: "center", of: "svg"}
}); });
@ -40,7 +46,10 @@ function editNamesbase() {
function updateInputs() { function updateInputs() {
const base = +document.getElementById("namesbaseSelect").value; const base = +document.getElementById("namesbaseSelect").value;
if (!nameBases[base]) {tip(`Namesbase ${base} is not defined`, false, "error"); return;} if (!nameBases[base]) {
tip(`Namesbase ${base} is not defined`, false, "error");
return;
}
document.getElementById("namesbaseTextarea").value = nameBases[base].b; document.getElementById("namesbaseTextarea").value = nameBases[base].b;
document.getElementById("namesbaseName").value = nameBases[base].name; document.getElementById("namesbaseName").value = nameBases[base].name;
document.getElementById("namesbaseMin").value = nameBases[base].min; document.getElementById("namesbaseMin").value = nameBases[base].min;
@ -52,7 +61,7 @@ function editNamesbase() {
function updateExamples() { function updateExamples() {
const base = +document.getElementById("namesbaseSelect").value; const base = +document.getElementById("namesbaseSelect").value;
let examples = ""; let examples = "";
for (let i=0; i < 10; i++) { for (let i = 0; i < 10; i++) {
const example = Names.getBase(base); const example = Names.getBase(base);
if (example === undefined) { if (example === undefined) {
examples = "Cannot generate examples. Please verify the data"; examples = "Cannot generate examples. Please verify the data";
@ -84,13 +93,19 @@ function editNamesbase() {
function updateBaseMin() { function updateBaseMin() {
const base = +document.getElementById("namesbaseSelect").value; const base = +document.getElementById("namesbaseSelect").value;
if (+this.value > nameBases[base].max) {tip("Minimal length cannot be greater than maximal", false, "error"); return;} if (+this.value > nameBases[base].max) {
tip("Minimal length cannot be greater than maximal", false, "error");
return;
}
nameBases[base].min = +this.value; nameBases[base].min = +this.value;
} }
function updateBaseMax() { function updateBaseMax() {
const base = +document.getElementById("namesbaseSelect").value; const base = +document.getElementById("namesbaseSelect").value;
if (+this.value < nameBases[base].min) {tip("Maximal length should be greater than minimal", false, "error"); return;} if (+this.value < nameBases[base].min) {
tip("Maximal length should be greater than minimal", false, "error");
return;
}
nameBases[base].max = +this.value; nameBases[base].max = +this.value;
} }
@ -99,59 +114,70 @@ function editNamesbase() {
nameBases[base].d = this.value; nameBases[base].d = this.value;
} }
function analizeNamesbase() { function analyzeNamesbase() {
const string = document.getElementById("namesbaseTextarea").value; const namesSourceString = document.getElementById("namesbaseTextarea").value;
if (!string) {tip("Names data field should not be empty", false, "error"); return;} const namesArray = namesSourceString.toLowerCase().split(",");
const base = string.toLowerCase(); const length = namesArray.length;
const array = base.split(","); if (!namesSourceString || !length) return tip("Names data should not be empty", false, "error");
const l = array.length;
if (!l) {tip("Names data should not be empty", false, "error"); return;}
const wordsLength = array.map(n => n.length); const chain = Names.calculateChain(namesSourceString);
const multi = rn(d3.mean(array.map(n => (n.match(/ /i)||[]).length)) * 100, 2); const variety = rn(d3.mean(Object.values(chain).map(keyValue => keyValue.length)));
const geminate = array.map(name => name.match(/[^\w\s]|(.)(?=\1)/g)||[]).flat();
const doubled = ([...new Set(geminate)].filter(l => geminate.filter(d => d === l).length > 3)||["none"]).join("");
const chain = Names.calculateChain(string);
const depth = rn(d3.mean(Object.keys(chain).map(key => chain[key].filter(c => c !== " ").length)));
const nonLatin = (string.match(/[^\u0000-\u007f]/g)||["none"]).join("");
const lengthStat = const wordsLength = namesArray.map(n => n.length);
l < 30 ? "<span style='color:red'>[not enough]</span>" :
l < 150 ? "<span style='color:darkred'>[low]</span>" :
l < 150 ? "<span style='color:orange'>[low]</span>" :
l < 400 ? "<span style='color:green'>[good]</span>" :
l < 600 ? "<span style='color:orange'>[overmuch]</span>" :
"<span style='color:darkred'>[overmuch]</span>";
const rangeStat = const nonLatin = namesSourceString.match(/[^\u0000-\u007f]/g);
l < 10 ? "<span style='color:red'>[low]</span>" : const nonBasicLatinChars = nonLatin
l < 15 ? "<span style='color:darkred'>[low]</span>" : ? unique(
l < 20 ? "<span style='color:orange'>[low]</span>" : namesSourceString
"<span style='color:green'>[good]</span>"; .match(/[^\u0000-\u007f]/g)
.join("")
.toLowerCase()
).join("")
: "none";
const depthStat = const geminate = namesArray.map(name => name.match(/[^\w\s]|(.)(?=\1)/g) || []).flat();
l < 15 ? "<span style='color:red'>[low]</span>" : const doubled = unique(geminate).filter(char => geminate.filter(doudledChar => doudledChar === char).length > 3) || ["none"];
l < 20 ? "<span style='color:darkred'>[low]</span>" :
l < 25 ? "<span style='color:orange'>[low]</span>" : const duplicates = unique(namesArray.filter((e, i, a) => a.indexOf(e) !== i)).join(", ") || "none";
"<span style='color:green'>[good]</span>"; const multiwordRate = d3.mean(namesArray.map(n => +n.includes(" ")));
const getLengthQuality = () => {
if (length < 30) return "<span data-tip='Namesbase contains < 30 names - not enough to generate reasonable data' style='color:red'>[not enough]</span>";
if (length < 100) return "<span data-tip='Namesbase contains < 100 names - not enough to generate good names' style='color:darkred'>[low]</span>";
if (length <= 400) return "<span data-tip='Namesbase contains a reasonable number of samples' style='color:green'>[good]</span>";
return "<span data-tip='Namesbase contains > 400 names. That is too much, try to reduce it to ~300 names' style='color:darkred'>[overmuch]</span>";
};
const getVarietyLevel = () => {
if (variety < 15) return "<span data-tip='Namesbase average variety < 15 - generated names will be too repetitive' style='color:red'>[low]</span>";
if (variety < 30) return "<span data-tip='Namesbase average variety < 30 - names can be too repetitive' style='color:orange'>[mean]</span>";
return "<span data-tip='Namesbase variety is good' style='color:green'>[good]</span>";
};
alertMessage.innerHTML = `<div style="line-height: 1.6em; max-width: 20em"> alertMessage.innerHTML = `<div style="line-height: 1.6em; max-width: 20em">
<div>Namesbase length: ${l} ${lengthStat}</div> <div data-tip="Number of names provided">Namesbase length: ${length} ${getLengthQuality()}</div>
<div>Namesbase range: ${Object.keys(chain).length-1} ${rangeStat}</div> <div data-tip="Average number of generation variants for each key in the chain">Namesbase variety: ${variety} ${getVarietyLevel()}</div>
<div>Namesbase depth: ${depth} ${depthStat}</div>
<div>Non-basic chars: ${nonLatin}</div>
<hr> <hr>
<div>Min name length: ${d3.min(wordsLength)}</div> <div data-tip="The shortest name length">Min name length: ${d3.min(wordsLength)}</div>
<div>Max name length: ${d3.max(wordsLength)}</div> <div data-tip="The longest name length">Max name length: ${d3.max(wordsLength)}</div>
<div>Mean name length: ${rn(d3.mean(wordsLength), 1)}</div> <div data-tip="Average name length">Mean name length: ${rn(d3.mean(wordsLength), 1)}</div>
<div>Median name length: ${d3.median(wordsLength)}</div> <div data-tip="Common name length">Median name length: ${d3.median(wordsLength)}</div>
<div>Doubled chars: ${doubled}</div> <hr>
<div>Multi-word names: ${multi}%</div> <div data-tip="Characters outside of Basic Latin have bad font support">Non-basic chars: ${nonBasicLatinChars}</div>
<div data-tip="Characters that are frequently (more than 3 times) doubled">Doubled chars: ${doubled.join("")}</div>
<div data-tip="Names used more than one time">Duplicates: ${duplicates}</div>
<div data-tip="Percentage of names containing space character">Multi-word names: ${rn(multiwordRate * 100, 2)}%</div>
</div>`; </div>`;
$("#alert").dialog({ $("#alert").dialog({
resizable: false, title: "Data Analysis", resizable: false,
title: "Data Analysis",
position: {my: "left top-30", at: "right+10 top", of: "#namesbaseEditor"}, position: {my: "left top-30", at: "right+10 top", of: "#namesbaseEditor"},
buttons: {OK: function() {$(this).dialog("close");}} buttons: {
OK: function () {
$(this).dialog("close");
}
}
}); });
} }
@ -171,35 +197,42 @@ function editNamesbase() {
function namesbaseRestoreDefault() { function namesbaseRestoreDefault() {
alertMessage.innerHTML = `Are you sure you want to restore default namesbase?`; alertMessage.innerHTML = `Are you sure you want to restore default namesbase?`;
$("#alert").dialog({resizable: false, title: "Restore default data", $("#alert").dialog({
resizable: false,
title: "Restore default data",
buttons: { buttons: {
Restore: function() { Restore: function () {
$(this).dialog("close"); $(this).dialog("close");
Names.clearChains(); Names.clearChains();
nameBases = Names.getNameBases(); nameBases = Names.getNameBases();
createBasesList(); createBasesList();
updateInputs(); updateInputs();
}, },
Cancel: function() {$(this).dialog("close");} Cancel: function () {
$(this).dialog("close");
}
} }
}); });
} }
function namesbaseDownload() { function namesbaseDownload() {
const data = nameBases.map((b,i) => `${b.name}|${b.min}|${b.max}|${b.d}|${b.m}|${b.b}`).join("\r\n"); const data = nameBases.map((b, i) => `${b.name}|${b.min}|${b.max}|${b.d}|${b.m}|${b.b}`).join("\r\n");
const name = getFileName("Namesbase") + ".txt"; const name = getFileName("Namesbase") + ".txt";
downloadFile(data, name); downloadFile(data, name);
} }
function namesbaseUpload(dataLoaded) { function namesbaseUpload(dataLoaded) {
const data = dataLoaded.split("\r\n"); const data = dataLoaded.split("\r\n");
if (!data || !data[0]) {tip("Cannot load a namesbase. Please check the data format", false, "error"); return;} if (!data || !data[0]) {
tip("Cannot load a namesbase. Please check the data format", false, "error");
return;
}
Names.clearChains(); Names.clearChains();
nameBases = []; nameBases = [];
data.forEach(d => { data.forEach(d => {
const e = d.split("|"); const e = d.split("|");
nameBases.push({name:e[0], min:e[1], max:e[2], d:e[3], m:e[4], b:e[5]}); nameBases.push({name: e[0], min: e[1], max: e[2], d: e[3], m: e[4], b: e[5]});
}); });
createBasesList(); createBasesList();

View file

@ -102,7 +102,8 @@ function showSupporters() {
Dominick Ormsby,Linn Browning,Václav Švec,Alan Buehne,George J.Lekkas,Alexandre Boivin,Tommy Mayfield,Skylar Mangum-Turner,Karen Blythe,Stefan Gugerel, Dominick Ormsby,Linn Browning,Václav Švec,Alan Buehne,George J.Lekkas,Alexandre Boivin,Tommy Mayfield,Skylar Mangum-Turner,Karen Blythe,Stefan Gugerel,
Mike Conley,Xavier privé,Hope You're Well,Mark Sprietsma,Robert Landry,Nick Mowry,steve hall,Markell,Josh Wren,Neutrix,BLRageQuit,Rocky, Mike Conley,Xavier privé,Hope You're Well,Mark Sprietsma,Robert Landry,Nick Mowry,steve hall,Markell,Josh Wren,Neutrix,BLRageQuit,Rocky,
Dario Spadavecchia,Bas Kroot,John Patrick Callahan Jr,Alexandra Vesey,D,Exp1nt,james,Braxton Istace,w,Rurikid,AntiBlock,Redsauz,BigE0021, Dario Spadavecchia,Bas Kroot,John Patrick Callahan Jr,Alexandra Vesey,D,Exp1nt,james,Braxton Istace,w,Rurikid,AntiBlock,Redsauz,BigE0021,
Jonathan Williams,ojacid .,Brian Wilson,A Patreon of the Ahts,Shubham Jakhotiya`; Jonathan Williams,ojacid .,Brian Wilson,A Patreon of the Ahts,Shubham Jakhotiya,www15o,Jan Bundesmann,Angelique Badger,Joshua Xiong,Moist mongol,
Frank Fewkes,jason baldrick,Game Master Pro,Andrew Kircher,Preston Mitchell,Chris Kohut`;
const array = supporters const array = supporters
.replace(/(?:\r\n|\r|\n)/g, "") .replace(/(?:\r\n|\r|\n)/g, "")
@ -287,18 +288,16 @@ function generateMapWithSeed(source) {
} }
function showSeedHistoryDialog() { function showSeedHistoryDialog() {
const alert = mapHistory const lines = mapHistory.map((h, i) => {
.map(function (h, i) {
const created = new Date(h.created).toLocaleTimeString(); const created = new Date(h.created).toLocaleTimeString();
const button = `<i data-tip"Click to generate a map with this seed" onclick="restoreSeed(${i})" class="icon-history optionsSeedRestore"></i>`; const button = `<i data-tip="Click to generate a map with this seed" onclick="restoreSeed(${i})" class="icon-history optionsSeedRestore"></i>`;
return `<div>${i + 1}. Seed: ${h.seed} ${button}. Size: ${h.width}x${h.height}. Template: ${h.template}. Created: ${created}</div>`; return `<li>Seed: ${h.seed} ${button}. Size: ${h.width}x${h.height}. Template: ${h.template}. Created: ${created}</li>`;
}) });
.join(""); alertMessage.innerHTML = `<ol style="margin: 0; padding-left: 1.5em">${lines.join("")}</ol>`;
alertMessage.innerHTML = alert;
$("#alert").dialog({ $("#alert").dialog({
resizable: false, resizable: false,
title: "Seed history", title: "Seed history",
width: fitContent(),
position: {my: "center", at: "center", of: "svg"} position: {my: "center", at: "center", of: "svg"}
}); });
} }

View file

@ -137,7 +137,7 @@ function editProvinces() {
p.color p.color
}" class="fillRect pointer"></svg> }" class="fillRect pointer"></svg>
<input data-tip="Province name. Click to change" class="name pointer" value="${p.name}" readonly> <input data-tip="Province name. Click to change" class="name pointer" value="${p.name}" readonly>
<svg data-tip="Click to show and edit province emblem" class="coaIcon hide" viewBox="0 0 200 200"><use href="#provinceCOA${p.i}"></use></svg> <svg data-tip="Click to show and edit province emblem" class="coaIcon pointer hide" viewBox="0 0 200 200"><use href="#provinceCOA${p.i}"></use></svg>
<input data-tip="Province form name. Click to change" class="name pointer hide" value="${p.formName}" readonly> <input data-tip="Province form name. Click to change" class="name pointer hide" value="${p.formName}" readonly>
<span data-tip="Province capital. Click to zoom into view" class="icon-star-empty pointer hide ${p.burg ? "" : "placeholder"}"></span> <span data-tip="Province capital. Click to zoom into view" class="icon-star-empty pointer hide ${p.burg ? "" : "placeholder"}"></span>
<select data-tip="Province capital. Click to select from burgs within the state. No capital means the province is governed from the state capital" class="cultureBase hide ${ <select data-tip="Province capital. Click to select from burgs within the state. No capital means the province is governed from the state capital" class="cultureBase hide ${

View file

@ -89,7 +89,8 @@ function createRiver() {
const source = riverCells[0]; const source = riverCells[0];
const mouth = parent === riverId ? last(riverCells) : riverCells[riverCells.length - 2]; const mouth = parent === riverId ? last(riverCells) : riverCells[riverCells.length - 2];
const sourceWidth = 0.05; const sourceWidth = 0.05;
const widthFactor = 1.2; const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2);
const widthFactor = 1.2 * defaultWidthFactor;
const meanderedPoints = addMeandering(riverCells); const meanderedPoints = addMeandering(riverCells);

View file

@ -136,7 +136,7 @@ function editStates() {
s.color s.color
}" class="fillRect pointer"></svg> }" class="fillRect pointer"></svg>
<input data-tip="State name. Click to change" class="stateName name pointer" value="${s.name}" readonly> <input data-tip="State name. Click to change" class="stateName name pointer" value="${s.name}" readonly>
<svg data-tip="Click to show and edit state emblem" class="coaIcon hide" viewBox="0 0 200 200"><use href="#stateCOA${s.i}"></use></svg> <svg data-tip="Click to show and edit state emblem" class="coaIcon pointer hide" viewBox="0 0 200 200"><use href="#stateCOA${s.i}"></use></svg>
<input data-tip="State form name. Click to change" class="stateForm name pointer" value="${s.formName}" readonly> <input data-tip="State form name. Click to change" class="stateForm name pointer" value="${s.formName}" readonly>
<span data-tip="State capital. Click to zoom into view" class="icon-star-empty pointer hide"></span> <span data-tip="State capital. Click to zoom into view" class="icon-star-empty pointer hide"></span>
<input data-tip="Capital name. Click and type to rename" class="stateCapital hide" value="${capital}" autocorrect="off" spellcheck="false"/> <input data-tip="Capital name. Click and type to rename" class="stateCapital hide" value="${capital}" autocorrect="off" spellcheck="false"/>

View file

@ -831,7 +831,7 @@ function applyDefaultStyle() {
landmass.attr("opacity", 1).attr("fill", "#eef6fb").attr("filter", null); landmass.attr("opacity", 1).attr("fill", "#eef6fb").attr("filter", null);
markers.attr("opacity", null).attr("rescale", 1).attr("filter", "url(#dropShadow01)"); markers.attr("opacity", null).attr("rescale", 1).attr("filter", "url(#dropShadow01)");
prec.attr("opacity", null).attr("stroke", "#000000").attr("stroke-width", 0.1).attr("fill", "#003dff").attr("filter", null); prec.attr("opacity", null).attr("stroke", "#000000").attr("stroke-width", 0).attr("fill", "#003dff").attr("filter", null);
population.attr("opacity", null).attr("stroke-width", 1.6).attr("stroke-dasharray", null).attr("stroke-linecap", "butt").attr("filter", null); population.attr("opacity", null).attr("stroke-width", 1.6).attr("stroke-dasharray", null).attr("stroke-linecap", "butt").attr("filter", null);
population.select("#rural").attr("stroke", "#0000ff"); population.select("#rural").attr("stroke", "#0000ff");
population.select("#urban").attr("stroke", "#ff0000"); population.select("#urban").attr("stroke", "#ff0000");
@ -938,7 +938,7 @@ function applyDefaultStyle() {
.attr("stroke-linecap", "round"); .attr("stroke-linecap", "round");
legend.select("#legendBox").attr("fill", "#ffffff").attr("fill-opacity", 0.8); legend.select("#legendBox").attr("fill", "#ffffff").attr("fill-opacity", 0.8);
const citiesSize = Math.max(rn(8 - regionsInput.value / 20), 3); const citiesSize = Math.max(rn(8 - regionsOutput.value / 20), 3);
burgLabels burgLabels
.select("#cities") .select("#cities")
.attr("fill", "#3e3e4b") .attr("fill", "#3e3e4b")
@ -979,7 +979,7 @@ function applyDefaultStyle() {
.attr("stroke-linecap", "butt"); .attr("stroke-linecap", "butt");
anchors.select("#towns").attr("opacity", 1).attr("fill", "#ffffff").attr("stroke", "#3e3e4b").attr("stroke-width", 1.2).attr("size", 1); anchors.select("#towns").attr("opacity", 1).attr("fill", "#ffffff").attr("stroke", "#3e3e4b").attr("stroke-width", 1.2).attr("size", 1);
const stateLabelSize = Math.max(rn(24 - regionsInput.value / 6), 6); const stateLabelSize = Math.max(rn(24 - regionsOutput.value / 6), 6);
labels labels
.select("#states") .select("#states")
.attr("fill", "#3e3e4b") .attr("fill", "#3e3e4b")

View file

@ -143,7 +143,7 @@ function regenerateStates() {
const localSeed = Math.floor(Math.random() * 1e9); // new random seed const localSeed = Math.floor(Math.random() * 1e9); // new random seed
Math.random = aleaPRNG(localSeed); Math.random = aleaPRNG(localSeed);
const statesCount = +regionsInput.value; const statesCount = +regionsOutput.value;
const burgs = pack.burgs.filter(b => b.i && !b.removed); const burgs = pack.burgs.filter(b => b.i && !b.removed);
if (!burgs.length) return tip("There are no any burgs to generate states. Please create burgs first", false, "error"); if (!burgs.length) return tip("There are no any burgs to generate states. Please create burgs first", false, "error");
if (burgs.length < statesCount) tip(`Not enough burgs to generate ${statesCount} states. Will generate only ${burgs.length} states`, false, "warn"); if (burgs.length < statesCount) tip(`Not enough burgs to generate ${statesCount} states. Will generate only ${burgs.length} states`, false, "warn");
@ -624,7 +624,9 @@ function addRiverOnClick() {
const source = riverCells[0]; const source = riverCells[0];
const mouth = riverCells[riverCells.length - 2]; const mouth = riverCells[riverCells.length - 2];
const widthFactor = river?.widthFactor || (!parent || parent === riverId ? 1.2 : 1);
const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2);
const widthFactor = river?.widthFactor || (!parent || parent === riverId ? defaultWidthFactor * 1.2 : defaultWidthFactor);
const meanderedPoints = addMeandering(riverCells); const meanderedPoints = addMeandering(riverCells);
const discharge = cells.fl[mouth]; // m3 in second const discharge = cells.fl[mouth]; // m3 in second
@ -739,7 +741,7 @@ function configMarkersGeneration() {
const inputId = `markerIconInput${index}`; const inputId = `markerIconInput${index}`;
return `<tr> return `<tr>
<td><input value="${type}" /></td> <td><input value="${type}" /></td>
<td> <td style="position: relative">
<input id="${inputId}" style="width: 5em" value="${icon}" /> <input id="${inputId}" style="width: 5em" value="${icon}" />
<i class="icon-edit pointer" style="position: absolute; margin:.4em 0 0 -1.4em; font-size:.85em"></i> <i class="icon-edit pointer" style="position: absolute; margin:.4em 0 0 -1.4em; font-size:.85em"></i>
</td> </td>

View file

@ -154,18 +154,23 @@ void (function () {
const prompt = document.getElementById("prompt"); const prompt = document.getElementById("prompt");
const form = prompt.querySelector("#promptForm"); const form = prompt.querySelector("#promptForm");
window.prompt = function (promptText = "Please provide an input", options = {default: 1, step: 0.01, min: 0, max: 100}, callback) { const defaultText = "Please provide an input";
if (options.default === undefined) { const defaultOptions = {default: 1, step: 0.01, min: 0, max: 100, required: true};
ERROR && console.error("Prompt: options object does not have default value defined");
return; window.prompt = function (promptText = defaultText, options = defaultOptions, callback) {
} if (options.default === undefined) return ERROR && console.error("Prompt: options object does not have default value defined");
const input = prompt.querySelector("#promptInput"); const input = prompt.querySelector("#promptInput");
prompt.querySelector("#promptText").innerHTML = promptText; prompt.querySelector("#promptText").innerHTML = promptText;
const type = typeof options.default === "number" ? "number" : "text"; const type = typeof options.default === "number" ? "number" : "text";
input.type = type; input.type = type;
if (options.step !== undefined) input.step = options.step; if (options.step !== undefined) input.step = options.step;
if (options.min !== undefined) input.min = options.min; if (options.min !== undefined) input.min = options.min;
if (options.max !== undefined) input.max = options.max; if (options.max !== undefined) input.max = options.max;
input.required = options.required === false ? false : true;
input.placeholder = "type a " + type; input.placeholder = "type a " + type;
input.value = options.default; input.value = options.default;
prompt.style.display = "block"; prompt.style.display = "block";
@ -173,9 +178,9 @@ void (function () {
form.addEventListener( form.addEventListener(
"submit", "submit",
event => { event => {
event.preventDefault();
prompt.style.display = "none"; prompt.style.display = "none";
const v = type === "number" ? +input.value : input.value; const v = type === "number" ? +input.value : input.value;
event.preventDefault();
if (callback) callback(v); if (callback) callback(v);
}, },
{once: true} {once: true}
@ -183,7 +188,9 @@ void (function () {
}; };
const cancel = prompt.querySelector("#promptCancel"); const cancel = prompt.querySelector("#promptCancel");
cancel.addEventListener("click", () => (prompt.style.display = "none")); cancel.addEventListener("click", () => {
prompt.style.display = "none";
});
})(); })();
// indexedDB; ldb object // indexedDB; ldb object