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;
}
input[type="radio"] {
vertical-align: bottom;
cursor: pointer;
}
textarea {
padding: 2px;
text-indent: 1px;
padding: 3px;
box-sizing: border-box;
width: 100%;
}
iframe {
border: 0;
pointer-events: none;
width: 100%;
}
@ -835,75 +838,79 @@ fieldset {
}
.matrix-table {
width: 100%;
font-size: smaller;
text-align: center;
border-collapse: collapse;
max-height: 80vh;
max-width: 85vw;
scrollbar-width: thin;
overflow: auto;
}
table.matrix-table th,
table.matrix-table td {
.matrix-table > table {
text-align: center;
border-collapse: collapse;
font-size: smaller;
}
.matrix-table > table th,
.matrix-table > table td {
border: 1px solid var(--dark-solid);
height: 2em;
padding: 0.2em;
position: relative;
}
table.matrix-table th {
.matrix-table > table th {
background-color: #302a2a;
color: #ffffff;
}
table.matrix-table tr:hover th {
background: #3e3636;
}
table.matrix-table td:hover {
.matrix-table > table td:hover {
outline: 2px solid var(--dark-solid);
outline-offset: -1px;
z-index: 1;
cursor: pointer;
}
table.matrix-table td.Ally {
.matrix-table > table td.Ally {
background-color: #73ec73;
color: #000000;
}
table.matrix-table td.Friendly {
.matrix-table > table td.Friendly {
background-color: #d4f8aa;
}
table.matrix-table td.Neutral {
.matrix-table > table td.Neutral {
background-color: #d8d9d3;
}
table.matrix-table td.Suspicion {
.matrix-table > table td.Suspicion {
background-color: #eeafaa;
}
table.matrix-table td.Enemy {
.matrix-table > table td.Enemy {
background-color: #ffa39c;
color: #af0d23;
}
table.matrix-table td.Unknown {
.matrix-table > table td.Unknown {
background-color: #c1bfbf;
}
table.matrix-table td.Rival {
.matrix-table > table td.Rival {
background-color: #bd845c;
}
table.matrix-table td.Vassal {
.matrix-table > table td.Vassal {
background-color: #87cefa;
}
table.matrix-table td.Suzerain {
.matrix-table > table td.Suzerain {
background-color: #8f8fe1;
}
table.matrix-table td.x {
.matrix-table > table td.x {
background-color: #d4ca94;
cursor: initial;
}
#sizeOutput {
@ -1311,19 +1318,23 @@ div.slider .ui-slider-handle {
}
#alertMessage::-webkit-scrollbar,
.table::-webkit-scrollbar {
.table::-webkit-scrollbar,
.matrix-table::-webkit-scrollbar {
width: 6px;
height: 6px;
background-color: transparent;
}
#alertMessage::-webkit-scrollbar-thumb,
.table::-webkit-scrollbar-thumb {
.table::-webkit-scrollbar-thumb,
.matrix-table::-webkit-scrollbar-thumb {
background-color: #aaa;
border-radius: 6px;
}
#alertMessage::-webkit-scrollbar-thumb:hover,
.table::-webkit-scrollbar-thumb:hover {
.table::-webkit-scrollbar-thumb:hover,
.matrix-table::-webkit-scrollbar-thumb:hover {
background: #666;
}
@ -1505,15 +1516,14 @@ div.states > .riverType {
width: 5em;
}
div.states > .coaIcon {
.coaIcon {
stroke-width: 3;
width: 1.4em;
height: 1.4em;
margin: -0.3em 0;
cursor: pointer;
}
div.states > .coaIcon > use {
.coaIcon > use {
pointer-events: none;
}
@ -2001,7 +2011,7 @@ div.textual fieldset {
div.textual span,
.textual legend {
font-size: 0.8em;
font-size: 0.9em;
font-weight: bold;
}
@ -2120,8 +2130,9 @@ svg.button {
text-decoration: underline;
}
#info-line {
.info-line {
font-size: 0.9em;
font-style: italic;
color: gray;
user-select: none;
}
@ -2265,10 +2276,10 @@ svg.button {
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
max-width: 22em;
background-color: #fff;
max-width: 23em;
padding: 1.2em;
border: solid 1px #000;
background-color: var(--bg-dialogs);
border: solid 1px var(--dark-solid);
font-size: 1.2em;
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">
<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>Map seed</td>
<td>
@ -1062,7 +1062,7 @@
<input id="regionsInput" data-stored="regions" type="range" min=0 max=99 value=13>
</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>
</tr>
@ -1451,9 +1451,21 @@
</div>
<div id="aboutContent" class="tabcontent">
<p><a href="https://github.com/Azgaar/Fantasy-Map-Generator" target="_blank">Fantasy Map Generator</a> is a free <a href="https://github.com/Azgaar/Fantasy-Map-Generator/blob/master/LICENSE" target="_blank">open source</a> tool which procedurally generates fantasy maps. You may use auto-generated maps as they are, edit them or even create a new map from scratch. Check out the <a href="https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Quick-Start-Tutorial" target="_blank">quick start tutorial</a>, <a href="https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Q&A" target="_blank">Q&A</a> and <a href="https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys" target="_blank">hotkeys</a> for guidance.</p>
<p>Join our <a href='https://discordapp.com/invite/X7E84HU' target='_blank'>Discord server</a> and <a href="https://www.reddit.com/r/FantasyMapGenerator/" target="_blank">Reddit community</a> to ask questions, get help and share 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>
<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</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">
<a href="https://www.patreon.com/azgaar" target="_blank" style="color: white; text-decoration: none; font-family: sans-serif">
<div>
@ -1466,7 +1478,10 @@
</div>
</a>
</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">
<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 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>
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" />
<i id="regenerateMFCGBurgSeed" data-tip="Randomize Medieval Fantasy City Generator burg seed" class="icon-arrows-cw pointer" style="margin-left: .1em"></i>
</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>
</div>
</div>
@ -2623,8 +2641,10 @@
<option value="Sultanate">Sultanate</option>
<option value="Tsardom">Tsardom</option>
<option value="Ulus">Ulus</option>
<option value="Viceroyalty">Viceroyalty</option>
</optgroup>
<optgroup label="Republic">
<option value="Chancellery">Chancellery</option>
<option value="City-state">City-state</option>
<option value="Diarchy">Diarchy</option>
<option value="Federation">Federation</option>
@ -2662,8 +2682,10 @@
<option value="Divine Kingdom">Divine Kingdom</option>
<option value="Divine Empire">Divine Empire</option>
<option value="Eparchy">Eparchy</option>
<option value="Exarchate">Exarchate</option>
<option value="Holy State">Holy State</option>
<option value="Imamah">Imamah</option>
<option value="Patriarchate">Patriarchate</option>
<option value="Theocracy">Theocracy</option>
</optgroup>
<optgroup label="Anarchy">
@ -2740,33 +2762,27 @@
<div id="diplomacyEditor" class="dialog stable" style="display: none">
<div id="diplomacyHeader" class="header">
<div style="left:.2em" data-tip="Click to sort by state name" class="sortable alphabetically" data-sortby="name">State&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 id="diplomacyBodySection" class="table"></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 class="info-line">Click on state name to see relations.<br>Click on relations name to change it</div>
<div id="diplomacyBottom" style="margin-top: .1em">
<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="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="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>
</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>
<div data-tip="Province short name" class="label">Short name:</div>
@ -2784,6 +2800,8 @@
<option value="Autonomy">Autonomy</option>
<option value="Barony">Barony</option>
<option value="Canton">Canton</option>
<option value="Captaincy">Captaincy</option>
<option value="Chiefdom">Chiefdom</option>
<option value="Clan">Clan</option>
<option value="Colony">Colony</option>
<option value="Council">Council</option>
@ -2791,6 +2809,7 @@
<option value="Deanery">Deanery</option>
<option value="Department">Department</option>
<option value="Dependency">Dependency</option>
<option value="Diaconate">Diaconate</option>
<option value="District">District</option>
<option value="Earldom">Earldom</option>
<option value="Governorate">Governorate</option>
@ -2800,6 +2819,7 @@
<option value="Landgrave">Landgrave</option>
<option value="Mandate">Mandate</option>
<option value="Margrave">Margrave</option>
<option value="Municipality">Municipality</option>
<option value="Occupation zone">Occupation zone</option>
<option value="Parish">Parish</option>
<option value="Prefecture">Prefecture</option>
@ -2807,6 +2827,7 @@
<option value="Region">Region</option>
<option value="Republic">Republic</option>
<option value="Reservation">Reservation</option>
<option value="Seneschalty">Seneschalty</option>
<option value="Shire">Shire</option>
<option value="State">State</option>
<option value="Territory">Territory</option>
@ -2874,21 +2895,22 @@
<div id="namesbaseBasesTop">
<span>Select base: </span>
<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 id="namesbaseBody">
<span>Names data:</span><br>
<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 id="namesbaseBody" style="margin-block: 2px">
<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>
<div>
<span>Name: </span>
<input id="namesbaseName" data-tip="Type to change a base name" placeholder="Base name" autocorrect="off" spellcheck="false" style="width:12em"/>
<span>Length: </span>
<input id="namesbaseMin" data-tip="Recommended minimum name length" type="number" min=2 max=100>
<input id="namesbaseMax" data-tip="Recommended maximum name length" type="number" min=2 value=10>
<span>Double: </span>
<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">
</div>
<fieldset>
<legend>Generated examples: </legend>
<div id="namesbaseExamples" data-tip="Examples. Click to re-generate"></div>
@ -2897,12 +2919,12 @@
<div id="namesbaseBottom">
<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="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="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>
</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="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="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>
</div>
</div>
@ -3640,8 +3663,8 @@
</div>
<div data-tip="Image scale relative to image size (e.g. 5x)" style="margin-bottom: .3em">
<div class="label">Scale:</div>
<input id="tileScaleInput" data-stored="tileScale" type="range" min=1 max=4 value=1 style="width: 11em">
<input id="tileScaleOutput" data-stored="tileScale" type="number" min=1 value=1>
<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 >
</div>
<div data-tip="Calculated size of image if combined" style="margin-bottom: .3em">
<div class="label">Total size:</div>
@ -3695,7 +3718,7 @@
<div id="prompt" style="display: none" class="dialog">
<form id="promptForm">
<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="button" id="promptCancel" formnovalidate>Cancel</button>
</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());
// 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 seed;
let mapId;
@ -161,7 +161,7 @@ let urbanDensity = +document.getElementById("urbanDensityInput").value;
applyStoredOptions();
// voronoi graph extention, cannot be changed arter generation
// voronoi graph extension, cannot be changed after generation
let graphWidth = +mapWidthInput.value;
let graphHeight = +mapHeightInput.value;
@ -493,7 +493,7 @@ function invokeActiveZooming() {
coastline.select("#sea_island").attr("filter", filter);
}
// rescale lables on zoom
// rescale labels on zoom
if (labels.style("display") !== "none") {
labels.selectAll("g").each(function () {
if (this.id === "burgLabels") return;
@ -668,7 +668,7 @@ function generate() {
const parsedError = parseError(error);
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.
<p id="errorBox">${parsedError}</p>`;
$("#alert").dialog({
@ -909,7 +909,7 @@ function defineMapSize() {
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 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 (template === "Pangea") return [100, 50];
@ -981,7 +981,10 @@ function generatePrecipitation() {
prec.selectAll("*").remove();
const {cells, cellsX, cellsY} = grid;
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 easterly = [];
@ -997,14 +1000,14 @@ function generatePrecipitation() {
// x2 = 60-70 latitude: wet summer (rising zone), dry winter (sinking zone)
// x1 = 70-85 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;
// define wind directions based on cells latitude and prevailing winds there
d3.range(0, cells.i.length, cellsX).forEach(function (c, i) {
const lat = mapCoordinates.latN - (i / cellsY) * mapCoordinates.latT;
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 {isWest, isEast, isNorth, isSouth} = getWindDirections(windTier);
@ -1021,14 +1024,14 @@ function generatePrecipitation() {
const vertT = southerly + northerly;
if (northerly) {
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;
passWind(d3.range(0, cellsX, 1), maxPrecN, cellsX, cellsY);
}
if (southerly) {
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;
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
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) {
if (cells.temp[current] < -5) continue; // no flux in permafrost
@ -1182,7 +1185,7 @@ function reGraph() {
TIME && console.timeEnd("reGraph");
}
// Detect and draw the coasline
// Detect and draw the coastline
function drawCoastline() {
TIME && console.time("drawCoastline");
reMarkFeatures();
@ -1191,7 +1194,7 @@ function drawCoastline() {
vertices = pack.vertices,
n = cells.i.length,
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(
features.map(f => (f.land ? f.cells : 0)),
(a, b) => b - a
@ -1395,6 +1398,12 @@ function 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
function defineBiomes() {
TIME && console.time("defineBiomes");
@ -1427,7 +1436,7 @@ function defineBiomes() {
function getBiomeId(moisture, temperature, height) {
if (height < 20) return 0; // marine biome: all water cells
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 temperatureBand = Math.min(Math.max(20 - temperature, 0), 25); // [0-25]

View file

@ -31,7 +31,7 @@ window.BurgsAndStates = (function () {
function placeCapitals() {
TIME && console.time("placeCapitals");
let count = +regionsInput.value;
let count = +regionsOutput.value;
let burgs = [0];
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.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.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 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;
@ -726,7 +726,7 @@ window.BurgsAndStates = (function () {
TIME && console.time("assignColors");
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 => {
if (!s.i || s.removed) return;
const neibs = s.neighbors;
@ -962,12 +962,12 @@ window.BurgsAndStates = (function () {
const republic = {
Republic: 75,
Federation: 4,
Oligarchy: 2,
"Trade Company": 4,
"Most Serene Republic": 2,
Oligarchy: 2,
Tetrarchy: 1,
Triumvirate: 1,
Diarchy: 1,
"Trade Company": 4,
Junta: 1
}; // weighted random
const union = {Union: 3, League: 4, Confederation: 1, "United Kingdom": 1, "United Republic": 1, "United Provinces": 2, Commonwealth: 1, Heptarchy: 1}; // weighted random
@ -997,7 +997,7 @@ window.BurgsAndStates = (function () {
const form = monarchy[tier];
// Default name depends on exponent tier, some culture bases have special names for tiers
if (s.diplomacy) {
if (form === "Duchy" && s.neighbors.length > 1 && rand(6) < s.neighbors.length && s.diplomacy.includes("Vassal")) return "Marches"; // some vassal dutchies on borderland
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 (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 "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 (tier > 2 && P(0.8) && [18, 17, 28].includes(base)) return "Caliphate"; // Arabic, Berber, Swahili
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 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},
Theocracy: {Parish: 3, Deanery: 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);
});
const name = getFileName("Cells") + ".geojson";
downloadFile(JSON.stringify(json), name, "application/json");
const fileName = getFileName("Cells") + ".geojson";
downloadFile(JSON.stringify(json), fileName, "application/json");
}
function saveGeoJSON_Routes() {
@ -421,30 +421,25 @@ function saveGeoJSON_Routes() {
json.features.push(feature);
});
const name = getFileName("Routes") + ".geojson";
downloadFile(JSON.stringify(json), name, "application/json");
const fileName = getFileName("Routes") + ".geojson";
downloadFile(JSON.stringify(json), fileName, "application/json");
}
function saveGeoJSON_Rivers() {
const json = {type: "FeatureCollection", features: []};
rivers.selectAll("path").each(function () {
const coordinates = getRiverPoints(this);
const id = this.id;
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 river = pack.rivers.find(r => r.i === +this.id.slice(5));
if (!river) return;
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);
});
const name = getFileName("Rivers") + ".geojson";
downloadFile(JSON.stringify(json), name, "application/json");
const fileName = getFileName("Rivers") + ".geojson";
downloadFile(JSON.stringify(json), fileName, "application/json");
}
function saveGeoJSON_Markers() {

View file

@ -828,6 +828,7 @@ function parseLoadedData(data) {
// v 1.65 changed rivers data
d3.select("#rivers").attr("style", null); // remove style to unhide layer
const {cells, rivers} = pack;
const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2);
for (const river of rivers) {
const node = document.getElementById("river" + river.i);
@ -856,7 +857,7 @@ function parseLoadedData(data) {
river.points = riverPoints;
}
river.widthFactor = 1;
river.widthFactor = defaultWidthFactor;
cells.i.forEach(i => {
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);
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();

View file

@ -97,7 +97,7 @@ window.Markers = (function () {
}
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 marker = {i, icon, type, x, y, cell};
if (dx) marker.dx = dx;

View file

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

View file

@ -23,10 +23,14 @@ window.Rivers = (function () {
resolveDepressions(h);
drainWater();
defineRivers();
calculateConfluenceFlux();
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");
@ -34,6 +38,8 @@ window.Rivers = (function () {
const pixel2 = distanceScale * distanceScale
//const MIN_FLUX_TO_FORM_RIVER = 10 * distanceScale;
const MIN_FLUX_TO_FORM_RIVER = 30;
const cellsNumberModifier = (pointsInput.dataset.cells / 10000) ** 0.25;
const prec = grid.cells.prec;
// const area = c => pack.cells.area[c] * pixel2;
const area = pack.cells.area;
@ -41,7 +47,7 @@ window.Rivers = (function () {
const lakeOutCells = Lakes.setClimateData(h);
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
const lakes = lakeOutCells[i] ? features.filter(feature => i === feature.outCell && feature.flux > feature.evaporation) : [];
@ -93,6 +99,15 @@ window.Rivers = (function () {
// cells is depressed
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) {
// flux is too small to operate as a river
if (h[min] >= 20) cells.fl[min] += cells.fl[i];
@ -152,6 +167,9 @@ window.Rivers = (function () {
cells.conf = new Uint16Array(cells.i.length);
pack.rivers = [];
const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2);
const mainStemWidthFactor = defaultWidthFactor * 1.2;
for (const key in riversData) {
const riverCells = riversData[key];
if (riverCells.length < 3) continue; // exclude tiny rivers
@ -169,7 +187,7 @@ window.Rivers = (function () {
const mouth = riverCells[riverCells.length - 2];
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 discharge = cells.fl[mouth]; // m3 in second
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() {
for (const i of cells.i) {
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 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 lengthWidth = pointNumber * STEP_WIDTH + (LENGTH_PROGRESSION[pointNumber] || MAX_PROGRESSION);
return widthFactor * (lengthWidth + fluxWidth) + startingWidth;
};
// build polygon from a list of points and calculated offset (width)
const getRiverPath = function (points, widthFactor = 1, startingWidth = 0) {
const getRiverPath = function (points, widthFactor, startingWidth = 0) {
const riverPointsLeft = [];
const riverPointsRight = [];
@ -447,5 +481,20 @@ window.Rivers = (function () {
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));
document.getElementById("mfcgBurgSeed").addEventListener("change", changeSeed);
document.getElementById("regenerateMFCGBurgSeed").addEventListener("click", randomizeSeed);
document.getElementById("addCustomMFCGBurgLink").addEventListener("click", addCustomMfcgLink);
document.getElementById("burgStyleShow").addEventListener("click", showStyleSection);
document.getElementById("burgStyleHide").addEventListener("click", hideStyleSection);
@ -112,7 +113,13 @@ function editBurg(id) {
if (options.showMFCGMap) {
document.getElementById("mfcgPreviewSection").style.display = "block";
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);
}
} else {
document.getElementById("mfcgPreviewSection").style.display = "none";
}
@ -347,22 +354,25 @@ function editBurg(id) {
function toggleFeature() {
const id = +elSelected.attr("data-id");
const b = pack.burgs[id];
const burg = pack.burgs[id];
const feature = this.dataset.feature;
const turnOn = this.classList.contains("inactive");
if (feature === "port") togglePort(id);
else if (feature === "capital") toggleCapital(id);
else b[feature] = +turnOn;
if (b[feature]) this.classList.remove("inactive");
else if (!b[feature]) this.classList.add("inactive");
else burg[feature] = +turnOn;
if (burg[feature]) this.classList.remove("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";
updateMFCGFrame(burg);
}
function toggleBurgLockButton() {
const id = +elSelected.attr("data-id");
toggleBurgLock(id);
const burg = pack.burgs[id];
burg.lock = !burg.lock;
updateBurgLockIcon();
}
@ -405,7 +415,7 @@ function editBurg(id) {
function updateMFCGFrame(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);
}
@ -426,6 +436,17 @@ function editBurg(id) {
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() {
const id = +elSelected.attr("data-id"),
burg = pack.burgs[id];

View file

@ -7,6 +7,7 @@ function overviewBurgs() {
const body = document.getElementById("burgsBody");
updateFilter();
updateLockAllIcon();
burgsOverviewAddLines();
$("#burgsOverview").dialog();
@ -33,6 +34,7 @@ function overviewBurgs() {
document.getElementById("burgsListToLoad").addEventListener("change", function () {
uploadFile(this, importBurgNames);
});
document.getElementById("burgsLockAll").addEventListener("click", toggleLockAll);
document.getElementById("burgsRemoveAll").addEventListener("click", triggerAllBurgsRemove);
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 province" class="burgState" value="${province}" 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
)}</select>
<span data-tip="Burg population" class="icon-male"></span>
@ -195,8 +197,11 @@ function overviewBurgs() {
}
function toggleBurgLockStatus() {
const burg = +this.parentNode.dataset.id;
toggleBurgLock(burg);
const burgId = +this.parentNode.dataset.id;
const burg = pack.burgs[burgId];
burg.lock = !burg.lock;
if (this.classList.contains("icon-lock")) {
this.classList.remove("icon-lock");
this.classList.add("icon-lock-open");
@ -478,9 +483,9 @@ function overviewBurgs() {
}
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`;
alertMessage.innerHTML = message;
$("#alert").dialog({
title: "Burgs bulk renaming",
@ -562,4 +567,21 @@ function overviewBurgs() {
pack.burgs = pack.burgs.map(burg => ({...burg, lock: !burg.lock}));
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";
function editDiplomacy() {
if (customization) return;
if (pack.states.filter(s => s.i && !s.removed).length < 2) {
tip("There should be at least 2 states to edit the diplomacy", false, "error");
return;
}
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");
const body = document.getElementById("diplomacyBodySection");
closeDialogs("#diplomacyEditor, .stable");
if (!layerIsOn("toggleStates")) toggleStates();
@ -14,21 +13,29 @@ function editDiplomacy() {
if (layerIsOn("toggleBiomes")) toggleBiomes();
if (layerIsOn("toggleReligions")) toggleReligions();
const body = document.getElementById("diplomacyBodySection");
const statuses = ["Ally", "Friendly", "Neutral", "Suspicion", "Enemy", "Unknown", "Rival", "Vassal", "Suzerain"];
const description = [" is an ally of ", " is friendly to ", " is neutral to ", " is suspicious of ",
" is at war with ", " does not know about ", " is a rival of ", " is a vassal of ", " is suzerain to "];
const colors = ["#00b300", "#d4f8aa", "#edeee8", "#eeafaa", "#e64b40", "#a9a9a9", "#ad5a1f", "#87CEFA", "#00008B"];
refreshDiplomacyEditor();
const relations = {
Ally: {inText: "is an ally of", color: "#00b300", tip: "Allies formed a defensive pact and protect each other in case of third party aggression"},
Friendly: {inText: "is friendly to", color: "#d4f8aa", tip: "State is friendly to anouther state when they share some common interests"},
Neutral: {inText: "is neutral to", color: "#edeee8", tip: "Neutral means states relations are neither positive nor negative"},
Suspicion: {inText: "is suspicious of", color: "#eeafaa", tip: "Suspicion means state has a cautious distrust of another state"},
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);
if (modules.editDiplomacy) return;
modules.editDiplomacy = true;
$("#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"}
});
@ -36,10 +43,30 @@ function editDiplomacy() {
document.getElementById("diplomacyEditorRefresh").addEventListener("click", refreshDiplomacyEditor);
document.getElementById("diplomacyEditStyle").addEventListener("click", () => editStyle("regions"));
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("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() {
diplomacyEditorAddLines();
@ -50,33 +77,36 @@ function editDiplomacy() {
function diplomacyEditorAddLines() {
const states = pack.states;
const selectedLine = body.querySelector("div.Self");
const sel = selectedLine ? +selectedLine.dataset.id : states.find(s => s.i && !s.removed).i;
const selName = states[sel].fullName;
diplomacySelect.style.display = "none";
const selectedId = selectedLine ? +selectedLine.dataset.id : states.find(s => s.i && !s.removed).i;
const selectedName = states[selectedId].name;
COArenderer.trigger("stateCOA"+sel, states[sel].coa);
let lines = `<div class="states Self" data-id=${sel} data-tip="List below shows relations to ${selName}">
<div style="width: max-content">${selName}</div>
<svg class="coaIcon" viewBox="0 0 200 200"><use href="#stateCOA${sel}"></use></svg>
COArenderer.trigger("stateCOA" + selectedId, states[selectedId].coa);
let lines = `<div class="states Self" data-id=${selectedId} data-tip="List below shows relations to ${selectedName}">
<div style="width: max-content">${states[selectedId].fullName}</div>
<svg class="coaIcon" viewBox="0 0 200 200"><use href="#stateCOA${selectedId}"></use></svg>
</div>`;
for (const s of states) {
if (!s.i || s.removed || s.i === sel) continue;
const relation = s.diplomacy[sel];
const index = statuses.indexOf(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);
for (const state of states) {
if (!state.i || state.removed || state.i === selectedId) continue;
const relation = state.diplomacy[selectedId];
const {color, inText} = relations[relation];
lines += `<div class="states" data-id=${s.i} data-name="${s.fullName}" data-relations="${relation}">
<svg data-tip="${tipSelect}" class="coaIcon" viewBox="0 0 200 200"><use href="#stateCOA${s.i}"></use></svg>
<div data-tip="${tipSelect}" style="width:12em">${s.fullName}</div>
<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 tip = `${state.name} ${inText} ${selectedName}`;
const tipSelect = `${tip}. Click to see relations to ${state.name}`;
const tipChange = `Click to change relations. ${tip}`;
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>
<input data-tip="${tipChange}" class="changeRelations diplomacyRelations" value="${relation}" readonly/>
${relation}
</div>
</div>`;
}
body.innerHTML = lines;
@ -84,8 +114,6 @@ function editDiplomacy() {
// add listeners
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("click", selectStateOnLineClick));
body.querySelectorAll(".changeRelations").forEach(el => el.addEventListener("click", toggleDiplomacySelect));
applySorting(diplomacyHeader);
$("#diplomacyEditor").dialog();
@ -95,19 +123,31 @@ function editDiplomacy() {
if (!layerIsOn("toggleStates")) return;
const state = +event.target.dataset.id;
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)
.attr("fill", "none").attr("stroke", "red").attr("stroke-width", 1).attr("opacity", 1)
const path = debug
.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)");
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);
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) {
debug.selectAll(".highlight").each(function() {
debug.selectAll(".highlight").each(function () {
d3.select(this).transition().duration(1000).attr("opacity", 0).remove();
});
}
@ -118,22 +158,17 @@ function editDiplomacy() {
if (!sel) return;
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
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() {
if (this.classList.contains("Self")) return;
body.querySelector("div.Self").classList.remove("Self");
this.classList.add("Self");
refreshDiplomacyEditor();
const relation = pack.states[id].diplomacy[sel];
const color = relations[relation]?.color || "#4682b4";
this.setAttribute("fill", color);
statesBody.select("#state-gap" + id).attr("stroke", color);
statesHalo.select("#state-border" + id).attr("stroke", d3.color(color).darker().hex());
});
}
function selectStateOnMapClick() {
@ -145,42 +180,63 @@ function editDiplomacy() {
if (+selectedLine.dataset.id === state) return;
selectedLine.classList.remove("Self");
body.querySelector("div[data-id='"+state+"']").classList.add("Self");
body.querySelector("div[data-id='" + state + "']").classList.add("Self");
refreshDiplomacyEditor();
}
function toggleDiplomacySelect(event) {
event.stopPropagation();
const select = document.getElementById("diplomacySelect");
const show = select.style.display === "none";
if (!show) {select.style.display = "none"; return;}
select.style.display = "block";
const input = event.target.closest("div").querySelector("input");
select.style.left = input.getBoundingClientRect().left + "px";
select.style.top = input.getBoundingClientRect().bottom + "px";
body.dataset.state = event.target.closest("div.states").dataset.id;
function selectRelation(subjectId, objectId, currentRelation) {
const states = pack.states;
const subject = states[subjectId];
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>`;
const options = Object.entries(relations)
.map(
([relation, {color, inText, tip}]) =>
`<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) {
event.stopPropagation();
diplomacySelect.style.display = "none";
const subject = +body.dataset.state;
const rel = event.target.innerHTML;
function changeRelation(subjectId, objectId, oldRelation, newRelation) {
if (newRelation === oldRelation) return;
const states = pack.states;
const chronicle = states[0].diplomacy;
const states = pack.states, chronicle = states[0].diplomacy;
const selectedLine = body.querySelector("div.Self");
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 subjectName = states[subjectId].name;
const objectName = states[objectId].name;
const oldRel = states[subject].diplomacy[object];
if (rel === oldRel) return;
states[subject].diplomacy[object] = rel;
states[object].diplomacy[subject] = rel === "Vassal" ? "Suzerain" : rel === "Suzerain" ? "Vassal" : rel;
states[subjectId].diplomacy[objectId] = newRelation;
states[objectId].diplomacy[subjectId] = newRelation === "Vassal" ? "Suzerain" : newRelation === "Suzerain" ? "Vassal" : newRelation;
// 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 vassal = () => [`Vassalization`, `${subjectName} became a vassal of ${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 peace = () => {
const treaty = `${subjectName} and ${objectName} agreed to cease fire and signed a peace treaty`;
const changed = rel === "Ally" ? ally()
: rel === "Vassal" ? vassal()
: rel === "Suzerain" ? suzerain()
: rel === "Unknown" ? unknown()
const changed =
newRelation === "Ally"
? ally()
: newRelation === "Vassal"
? vassal()
: newRelation === "Suzerain"
? suzerain()
: newRelation === "Unknown"
? unknown()
: change();
return [`War termination`, treaty, changed[1]];
}
};
if (oldRel === "Enemy") chronicle.push(peace()); else
if (rel === "Enemy") chronicle.push(war()); else
if (rel === "Vassal") chronicle.push(vassal()); else
if (rel === "Suzerain") chronicle.push(suzerain()); else
if (rel === "Ally") chronicle.push(ally()); else
if (rel === "Unknown") chronicle.push(unknown()); else
if (rel === "Rival") chronicle.push(rival()); else
chronicle.push(change());
if (oldRelation === "Enemy") chronicle.push(peace());
else if (newRelation === "Enemy") chronicle.push(war());
else if (newRelation === "Vassal") chronicle.push(vassal());
else if (newRelation === "Suzerain") chronicle.push(suzerain());
else if (newRelation === "Ally") chronicle.push(ally());
else if (newRelation === "Unknown") chronicle.push(unknown());
else if (newRelation === "Rival") chronicle.push(rival());
else chronicle.push(change());
refreshDiplomacyEditor();
if (diplomacyMatrix.offsetParent) {
document.getElementById("diplomacyMatrixBody").innerHTML = "";
showRelationsMatrix();
}
}
function regenerateRelations() {
@ -214,28 +279,52 @@ function editDiplomacy() {
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() {
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">`;
chronicle.forEach((e, d) => {
chronicle.forEach((entry, d) => {
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>`;
});
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));
$("#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: {
Save: function() {
Save: function () {
const data = this.querySelector("div").innerText.split("\n").join("\r\n");
const name = getFileName("Relations history") + ".txt";
downloadFile(data, name);
},
Clear: function() {pack.states[0].diplomacy = []; $(this).dialog("close");},
Close: function() {$(this).dialog("close");}
Clear: function () {
pack.states[0].diplomacy = [];
$(this).dialog("close");
},
Close: function () {
$(this).dialog("close");
}
}
});
}
@ -251,22 +340,48 @@ function editDiplomacy() {
function showRelationsMatrix() {
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>`;
message += states.map(s => `<th data-tip='See relations to ${s.fullName}'>${s.name}</th>`).join("") + `</tr>`; // headers
states.forEach(s => {
message += `<tr><th data-tip='See relations of ${s.fullName}'>${s.name}</th>` + s.diplomacy
.filter((v, i) => valid.includes(i)).map((r, i) => {
const desc = description[statuses.indexOf(r)];
const tip = desc ? s.fullName + desc + pack.states[valid[i]].fullName : '&#8205;';
return `<td data-tip='${tip}' class='${r}'>${r}</td>`
}).join("") + "</tr>";
let table = `<table><thead><tr><th data-tip='&#8205;'></th>`;
table += states.map(state => `<th data-tip='Relations to ${state.fullName}'>${state.name}</th>`).join("") + `</tr>`;
table += `<tbody>`;
states.forEach(state => {
table +=
`<tr data-id=${state.i}><th data-tip='Relations of ${state.fullName}'>${state.name}</th>` +
state.diplomacy
.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() {
@ -288,7 +403,8 @@ function editDiplomacy() {
clearMainTip();
const selected = body.querySelector("div.Self");
if (selected) selected.classList.remove("Self");
if (layerIsOn("toggleStates")) drawStates(); else toggleStates();
if (layerIsOn("toggleStates")) drawStates();
else toggleStates();
debug.selectAll(".highlight").remove();
}
}

View file

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

View file

@ -460,32 +460,31 @@ function showInfo() {
const Discord = link("https://discordapp.com/invite/X7E84HU", "Discord");
const Reddit = link("https://www.reddit.com/r/FantasyMapGenerator", "Reddit");
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 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 VideoTutorial = link("https://youtube.com/playlist?list=PLtgiuDC8iVR2gIG8zMTRn7T_L0arl9h1C", "Video tutorial");
alertMessage.innerHTML = `
<b>Fantasy Map Generator</b> (FMG) is an open-source application, it means the code is published an anyone can use it.
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.
<b>Fantasy Map Generator</b> (FMG) is a free open-source application.
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>
<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">
<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/wiki/Changelog", "Changelog")}</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>`;
$("#alert").dialog({

View file

@ -470,23 +470,26 @@ function togglePrec(event) {
function drawPrec() {
prec.selectAll("circle").remove();
const cells = grid.cells,
p = grid.points;
const {cells, points} = grid;
prec.style("display", "block");
const show = d3.transition().duration(800).ease(d3.easeSinIn);
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 getRadius = prec => rn(Math.sqrt(prec / 4) / cellsNumberModifier, 2);
prec
.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", d => p[d][0])
.attr("cy", d => p[d][1])
.attr("cx", d => points[d][0])
.attr("cy", d => points[d][1])
.attr("r", 0)
.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) {

View file

@ -17,18 +17,24 @@ function editNamesbase() {
document.getElementById("namesbaseMax").addEventListener("input", updateBaseMax);
document.getElementById("namesbaseDouble").addEventListener("input", updateBaseDublication);
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("namesbaseDownload").addEventListener("click", namesbaseDownload);
document.getElementById("namesbaseUpload").addEventListener("click", () => namesbaseToLoad.click());
document.getElementById("namesbaseToLoad").addEventListener("change", function() {uploadFile(this, namesbaseUpload)});
document.getElementById("namesbaseUpload").addEventListener("click", () => document.getElementById("namesbaseToLoad").click());
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));
createBasesList();
updateInputs();
$("#namesbaseEditor").dialog({
title: "Namesbase Editor", width: "42.5em",
title: "Namesbase Editor",
width: "auto",
position: {my: "center", at: "center", of: "svg"}
});
@ -40,7 +46,10 @@ function editNamesbase() {
function updateInputs() {
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("namesbaseName").value = nameBases[base].name;
document.getElementById("namesbaseMin").value = nameBases[base].min;
@ -52,7 +61,7 @@ function editNamesbase() {
function updateExamples() {
const base = +document.getElementById("namesbaseSelect").value;
let examples = "";
for (let i=0; i < 10; i++) {
for (let i = 0; i < 10; i++) {
const example = Names.getBase(base);
if (example === undefined) {
examples = "Cannot generate examples. Please verify the data";
@ -84,13 +93,19 @@ function editNamesbase() {
function updateBaseMin() {
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;
}
function updateBaseMax() {
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;
}
@ -99,59 +114,70 @@ function editNamesbase() {
nameBases[base].d = this.value;
}
function analizeNamesbase() {
const string = document.getElementById("namesbaseTextarea").value;
if (!string) {tip("Names data field should not be empty", false, "error"); return;}
const base = string.toLowerCase();
const array = base.split(",");
const l = array.length;
if (!l) {tip("Names data should not be empty", false, "error"); return;}
function analyzeNamesbase() {
const namesSourceString = document.getElementById("namesbaseTextarea").value;
const namesArray = namesSourceString.toLowerCase().split(",");
const length = namesArray.length;
if (!namesSourceString || !length) return tip("Names data should not be empty", false, "error");
const wordsLength = array.map(n => n.length);
const multi = rn(d3.mean(array.map(n => (n.match(/ /i)||[]).length)) * 100, 2);
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 chain = Names.calculateChain(namesSourceString);
const variety = rn(d3.mean(Object.values(chain).map(keyValue => keyValue.length)));
const lengthStat =
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 wordsLength = namesArray.map(n => n.length);
const rangeStat =
l < 10 ? "<span style='color:red'>[low]</span>" :
l < 15 ? "<span style='color:darkred'>[low]</span>" :
l < 20 ? "<span style='color:orange'>[low]</span>" :
"<span style='color:green'>[good]</span>";
const nonLatin = namesSourceString.match(/[^\u0000-\u007f]/g);
const nonBasicLatinChars = nonLatin
? unique(
namesSourceString
.match(/[^\u0000-\u007f]/g)
.join("")
.toLowerCase()
).join("")
: "none";
const depthStat =
l < 15 ? "<span style='color:red'>[low]</span>" :
l < 20 ? "<span style='color:darkred'>[low]</span>" :
l < 25 ? "<span style='color:orange'>[low]</span>" :
"<span style='color:green'>[good]</span>";
const geminate = namesArray.map(name => name.match(/[^\w\s]|(.)(?=\1)/g) || []).flat();
const doubled = unique(geminate).filter(char => geminate.filter(doudledChar => doudledChar === char).length > 3) || ["none"];
const duplicates = unique(namesArray.filter((e, i, a) => a.indexOf(e) !== i)).join(", ") || "none";
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">
<div>Namesbase length: ${l} ${lengthStat}</div>
<div>Namesbase range: ${Object.keys(chain).length-1} ${rangeStat}</div>
<div>Namesbase depth: ${depth} ${depthStat}</div>
<div>Non-basic chars: ${nonLatin}</div>
<div data-tip="Number of names provided">Namesbase length: ${length} ${getLengthQuality()}</div>
<div data-tip="Average number of generation variants for each key in the chain">Namesbase variety: ${variety} ${getVarietyLevel()}</div>
<hr>
<div>Min name length: ${d3.min(wordsLength)}</div>
<div>Max name length: ${d3.max(wordsLength)}</div>
<div>Mean name length: ${rn(d3.mean(wordsLength), 1)}</div>
<div>Median name length: ${d3.median(wordsLength)}</div>
<div>Doubled chars: ${doubled}</div>
<div>Multi-word names: ${multi}%</div>
<div data-tip="The shortest name length">Min name length: ${d3.min(wordsLength)}</div>
<div data-tip="The longest name length">Max name length: ${d3.max(wordsLength)}</div>
<div data-tip="Average name length">Mean name length: ${rn(d3.mean(wordsLength), 1)}</div>
<div data-tip="Common name length">Median name length: ${d3.median(wordsLength)}</div>
<hr>
<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>`;
$("#alert").dialog({
resizable: false, title: "Data Analysis",
resizable: false,
title: "Data Analysis",
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() {
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: {
Restore: function() {
Restore: function () {
$(this).dialog("close");
Names.clearChains();
nameBases = Names.getNameBases();
createBasesList();
updateInputs();
},
Cancel: function() {$(this).dialog("close");}
Cancel: function () {
$(this).dialog("close");
}
}
});
}
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";
downloadFile(data, name);
}
function namesbaseUpload(dataLoaded) {
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();
nameBases = [];
data.forEach(d => {
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();

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,
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,
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
.replace(/(?:\r\n|\r|\n)/g, "")
@ -287,18 +288,16 @@ function generateMapWithSeed(source) {
}
function showSeedHistoryDialog() {
const alert = mapHistory
.map(function (h, i) {
const lines = mapHistory.map((h, i) => {
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>`;
return `<div>${i + 1}. Seed: ${h.seed} ${button}. Size: ${h.width}x${h.height}. Template: ${h.template}. Created: ${created}</div>`;
})
.join("");
alertMessage.innerHTML = alert;
const button = `<i data-tip="Click to generate a map with this seed" onclick="restoreSeed(${i})" class="icon-history optionsSeedRestore"></i>`;
return `<li>Seed: ${h.seed} ${button}. Size: ${h.width}x${h.height}. Template: ${h.template}. Created: ${created}</li>`;
});
alertMessage.innerHTML = `<ol style="margin: 0; padding-left: 1.5em">${lines.join("")}</ol>`;
$("#alert").dialog({
resizable: false,
title: "Seed history",
width: fitContent(),
position: {my: "center", at: "center", of: "svg"}
});
}

View file

@ -137,7 +137,7 @@ function editProvinces() {
p.color
}" class="fillRect pointer"></svg>
<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>
<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 ${

View file

@ -89,7 +89,8 @@ function createRiver() {
const source = riverCells[0];
const mouth = parent === riverId ? last(riverCells) : riverCells[riverCells.length - 2];
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);

View file

@ -136,7 +136,7 @@ function editStates() {
s.color
}" class="fillRect pointer"></svg>
<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>
<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"/>

View file

@ -831,7 +831,7 @@ function applyDefaultStyle() {
landmass.attr("opacity", 1).attr("fill", "#eef6fb").attr("filter", null);
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.select("#rural").attr("stroke", "#0000ff");
population.select("#urban").attr("stroke", "#ff0000");
@ -938,7 +938,7 @@ function applyDefaultStyle() {
.attr("stroke-linecap", "round");
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
.select("#cities")
.attr("fill", "#3e3e4b")
@ -979,7 +979,7 @@ function applyDefaultStyle() {
.attr("stroke-linecap", "butt");
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
.select("#states")
.attr("fill", "#3e3e4b")

View file

@ -143,7 +143,7 @@ function regenerateStates() {
const localSeed = Math.floor(Math.random() * 1e9); // new random seed
Math.random = aleaPRNG(localSeed);
const statesCount = +regionsInput.value;
const statesCount = +regionsOutput.value;
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 < 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 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 discharge = cells.fl[mouth]; // m3 in second
@ -739,7 +741,7 @@ function configMarkersGeneration() {
const inputId = `markerIconInput${index}`;
return `<tr>
<td><input value="${type}" /></td>
<td>
<td style="position: relative">
<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>
</td>

View file

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