Merge branch 'Azgaar:master' into grid-overlay-layer

This commit is contained in:
Ángel Montero Lamas 2024-04-09 20:21:25 +02:00 committed by GitHub
commit 20a9102b6e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 657 additions and 475 deletions

View file

@ -1,5 +1,3 @@
[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://stand-with-ukraine.pp.ua)
# Fantasy Map Generator # Fantasy Map Generator
Azgaar's _Fantasy Map Generator_ is a free web application that helps fantasy writers, game masters, and cartographers create and edit fantasy maps. Azgaar's _Fantasy Map Generator_ is a free web application that helps fantasy writers, game masters, and cartographers create and edit fantasy maps.
@ -8,19 +6,17 @@ Link: [azgaar.github.io/Fantasy-Map-Generator](https://azgaar.github.io/Fantasy-
Refer to the [project wiki](https://github.com/Azgaar/Fantasy-Map-Generator/wiki) for guidance. The current progress is tracked in [Trello](https://trello.com/b/7x832DG4/fantasy-map-generator). Some details are covered in my old blog [_Fantasy Maps for fun and glory_](https://azgaar.wordpress.com). Refer to the [project wiki](https://github.com/Azgaar/Fantasy-Map-Generator/wiki) for guidance. The current progress is tracked in [Trello](https://trello.com/b/7x832DG4/fantasy-map-generator). Some details are covered in my old blog [_Fantasy Maps for fun and glory_](https://azgaar.wordpress.com).
[![preview](https://cdn.discordapp.com/attachments/587406457725779968/594840629213659136/preview1.png)](https://i.redd.it/8bf81ir2cy631.png) [![preview](https://github.com/Azgaar/Fantasy-Map-Generator/assets/26469650/9502eae9-92e0-4d0d-9f17-a2ba4a565c01)](https://github.com/Azgaar/Fantasy-Map-Generator/assets/26469650/11a42446-4bd5-4526-9cb1-3ef97c868992)
[![preview](https://cdn.discordapp.com/attachments/587406457725779968/594840633911279636/preview2.png)](https://cdn.discordapp.com/attachments/515359185664344071/593888810782162964/The_Wichin_Island_sepia.png) [![preview](https://github.com/Azgaar/Fantasy-Map-Generator/assets/26469650/e751a9e5-7986-4638-b8a9-362395ef7583)](https://github.com/Azgaar/Fantasy-Map-Generator/assets/26469650/e751a9e5-7986-4638-b8a9-362395ef7583)
[![preview](https://cdn.discordapp.com/attachments/587406457725779968/594840632296734720/preview3.png)](https://cdn.discordapp.com/attachments/515359096925454350/593891237984206848/The_Wichin_Island_-_diplomacy.png) [![preview](https://github.com/Azgaar/Fantasy-Map-Generator/assets/26469650/b0d0efde-a0d1-4e80-8818-ea3dd83c2323)](https://github.com/Azgaar/Fantasy-Map-Generator/assets/26469650/b0d0efde-a0d1-4e80-8818-ea3dd83c2323)
Join our [Discord server](https://discordapp.com/invite/X7E84HU) and [Reddit community](https://www.reddit.com/r/FantasyMapGenerator) to share your creations, discuss the Generator, suggest ideas and get the most recent updates. Join our [Discord server](https://discordapp.com/invite/X7E84HU) and [Reddit community](https://www.reddit.com/r/FantasyMapGenerator) to share your creations, discuss the Generator, suggest ideas and get the most recent updates.
Contact me via [email](mailto:azgaar.fmg@yandex.by) if you have non-public suggestions. For bug reports please use [GitHub issues](https://github.com/Azgaar/Fantasy-Map-Generator/issues) or _#bugs_ channel on Discord. If you are facing performance issues, please read [the tips](https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Tips#performance-tips). Contact me via [email](mailto:azgaar.fmg@yandex.com) if you have non-public suggestions. For bug reports please use [GitHub issues](https://github.com/Azgaar/Fantasy-Map-Generator/issues) or _#fmg-bugs_ channel on Discord. If you are facing performance issues, please read [the tips](https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Tips#performance-tips).
Electron desktop application is available in [releases](https://github.com/Azgaar/Fantasy-Map-Generator/releases). Download archive for your architecture, unzip and run. Pull requests are highly welcomed. The codebase is messy and requires re-design. I will appreciate if you start with minor changes. Check out the [data model](https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Data-model) before contributing.
Pull requests are highly welcomed. The codebase is messy and requires re-design, but I will appreciate if you start with minor changes. Check out the [data model](https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Data-model) before contributing.
You can support the project on [Patreon](https://www.patreon.com/azgaar). You can support the project on [Patreon](https://www.patreon.com/azgaar).

View file

@ -3335,6 +3335,22 @@
<input id="burgPopulation" type="number" min="0" step="1" style="width: 8em" /> <input id="burgPopulation" type="number" min="0" step="1" style="width: 8em" />
</div> </div>
<div data-tip="Burg mean annual temperature and real-world city for comparison">
<div class="label">Temperature:</div>
<span id="burgTemperature"></span>, like in
<span id="burgTemperatureLikeIn"></span>
<i
id="burgTemperatureGraph"
data-tip="Show temperature graph for the burg"
class="icon-chart-area pointer"
></i>
</div>
<div data-tip="Burg height above mean sea level">
<div class="label">Elevation:</div>
<span id="burgElevation"></span> above sea level
</div>
<div> <div>
<div class="label">Features:</div> <div class="label">Features:</div>
<span <span
@ -3384,49 +3400,23 @@
style="font-size: 1em" style="font-size: 1em"
></span> ></span>
</div> </div>
<div data-tip="Burg mean annual temperature and real-world city for comparison">
<div class="label">Temperature:</div>
<span id="burgTemperature"></span>, like in
<span id="burgTemperatureLikeIn"></span>
<i
id="burgTemperatureGraph"
data-tip="Show temperature graph for the burg"
class="icon-chart-area pointer"
></i>
</div>
<div data-tip="Burg height above mean sea level">
<div class="label">Elevation:</div>
<span id="burgElevation"></span> above sea level
</div>
</div> </div>
</div> </div>
<div <div id="burgPreviewSection" data-tip="Burg map preview" style="display: flex; flex-direction: column">
id="mfcgPreviewSection" <div style="display: flex; justify-content: space-between">
data-tip="Burg preview in the Medieval Fantasy City Generator. Default seed is a combination of map seed and burg id" <span>Burg preview:</span>
style="display: flex; flex-direction: column" <div style="display: flex; gap: 0.5em">
>
<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 <i
id="regenerateMFCGBurgSeed" id="burgLinkEdit"
data-tip="Randomize Medieval Fantasy City Generator burg seed"
class="icon-arrows-cw pointer"
style="margin-left: 0.1em"
></i>
</div>
<i
id="addCustomMFCGBurgLink"
data-tip="Provide custom link to the burg map" data-tip="Provide custom link to the burg map"
class="icon-pencil pointer" class="icon-pencil pointer"
style="margin-left: 0.1em" style="margin-top: -0.1em"
></i> ></i>
<i id="burgLinkOpen" data-tip="Open burg map in a new tab" class="icon-link-ext pointer"></i>
</div> </div>
<iframe id="mfcgPreview" sandbox="allow-scripts"></iframe> </div>
<div id="burgPreviewObject" style="max-width: 30em; pointer-events: none"></div>
</div> </div>
</div> </div>
@ -3466,7 +3456,7 @@
</div> </div>
<button id="burgEditEmblem" data-tip="Edit emblem" class="icon-shield-alt"></button> <button id="burgEditEmblem" data-tip="Edit emblem" class="icon-shield-alt"></button>
<button id="burgToggleMFCGMap" data-tip="Toggle MFCG map" class="icon-map"></button> <button id="burgTogglePreview" data-tip="Toggle preview" class="icon-map"></button>
<button id="burgRelocate" data-tip="Relocate burg" class="icon-target"></button> <button id="burgRelocate" data-tip="Relocate burg" class="icon-target"></button>
<button id="burglLegend" data-tip="Edit free text notes (legend) for this burg" class="icon-edit"></button> <button id="burglLegend" data-tip="Edit free text notes (legend) for this burg" class="icon-edit"></button>
<button id="burgLock" class="icon-lock-open" onmouseover="showElementLockTip(event)"></button> <button id="burgLock" class="icon-lock-open" onmouseover="showElementLockTip(event)"></button>
@ -5165,6 +5155,8 @@
<option value="km">Kilometer (km)</option> <option value="km">Kilometer (km)</option>
<option value="lg">League (lg)</option> <option value="lg">League (lg)</option>
<option value="vr">Versta (vr)</option> <option value="vr">Versta (vr)</option>
<option value="nmi">Nautical mile (nmi)</option>
<option value="nlg">Nautical league (nlg)</option>
<option value="custom_name">Custom name</option> <option value="custom_name">Custom name</option>
</select> </select>
</div> </div>
@ -5603,7 +5595,9 @@
<th data-tip="Average number of people in crew (used for total personnel calculation)">Crew</th> <th data-tip="Average number of people in crew (used for total personnel calculation)">Crew</th>
<th data-tip="Unit military power (used for battle simulation)">Power</th> <th data-tip="Unit military power (used for battle simulation)">Power</th>
<th data-tip="Unit type to apply special rules on forces recalculation">Type</th> <th data-tip="Unit type to apply special rules on forces recalculation">Type</th>
<th data-tip="Check if unit is separate and can be stacked only with units of the same type">Sep.</th> <th data-tip="Check if unit is separate and can be stacked only with units of the same type">
Separate
</th>
</tr> </tr>
</thead> </thead>
<tbody></tbody> <tbody></tbody>
@ -5769,7 +5763,7 @@
<table id="iconTable" class="table pointer" style="font-size: 2em; text-align: center; width: 100%"></table> <table id="iconTable" class="table pointer" style="font-size: 2em; text-align: center; width: 100%"></table>
<div style="font-style: italic; font-size: 1.2em; margin: 0.4em 0 0 0.4em"> <div style="font-style: italic; font-size: 1.2em; margin: 0.4em 0 0 0.4em">
<span>Select from the list or paste a Unicode character here: </span> <span>Select from the list or paste a Unicode character here: </span>
<input id="iconInput" style="width: 2em" /> <input id="iconInput" style="width: 2.5em" />
<span>. See <a href="https://emojipedia.org" target="_blank">Emojipedia</a> for reference</span> <span>. See <a href="https://emojipedia.org" target="_blank">Emojipedia</a> for reference</span>
</div> </div>
</div> </div>
@ -6046,7 +6040,7 @@
</div> </div>
<div id="exportToPngTilesScreen" style="display: none" class="dialog"> <div id="exportToPngTilesScreen" style="display: none" class="dialog">
<p>Map will be split into tiles and downloaded as a single zip file. Avoid saving to big images</p> <p>Map will be split into tiles and downloaded as a single zip file. Avoid saving too large images</p>
<div data-tip="Number of columns" style="margin-bottom: 0.3em"> <div data-tip="Number of columns" style="margin-bottom: 0.3em">
<div class="label">Columns:</div> <div class="label">Columns:</div>
<input <input
@ -6054,7 +6048,7 @@
data-stored="tileCols" data-stored="tileCols"
type="range" type="range"
min="2" min="2"
max="20" max="26"
value="8" value="8"
style="width: 10em" style="width: 10em"
/> />
@ -6067,7 +6061,7 @@
data-stored="tileRows" data-stored="tileRows"
type="range" type="range"
min="2" min="2"
max="20" max="26"
value="8" value="8"
style="width: 10em" style="width: 10em"
/> />
@ -6090,7 +6084,7 @@
<div class="label">Total size:</div> <div class="label">Total size:</div>
<div id="tileSize" style="display: inline-block">1000 x 1000 px</div> <div id="tileSize" style="display: inline-block">1000 x 1000 px</div>
</div> </div>
<div id="tileStatus" style="background-color: #33333310; font-style: italic"></div> <div id="tileStatus" style="font-style: italic"></div>
</div> </div>
<div id="resampleDialog" style="display: none" class="dialog"> <div id="resampleDialog" style="display: none" class="dialog">
@ -8064,12 +8058,12 @@
<script src="modules/lakes.js"></script> <script src="modules/lakes.js"></script>
<script src="modules/biomes.js"></script> <script src="modules/biomes.js"></script>
<script src="modules/names-generator.js?v=1.87.14"></script> <script src="modules/names-generator.js?v=1.87.14"></script>
<script src="modules/cultures-generator.js?v=1.96.00"></script> <script src="modules/cultures-generator.js?v=1.96.05"></script>
<script src="modules/renderers/state-labels.js"></script> <script src="modules/renderers/state-labels.js?v=1.96.04"></script>
<script src="modules/burgs-and-states.js?v=1.92.00"></script> <script src="modules/burgs-and-states.js?v=1.97.00"></script>
<script src="modules/routes-generator.js"></script> <script src="modules/routes-generator.js"></script>
<script src="modules/religions-generator.js?v=1.93.08"></script> <script src="modules/religions-generator.js?v=1.93.08"></script>
<script src="modules/military-generator.js?v=1.96.00"></script> <script src="modules/military-generator.js?v=1.96.01"></script>
<script src="modules/markers-generator.js?v=1.93.04"></script> <script src="modules/markers-generator.js?v=1.93.04"></script>
<script src="modules/coa-generator.js?v=1.91.05"></script> <script src="modules/coa-generator.js?v=1.91.05"></script>
<script src="modules/submap.js?v=1.96.00"></script> <script src="modules/submap.js?v=1.96.00"></script>
@ -8083,13 +8077,13 @@
<script src="modules/ui/general.js?v=1.96.00"></script> <script src="modules/ui/general.js?v=1.96.00"></script>
<script src="modules/ui/options.js?v=1.96.00"></script> <script src="modules/ui/options.js?v=1.96.00"></script>
<script src="main.js?v=1.96.00"></script> <script src="main.js?v=1.97.00"></script>
<script defer src="modules/relief-icons.js"></script> <script defer src="modules/relief-icons.js"></script>
<script defer src="modules/ui/style.js?v=1.96.00"></script> <script defer src="modules/ui/style.js?v=1.96.00"></script>
<script defer src="modules/ui/editors.js?v=1.96.00"></script> <script defer src="modules/ui/editors.js?v=1.97.00"></script>
<script defer src="modules/ui/tools.js?v=1.95.01"></script> <script defer src="modules/ui/tools.js?v=1.96.03"></script>
<script defer src="modules/ui/world-configurator.js?v=1.91.05"></script> <script defer src="modules/ui/world-configurator.js?v=1.98.00"></script>
<script defer src="modules/ui/heightmap-editor.js?v=1.96.00"></script> <script defer src="modules/ui/heightmap-editor.js?v=1.96.00"></script>
<script defer src="modules/ui/provinces-editor.js?v=1.96.00"></script> <script defer src="modules/ui/provinces-editor.js?v=1.96.00"></script>
<script defer src="modules/ui/biomes-editor.js?v=1.91.05"></script> <script defer src="modules/ui/biomes-editor.js?v=1.91.05"></script>
@ -8104,14 +8098,14 @@
<script defer src="modules/ui/rivers-editor.js"></script> <script defer src="modules/ui/rivers-editor.js"></script>
<script defer src="modules/ui/rivers-creator.js?v=1.89.13"></script> <script defer src="modules/ui/rivers-creator.js?v=1.89.13"></script>
<script defer src="modules/ui/relief-editor.js"></script> <script defer src="modules/ui/relief-editor.js"></script>
<script defer src="modules/ui/burg-editor.js?v=1.96.00"></script> <script defer src="modules/ui/burg-editor.js?v=1.97.00"></script>
<script defer src="modules/ui/units-editor.js?v=1.96.00"></script> <script defer src="modules/ui/units-editor.js?v=1.96.00"></script>
<script defer src="modules/ui/notes-editor.js?v=1.93.09"></script> <script defer src="modules/ui/notes-editor.js?v=1.93.09"></script>
<script defer src="modules/ui/diplomacy-editor.js?v=1.88.04"></script> <script defer src="modules/ui/diplomacy-editor.js?v=1.88.04"></script>
<script defer src="modules/ui/zones-editor.js"></script> <script defer src="modules/ui/zones-editor.js"></script>
<script defer src="modules/ui/burgs-overview.js?v=1.96.00"></script> <script defer src="modules/ui/burgs-overview.js?v=1.97.00"></script>
<script defer src="modules/ui/rivers-overview.js"></script> <script defer src="modules/ui/rivers-overview.js"></script>
<script defer src="modules/ui/military-overview.js"></script> <script defer src="modules/ui/military-overview.js?v=1.96.07"></script>
<script defer src="modules/ui/regiments-overview.js?v=1.89.20"></script> <script defer src="modules/ui/regiments-overview.js?v=1.89.20"></script>
<script defer src="modules/ui/markers-overview.js?v=1.89.38"></script> <script defer src="modules/ui/markers-overview.js?v=1.89.38"></script>
<script defer src="modules/ui/regiment-editor.js"></script> <script defer src="modules/ui/regiment-editor.js"></script>
@ -8125,10 +8119,9 @@
<script defer src="libs/rgbquant.min.js"></script> <script defer src="libs/rgbquant.min.js"></script>
<script defer src="libs/jquery.ui.touch-punch.min.js"></script> <script defer src="libs/jquery.ui.touch-punch.min.js"></script>
<script defer src="modules/io/save.js?v=1.96.00"></script> <script defer src="modules/io/save.js?v=1.96.00"></script>
<script defer src="modules/io/load.js?v=1.96.00"></script> <script defer src="modules/io/load.js?v=1.97.04"></script>
<script defer src="modules/io/cloud.js?v=1.96.00"></script> <script defer src="modules/io/cloud.js?v=1.96.00"></script>
<script defer src="modules/io/export.js?v=1.96.00"></script> <script defer src="modules/io/export.js?v=1.97.03"></script>
<script defer src="modules/io/formats.js"></script>
<!-- Web Components --> <!-- Web Components -->
<script defer src="components/fill-box.js"></script> <script defer src="components/fill-box.js"></script>

16
libs/jquery-ui.css vendored
View file

@ -313,8 +313,9 @@ body .ui-dialog {
background-color: inherit; background-color: inherit;
} }
.ui-dialog .ui-dialog-titlebar { .ui-dialog .ui-dialog-titlebar {
padding: 0.4em 1em; display: flex;
position: relative; padding: 0.4em 0.3em;
justify-content: space-evenly;
font-size: 1.2em; font-size: 1.2em;
min-width: 150px; min-width: 150px;
} }
@ -328,9 +329,6 @@ body .ui-dialog {
} }
.ui-dialog .ui-dialog-titlebar button { .ui-dialog .ui-dialog-titlebar button {
position: absolute;
right: 0.5em;
top: 53%;
padding: 0; padding: 0;
width: 1.8em; width: 1.8em;
height: 1.8em; height: 1.8em;
@ -340,14 +338,6 @@ body .ui-dialog {
border: 1px solid #c5c5c5; border: 1px solid #c5c5c5;
} }
.ui-dialog .ui-dialog-titlebar button.ui-dialog-titlebar-collapse {
margin: -1em 2.2em 0 0;
}
.ui-dialog .ui-dialog-titlebar button.ui-dialog-titlebar-close {
margin: -1em 0 0;
}
.ui-dialog .ui-dialog-titlebar button:active { .ui-dialog .ui-dialog-titlebar button:active {
border: 1px solid #5d4651; border: 1px solid #5d4651;
color: #5d4651; color: #5d4651;

View file

@ -185,12 +185,13 @@ const zoom = d3.zoom().scaleExtent([1, 20]).on("zoom", onZoomDebouced);
// default options, based on Earth data // default options, based on Earth data
let options = { let options = {
pinNotes: false, pinNotes: false,
showMFCGMap: true,
winds: [225, 45, 225, 315, 135, 315], winds: [225, 45, 225, 315, 135, 315],
temperatureEquator: 27, temperatureEquator: 27,
temperatureNorthPole: -30, temperatureNorthPole: -30,
temperatureSouthPole: -15, temperatureSouthPole: -15,
stateLabelsMode: "auto" stateLabelsMode: "auto",
showBurgPreview: true,
villageMaxPopulation: 2000
}; };
let mapCoordinates = {}; // map coordinates on globe let mapCoordinates = {}; // map coordinates on globe

View file

@ -252,13 +252,15 @@ window.BurgsAndStates = (function () {
.filter(b => (newburg ? b.i == newburg.i : b.i && !b.removed)) .filter(b => (newburg ? b.i == newburg.i : b.i && !b.removed))
.forEach(b => { .forEach(b => {
const pop = b.population; const pop = b.population;
b.citadel = b.capital || (pop > 50 && P(0.75)) || P(0.5) ? 1 : 0; b.citadel = Number(b.capital || (pop > 50 && P(0.75)) || (pop > 15 && P(0.5)) || P(0.1));
b.plaza = pop > 50 || (pop > 30 && P(0.75)) || (pop > 10 && P(0.5)) || P(0.25) ? 1 : 0; b.plaza = Number(pop > 20 || (pop > 10 && P(0.8)) || (pop > 4 && P(0.7)) || P(0.6));
b.walls = b.capital || pop > 30 || (pop > 20 && P(0.75)) || (pop > 10 && P(0.5)) || P(0.2) ? 1 : 0; b.walls = Number(b.capital || pop > 30 || (pop > 20 && P(0.75)) || (pop > 10 && P(0.5)) || P(0.1));
b.shanty = pop > 60 || (pop > 40 && P(0.75)) || (pop > 20 && b.walls && P(0.4)) ? 1 : 0; b.shanty = Number(pop > 60 || (pop > 40 && P(0.75)) || (pop > 20 && b.walls && P(0.4)));
const religion = cells.religion[b.cell]; const religion = cells.religion[b.cell];
const theocracy = pack.states[b.state].form === "Theocracy"; const theocracy = pack.states[b.state].form === "Theocracy";
b.temple = (religion && theocracy) || pop > 50 || (pop > 35 && P(0.75)) || (pop > 20 && P(0.5)) ? 1 : 0; b.temple = Number(
(religion && theocracy && P(0.5)) || pop > 50 || (pop > 35 && P(0.75)) || (pop > 20 && P(0.5))
);
}); });
}; };
@ -859,9 +861,10 @@ window.BurgsAndStates = (function () {
if (P(0.3) && s.diplomacy.includes("Vassal")) return "Protectorate"; // some vassals if (P(0.3) && s.diplomacy.includes("Vassal")) return "Protectorate"; // some vassals
} }
if (base === 16 && (form === "Empire" || form === "Kingdom")) return "Khaganate"; // Turkic if (base === 31 && (form === "Empire" || form === "Kingdom")) return "Khanate"; // Mongolian
if (base === 16 && form === "Principality") return "Beylik"; // Turkic
if (base === 5 && (form === "Empire" || form === "Kingdom")) return "Tsardom"; // Ruthenian if (base === 5 && (form === "Empire" || form === "Kingdom")) return "Tsardom"; // Ruthenian
if ([16, 31].includes(base) && (form === "Empire" || form === "Kingdom")) return "Khaganate"; // Turkic, Mongolian if (base === 16 && (form === "Empire" || form === "Kingdom")) return "Khaganate"; // Turkic
if (base === 12 && (form === "Kingdom" || form === "Grand Duchy")) return "Shogunate"; // Japanese if (base === 12 && (form === "Kingdom" || form === "Grand Duchy")) return "Shogunate"; // Japanese
if ([18, 17].includes(base) && form === "Empire") return "Caliphate"; // Arabic, Berber if ([18, 17].includes(base) && form === "Empire") return "Caliphate"; // Arabic, Berber
if (base === 18 && (form === "Grand Duchy" || form === "Duchy")) return "Emirate"; // Arabic if (base === 18 && (form === "Grand Duchy" || form === "Duchy")) return "Emirate"; // Arabic

View file

@ -190,12 +190,13 @@ window.Cultures = (function () {
name = Names.getCulture(culture, 5, 8, ""); name = Names.getCulture(culture, 5, 8, "");
base = pack.cultures[culture].base; base = pack.cultures[culture].base;
} }
const code = abbreviate( const code = abbreviate(
name, name,
pack.cultures.map(c => c.code) pack.cultures.map(c => c.code)
); );
const i = pack.cultures.length; const i = pack.cultures.length;
const color = d3.color(d3.scaleSequential(d3.interpolateRainbow)(Math.random())).hex(); const color = getRandomColor();
// define emblem shape // define emblem shape
let shield = culture.shield; let shield = culture.shield;
@ -214,7 +215,7 @@ window.Cultures = (function () {
area: 0, area: 0,
rural: 0, rural: 0,
urban: 0, urban: 0,
origins: [0], origins: [pack.cells.culture[center]],
code, code,
shield shield
}); });

View file

@ -743,7 +743,7 @@ export function resolveVersionConflicts(version) {
const opacity = terrs.attr("opacity"); const opacity = terrs.attr("opacity");
const filter = terrs.attr("filter"); const filter = terrs.attr("filter");
const scheme = terrs.attr("scheme"); const scheme = terrs.attr("scheme") || "bright";
const terracing = terrs.attr("terracing"); const terracing = terrs.attr("terracing");
const skip = terrs.attr("skip"); const skip = terrs.attr("skip");
const relax = terrs.attr("relax"); const relax = terrs.attr("relax");
@ -827,4 +827,20 @@ export function resolveVersionConflicts(version) {
}); });
}); });
} }
if (version < 1.97) {
// v1.97.00 changed MFCG link to an arbitrary preview URL
options.villageMaxPopulation = 2000;
options.showBurgPreview = options.showMFCGMap;
delete options.showMFCGMap;
pack.burgs.forEach(burg => {
if (!burg.i || burg.removed) return;
if (burg.MFCG) {
burg.link = getBurgLink(burg);
delete burg.MFCG;
}
});
}
} }

View file

@ -822,6 +822,7 @@ function addCulture() {
if (pack.cells.h[center] < 20) if (pack.cells.h[center] < 20)
return tip("You cannot place culture center into the water. Please click on a land cell", false, "error"); return tip("You cannot place culture center into the water. Please click on a land cell", false, "error");
const occupied = pack.cultures.some(c => !c.removed && c.center === center); const occupied = pack.cultures.some(c => !c.removed && c.center === center);
if (occupied) return tip("This cell is already a culture center. Please select a different cell", false, "error"); if (occupied) return tip("This cell is already a culture center. Please select a different cell", false, "error");
@ -857,8 +858,19 @@ function closeCulturesEditor() {
} }
async function uploadCulturesData() { async function uploadCulturesData() {
const csv = await Formats.csvParser(this.files[0]); const file = this.files[0];
this.value = ""; this.value = "";
const csv = await file.text();
const data = d3.csvParse(csv, d => ({
i: +d.Id,
name: d.Name,
color: d.Color,
expansionism: +d.Expansionism,
type: d.Type,
population: +d.Population,
emblemsShape: d["Emblems Shape"],
origins: d.Origins
}));
const {cultures, cells} = pack; const {cultures, cells} = pack;
const shapes = Object.keys(COA.shields.types) const shapes = Object.keys(COA.shields.types)
@ -870,20 +882,26 @@ async function uploadCulturesData() {
if (item.i) item.removed = true; if (item.i) item.removed = true;
}); });
for (const c of csv.iterator((a, b) => +a[0] > +b[0])) { for (const culture of data) {
let current; let current;
if (+c.id < cultures.length) { if (culture.i < cultures.length) {
current = cultures[c.id]; current = cultures[culture.i];
const ratio = current.urban / (current.rural + current.urban); const ratio = current.urban / (current.rural + current.urban);
applyPopulationChange(current.rural, current.urban, c.population * (1 - ratio), c.population * ratio, +c.id); applyPopulationChange(
current.rural,
current.urban,
culture.population * (1 - ratio),
culture.population * ratio,
culture.i
);
} else { } else {
current = {i: cultures.length, center: ra(populated), area: 0, cells: 0, origin: 0, rural: 0, urban: 0}; current = {i: cultures.length, center: ra(populated), area: 0, cells: 0, origin: 0, rural: 0, urban: 0};
cultures.push(current); cultures.push(current);
} }
current.removed = false; current.removed = false;
current.name = c.name; current.name = culture.name;
if (current.i) { if (current.i) {
current.code = abbreviate( current.code = abbreviate(
@ -891,10 +909,10 @@ async function uploadCulturesData() {
cultures.map(c => c.code) cultures.map(c => c.code)
); );
current.color = c.color; current.color = culture.color;
current.expansionism = +c.expansionism; current.expansionism = +culture.expansionism;
if (cultureTypes.includes(c.type)) current.type = c.type; if (cultureTypes.includes(culture.type)) current.type = culture.type;
else current.type = "Generic"; else current.type = "Generic";
} }
@ -913,13 +931,11 @@ async function uploadCulturesData() {
current.origins = originIds.filter(id => id !== null); current.origins = originIds.filter(id => id !== null);
if (!current.origins.length) current.origins = [0]; if (!current.origins.length) current.origins = [0];
} }
c.origins = current.i ? restoreOrigins(c.origins) : [null];
const shieldShape = c["emblems shape"].toLowerCase(); culture.origins = current.i ? restoreOrigins(culture.origins || "") : [null];
if (shapes.includes(shieldShape)) current.shield = shieldShape; current.shield = shapes.includes(culture.emblemsShape) ? culture.emblemsShape : "heater";
else current.shield = "heater";
const nameBaseIndex = nameBases.findIndex(n => n.name == c.namesbase); const nameBaseIndex = nameBases.findIndex(n => n.name == culture.namesbase);
current.base = nameBaseIndex === -1 ? 0 : nameBaseIndex; current.base = nameBaseIndex === -1 ? 0 : nameBaseIndex;
} }

View file

@ -624,28 +624,36 @@ function stateRemovePrompt(state) {
}); });
} }
function stateRemove(state) { function stateRemove(stateId) {
statesBody.select("#state" + state).remove(); statesBody.select("#state" + stateId).remove();
statesBody.select("#state-gap" + state).remove(); statesBody.select("#state-gap" + stateId).remove();
statesHalo.select("#state-border" + state).remove(); statesHalo.select("#state-border" + stateId).remove();
labels.select("#stateLabel" + state).remove(); labels.select("#stateLabel" + stateId).remove();
defs.select("#textPath_stateLabel" + state).remove(); defs.select("#textPath_stateLabel" + stateId).remove();
unfog("focusState" + state); unfog("focusState" + stateId);
pack.burgs.forEach(b => {
if (b.state === state) b.state = 0; pack.burgs.forEach(burg => {
if (burg.state === stateId) {
burg.state = 0;
if (burg.capital) {
burg.capital = 0;
moveBurgToGroup(burg.i, "towns");
}
}
}); });
pack.cells.state.forEach((s, i) => { pack.cells.state.forEach((s, i) => {
if (s === state) pack.cells.state[i] = 0; if (s === stateId) pack.cells.state[i] = 0;
}); });
// remove emblem // remove emblem
const coaId = "stateCOA" + state; const coaId = "stateCOA" + stateId;
byId(coaId).remove(); byId(coaId).remove();
emblems.select(`#stateEmblems > use[data-i='${state}']`).remove(); emblems.select(`#stateEmblems > use[data-i='${stateId}']`).remove();
// remove provinces // remove provinces
pack.states[state].provinces.forEach(p => { pack.states[stateId].provinces.forEach(p => {
pack.provinces[p] = {i: p, removed: true}; pack.provinces[p] = {i: p, removed: true};
pack.cells.province.forEach((pr, i) => { pack.cells.province.forEach((pr, i) => {
if (pr === p) pack.cells.province[i] = 0; if (pr === p) pack.cells.province[i] = 0;
@ -660,19 +668,14 @@ function stateRemove(state) {
}); });
// remove military // remove military
pack.states[state].military.forEach(m => { pack.states[stateId].military.forEach(m => {
const id = `regiment${state}-${m.i}`; const id = `regiment${stateId}-${m.i}`;
const index = notes.findIndex(n => n.id === id); const index = notes.findIndex(n => n.id === id);
if (index != -1) notes.splice(index, 1); if (index != -1) notes.splice(index, 1);
}); });
armies.select("g#army" + state).remove(); armies.select("g#army" + stateId).remove();
const capital = pack.states[state].capital; pack.states[stateId] = {i: stateId, removed: true};
pack.burgs[capital].capital = 0;
pack.burgs[capital].state = 0;
moveBurgToGroup(capital, "towns");
pack.states[state] = {i: state, removed: true};
debug.selectAll(".highlight").remove(); debug.selectAll(".highlight").remove();
if (!layerIsOn("toggleStates")) toggleStates(); if (!layerIsOn("toggleStates")) toggleStates();

View file

@ -71,8 +71,9 @@ async function exportToJpeg() {
} }
async function exportToPngTiles() { async function exportToPngTiles() {
return new Promise(async (resolve, reject) => { const status = byId("tileStatus");
// download schema status.innerHTML = "Preparing files...";
const urlSchema = await getMapURL("tiles", {debug: true, fullMap: true}); const urlSchema = await getMapURL("tiles", {debug: true, fullMap: true});
await import("../../libs/jszip.min.js"); await import("../../libs/jszip.min.js");
const zip = new window.JSZip(); const zip = new window.JSZip();
@ -84,58 +85,81 @@ async function exportToPngTiles() {
const imgSchema = new Image(); const imgSchema = new Image();
imgSchema.src = urlSchema; imgSchema.src = urlSchema;
imgSchema.onload = function () { await loadImage(imgSchema);
status.innerHTML = "Drawing schema...";
ctx.drawImage(imgSchema, 0, 0, canvas.width, canvas.height); ctx.drawImage(imgSchema, 0, 0, canvas.width, canvas.height);
canvas.toBlob(blob => zip.file(`fmg_tile_schema.png`, blob)); const blob = await canvasToBlob(canvas, "image/png");
}; ctx.clearRect(0, 0, canvas.width, canvas.height);
zip.file("schema.png", blob);
// download tiles // download tiles
const url = await getMapURL("tiles", {fullMap: true}); const url = await getMapURL("tiles", {fullMap: true});
const tilesX = +document.getElementById("tileColsInput").value; const tilesX = +byId("tileColsInput").value;
const tilesY = +document.getElementById("tileRowsInput").value; const tilesY = +byId("tileRowsInput").value;
const scale = +document.getElementById("tileScaleInput").value; const scale = +byId("tileScaleInput").value;
const tolesTotal = tilesX * tilesY;
const tileW = (graphWidth / tilesX) | 0; const tileW = (graphWidth / tilesX) | 0;
const tileH = (graphHeight / tilesY) | 0; const tileH = (graphHeight / tilesY) | 0;
const tolesTotal = tilesX * tilesY;
const width = graphWidth * scale; const width = graphWidth * scale;
const height = width * (tileH / tileW); const height = width * (tileH / tileW);
canvas.width = width; canvas.width = width;
canvas.height = height; canvas.height = height;
let loaded = 0;
const img = new Image(); const img = new Image();
img.src = url; img.src = url;
img.onload = function () { await loadImage(img);
for (let y = 0, i = 0; y + tileH <= graphHeight; y += tileH) {
for (let x = 0; x + tileW <= graphWidth; x += tileW, i++) {
ctx.drawImage(img, x, y, tileW, tileH, 0, 0, width, height);
const name = `fmg_tile_${i}.png`;
canvas.toBlob(blob => {
zip.file(name, blob);
loaded += 1;
if (loaded === tolesTotal) return downloadZip();
});
}
}
};
function downloadZip() { const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const name = `${getFileName()}.zip`; for (let y = 0, row = 0, id = 1; y + tileH <= graphHeight; y += tileH, row++) {
const rowName = alphabet[row % alphabet.length];
for (let x = 0, cell = 1; x + tileW <= graphWidth; x += tileW, cell++, id++) {
status.innerHTML = `Drawing tile ${rowName}${cell} (${id} of ${tolesTotal})...`;
ctx.drawImage(img, x, y, tileW, tileH, 0, 0, width, height);
const blob = await canvasToBlob(canvas, "image/png");
ctx.clearRect(0, 0, canvas.width, canvas.height);
zip.file(`${rowName}${cell}.png`, blob);
}
}
status.innerHTML = "Zipping files...";
zip.generateAsync({type: "blob"}).then(blob => { zip.generateAsync({type: "blob"}).then(blob => {
status.innerHTML = "Downloading the archive...";
const link = document.createElement("a"); const link = document.createElement("a");
link.href = URL.createObjectURL(blob); link.href = URL.createObjectURL(blob);
link.download = name; link.download = getFileName() + ".zip";
link.click(); link.click();
link.remove(); link.remove();
status.innerHTML = 'Done. Check .zip file in "Downloads" (crtl + J)';
setTimeout(() => URL.revokeObjectURL(link.href), 5000); setTimeout(() => URL.revokeObjectURL(link.href), 5000);
resolve(true); });
// promisified img.onload
function loadImage(img) {
return new Promise((resolve, reject) => {
img.onload = () => resolve();
img.onerror = err => reject(err);
}); });
} }
// promisified canvas.toBlob
function canvasToBlob(canvas, mimeType, qualityArgument = 1) {
return new Promise((resolve, reject) => {
canvas.toBlob(
blob => {
if (blob) resolve(blob);
else reject(new Error("Canvas toBlob() error"));
},
mimeType,
qualityArgument
);
}); });
} }
}
// parse map svg to object url // parse map svg to object url
async function getMapURL(type, options) { async function getMapURL(type, options) {
@ -148,14 +172,14 @@ async function getMapURL(type, options) {
fullMap = false fullMap = false
} = options || {}; } = options || {};
const cloneEl = document.getElementById("map").cloneNode(true); // clone svg const cloneEl = byId("map").cloneNode(true); // clone svg
cloneEl.id = "fantasyMap"; cloneEl.id = "fantasyMap";
document.body.appendChild(cloneEl); document.body.appendChild(cloneEl);
const clone = d3.select(cloneEl); const clone = d3.select(cloneEl);
if (!debug) clone.select("#debug")?.remove(); if (!debug) clone.select("#debug")?.remove();
const cloneDefs = cloneEl.getElementsByTagName("defs")[0]; const cloneDefs = cloneEl.getElementsByTagName("defs")[0];
const svgDefs = document.getElementById("defElements"); const svgDefs = byId("defElements");
const isFirefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1; const isFirefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
if (isFirefox && type === "mesh") clone.select("#oceanPattern")?.remove(); if (isFirefox && type === "mesh") clone.select("#oceanPattern")?.remove();
@ -218,7 +242,7 @@ async function getMapURL(type, options) {
.forEach(el => { .forEach(el => {
const href = el.getAttribute("href") || el.getAttribute("xlink:href"); const href = el.getAttribute("href") || el.getAttribute("xlink:href");
if (!href) return; if (!href) return;
const emblem = document.getElementById(href.slice(1)); const emblem = byId(href.slice(1));
if (emblem) cloneDefs.append(emblem.cloneNode(true)); if (emblem) cloneDefs.append(emblem.cloneNode(true));
}); });
} else { } else {
@ -364,7 +388,7 @@ function removeUnusedElements(clone) {
function updateMeshCells(clone) { function updateMeshCells(clone) {
const data = renderOcean.checked ? grid.cells.i : grid.cells.i.filter(i => grid.cells.h[i] >= 20); const data = renderOcean.checked ? grid.cells.i : grid.cells.i.filter(i => grid.cells.h[i] >= 20);
const scheme = getColorScheme(terrs.attr("scheme")); const scheme = getColorScheme(terrs.select("#landHeights").attr("scheme"));
clone.select("#heights").attr("filter", "url(#blur1)"); clone.select("#heights").attr("filter", "url(#blur1)");
clone clone
.select("#heights") .select("#heights")

View file

@ -1,24 +0,0 @@
"use strict";
window.Formats = (function () {
async function csvParser(file, separator = ",") {
const txt = await file.text();
const rows = txt.split("\n");
const headers = rows
.shift()
.split(separator)
.map(x => x.toLowerCase());
const data = rows.filter(a => a.trim() !== "").map(r => r.split(separator));
return {
headers,
data,
iterator: function* (sortf) {
const dataset = sortf ? this.data.sort(sortf) : this.data;
for (const d of dataset) yield Object.fromEntries(d.map((a, i) => [this.headers[i], a]));
}
};
}
return {csvParser};
})();

View file

@ -456,16 +456,16 @@ async function parseLoadedData(data, mapVersion) {
{ {
// dynamically import and run auto-update script // dynamically import and run auto-update script
const versionNumber = parseFloat(params[0]); const versionNumber = parseFloat(params[0]);
const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.96.00"); const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.97.04");
resolveVersionConflicts(versionNumber); resolveVersionConflicts(versionNumber);
} }
{
// add custom heightmap color scheme if any // add custom heightmap color scheme if any
const oceanScheme = terrs.select("#oceanHeights").attr("scheme"); if (heightmapColorSchemes) {
const landScheme = terrs.select("#landHeights").attr("scheme"); const oceanScheme = byId("oceanHeights")?.getAttribute("scheme");
if (!(oceanScheme in heightmapColorSchemes)) addCustomColorScheme(oceanScheme); if (oceanScheme && !(oceanScheme in heightmapColorSchemes)) addCustomColorScheme(oceanScheme);
if (!(landScheme in heightmapColorSchemes)) addCustomColorScheme(landScheme); const landScheme = byId("#landHeights")?.getAttribute("scheme");
if (landScheme && !(landScheme in heightmapColorSchemes)) addCustomColorScheme(landScheme);
} }
{ {
@ -478,7 +478,7 @@ async function parseLoadedData(data, mapVersion) {
const cells = pack.cells; const cells = pack.cells;
if (pack.cells.i.length !== pack.cells.state.length) { if (pack.cells.i.length !== pack.cells.state.length) {
const message = "Data Integrity Check. Striping issue detected. To fix edit the heightmap in erase mode"; const message = "Data integrity check. Striping issue detected. To fix edit the heightmap in ERASE mode";
ERROR && console.error(message); ERROR && console.error(message);
} }
@ -486,7 +486,7 @@ async function parseLoadedData(data, mapVersion) {
invalidStates.forEach(s => { invalidStates.forEach(s => {
const invalidCells = cells.i.filter(i => cells.state[i] === s); const invalidCells = cells.i.filter(i => cells.state[i] === s);
invalidCells.forEach(i => (cells.state[i] = 0)); invalidCells.forEach(i => (cells.state[i] = 0));
ERROR && console.error("Data Integrity Check. Invalid state", s, "is assigned to cells", invalidCells); ERROR && console.error("Data integrity check. Invalid state", s, "is assigned to cells", invalidCells);
}); });
const invalidProvinces = [...new Set(cells.province)].filter( const invalidProvinces = [...new Set(cells.province)].filter(
@ -495,14 +495,14 @@ async function parseLoadedData(data, mapVersion) {
invalidProvinces.forEach(p => { invalidProvinces.forEach(p => {
const invalidCells = cells.i.filter(i => cells.province[i] === p); const invalidCells = cells.i.filter(i => cells.province[i] === p);
invalidCells.forEach(i => (cells.province[i] = 0)); invalidCells.forEach(i => (cells.province[i] = 0));
ERROR && console.error("Data Integrity Check. Invalid province", p, "is assigned to cells", invalidCells); ERROR && console.error("Data integrity check. Invalid province", p, "is assigned to cells", invalidCells);
}); });
const invalidCultures = [...new Set(cells.culture)].filter(c => !pack.cultures[c] || pack.cultures[c].removed); const invalidCultures = [...new Set(cells.culture)].filter(c => !pack.cultures[c] || pack.cultures[c].removed);
invalidCultures.forEach(c => { invalidCultures.forEach(c => {
const invalidCells = cells.i.filter(i => cells.culture[i] === c); const invalidCells = cells.i.filter(i => cells.culture[i] === c);
invalidCells.forEach(i => (cells.province[i] = 0)); invalidCells.forEach(i => (cells.province[i] = 0));
ERROR && console.error("Data Integrity Check. Invalid culture", c, "is assigned to cells", invalidCells); ERROR && console.error("Data integrity check. Invalid culture", c, "is assigned to cells", invalidCells);
}); });
const invalidReligions = [...new Set(cells.religion)].filter( const invalidReligions = [...new Set(cells.religion)].filter(
@ -511,14 +511,14 @@ async function parseLoadedData(data, mapVersion) {
invalidReligions.forEach(r => { invalidReligions.forEach(r => {
const invalidCells = cells.i.filter(i => cells.religion[i] === r); const invalidCells = cells.i.filter(i => cells.religion[i] === r);
invalidCells.forEach(i => (cells.religion[i] = 0)); invalidCells.forEach(i => (cells.religion[i] = 0));
ERROR && console.error("Data Integrity Check. Invalid religion", r, "is assigned to cells", invalidCells); ERROR && console.error("Data integrity check. Invalid religion", r, "is assigned to cells", invalidCells);
}); });
const invalidFeatures = [...new Set(cells.f)].filter(f => f && !pack.features[f]); const invalidFeatures = [...new Set(cells.f)].filter(f => f && !pack.features[f]);
invalidFeatures.forEach(f => { invalidFeatures.forEach(f => {
const invalidCells = cells.i.filter(i => cells.f[i] === f); const invalidCells = cells.i.filter(i => cells.f[i] === f);
// No fix as for now // No fix as for now
ERROR && console.error("Data Integrity Check. Invalid feature", f, "is assigned to cells", invalidCells); ERROR && console.error("Data integrity check. Invalid feature", f, "is assigned to cells", invalidCells);
}); });
const invalidBurgs = [...new Set(cells.burg)].filter( const invalidBurgs = [...new Set(cells.burg)].filter(
@ -527,7 +527,7 @@ async function parseLoadedData(data, mapVersion) {
invalidBurgs.forEach(burgId => { invalidBurgs.forEach(burgId => {
const invalidCells = cells.i.filter(i => cells.burg[i] === burgId); const invalidCells = cells.i.filter(i => cells.burg[i] === burgId);
invalidCells.forEach(i => (cells.burg[i] = 0)); invalidCells.forEach(i => (cells.burg[i] = 0));
ERROR && console.error("Data Integrity Check. Invalid burg", burgId, "is assigned to cells", invalidCells); ERROR && console.error("Data integrity check. Invalid burg", burgId, "is assigned to cells", invalidCells);
}); });
const invalidRivers = [...new Set(cells.r)].filter(r => r && !pack.rivers.find(river => river.i === r)); const invalidRivers = [...new Set(cells.r)].filter(r => r && !pack.rivers.find(river => river.i === r));
@ -535,60 +535,112 @@ async function parseLoadedData(data, mapVersion) {
const invalidCells = cells.i.filter(i => cells.r[i] === r); const invalidCells = cells.i.filter(i => cells.r[i] === r);
invalidCells.forEach(i => (cells.r[i] = 0)); invalidCells.forEach(i => (cells.r[i] = 0));
rivers.select("river" + r).remove(); rivers.select("river" + r).remove();
ERROR && console.error("Data Integrity Check. Invalid river", r, "is assigned to cells", invalidCells); ERROR && console.error("Data integrity check. Invalid river", r, "is assigned to cells", invalidCells);
}); });
pack.burgs.forEach(burg => { pack.burgs.forEach(burg => {
if ((!burg.i || burg.removed) && burg.lock) { if (typeof burg.capital === "boolean") burg.capital = Number(burg.capital);
if (!burg.i && burg.lock) {
ERROR && console.error(`Data integrity check. Burg 0 is marked as locked, removing the status`);
delete burg.lock;
return;
}
if (burg.removed && burg.lock) {
ERROR && ERROR &&
console.error( console.error(`Data integrity check. Removed burg ${burg.i} is marked as locked. Unlocking the burg`);
`Data Integrity Check. Burg ${burg.i || "0"} is removed or invalid but still locked. Unlocking the burg`
);
delete burg.lock; delete burg.lock;
return; return;
} }
if (!burg.i || burg.removed) return; if (!burg.i || burg.removed) return;
if (burg.cell === undefined || burg.x === undefined || burg.y === undefined) { if (burg.cell === undefined || burg.x === undefined || burg.y === undefined) {
ERROR && ERROR &&
console.error( console.error(
`Data Integrity Check. Burg ${burg.i} is missing cell info or coordinates. Removing the burg` `Data integrity check. Burg ${burg.i} is missing cell info or coordinates. Removing the burg`
); );
burg.removed = true; burg.removed = true;
} }
if (burg.port < 0) { if (burg.port < 0) {
ERROR && console.error("Data Integrity Check. Burg", burg.i, "has invalid port value", burg.port); ERROR && console.error("Data integrity check. Burg", burg.i, "has invalid port value", burg.port);
burg.port = 0; burg.port = 0;
} }
if (burg.cell >= cells.i.length) { if (burg.cell >= cells.i.length) {
ERROR && console.error("Data Integrity Check. Burg", burg.i, "is linked to invalid cell", burg.cell); ERROR && console.error("Data integrity check. Burg", burg.i, "is linked to invalid cell", burg.cell);
burg.cell = findCell(burg.x, burg.y); burg.cell = findCell(burg.x, burg.y);
cells.i.filter(i => cells.burg[i] === burg.i).forEach(i => (cells.burg[i] = 0)); cells.i.filter(i => cells.burg[i] === burg.i).forEach(i => (cells.burg[i] = 0));
cells.burg[burg.cell] = burg.i; cells.burg[burg.cell] = burg.i;
} }
if (burg.state && !pack.states[burg.state]) { if (burg.state && !pack.states[burg.state]) {
ERROR && console.error("Data Integrity Check. Burg", burg.i, "is linked to invalid state", burg.state); ERROR && console.error("Data integrity check. Burg", burg.i, "is linked to invalid state", burg.state);
burg.state = 0; burg.state = 0;
} }
if (burg.state && pack.states[burg.state].removed) { if (burg.state && pack.states[burg.state].removed) {
ERROR && console.error("Data Integrity Check. Burg", burg.i, "is linked to removed state", burg.state); ERROR && console.error("Data integrity check. Burg", burg.i, "is linked to removed state", burg.state);
burg.state = 0; burg.state = 0;
} }
if (burg.state === undefined) { if (burg.state === undefined) {
ERROR && console.error("Data Integrity Check. Burg", burg.i, "has no state data"); ERROR && console.error("Data integrity check. Burg", burg.i, "has no state data");
burg.state = 0; burg.state = 0;
} }
}); });
pack.states.forEach(state => {
if (state.removed) return;
const stateBurgs = pack.burgs.filter(b => b.state === state.i && !b.removed);
const capitalBurgs = stateBurgs.filter(b => b.capital);
if (!state.i && capitalBurgs.length) {
ERROR &&
console.error(
`Data integrity check. Neutral burgs (${capitalBurgs
.map(b => b.i)
.join(", ")}) marked as capitals. Moving them to towns`
);
capitalBurgs.forEach(burg => {
burg.capital = 0;
moveBurgToGroup(burg.i, "towns");
});
return;
}
if (capitalBurgs.length > 1) {
const message = `Data integrity check. State ${state.i} has multiple capitals (${capitalBurgs
.map(b => b.i)
.join(", ")}) assigned. Keeping the first as capital and moving others to towns`;
ERROR && console.error(message);
capitalBurgs.forEach((burg, i) => {
if (!i) return;
burg.capital = 0;
moveBurgToGroup(burg.i, "towns");
});
return;
}
if (state.i && stateBurgs.length && !capitalBurgs.length) {
ERROR &&
console.error(`Data integrity check. State ${state.i} has no capital. Assigning the first burg as capital`);
stateBurgs[0].capital = 1;
moveBurgToGroup(stateBurgs[0].i, "cities");
}
});
pack.provinces.forEach(p => { pack.provinces.forEach(p => {
if (!p.i || p.removed) return; if (!p.i || p.removed) return;
if (pack.states[p.state] && !pack.states[p.state].removed) return; if (pack.states[p.state] && !pack.states[p.state].removed) return;
ERROR && console.error("Data Integrity Check. Province", p.i, "is linked to removed state", p.state); ERROR && console.error("Data integrity check. Province", p.i, "is linked to removed state", p.state);
p.removed = true; // remove incorrect province p.removed = true; // remove incorrect province
}); });
@ -598,7 +650,7 @@ async function parseLoadedData(data, mapVersion) {
pack.markers.forEach(marker => { pack.markers.forEach(marker => {
if (markerIds[marker.i]) { if (markerIds[marker.i]) {
ERROR && console.error("Data Integrity Check. Marker", marker.i, "has non-unique id. Changing to", nextId); ERROR && console.error("Data integrity check. Marker", marker.i, "has non-unique id. Changing to", nextId);
const domElements = document.querySelectorAll("#marker" + marker.i); const domElements = document.querySelectorAll("#marker" + marker.i);
if (domElements[1]) domElements[1].id = "marker" + nextId; // rename 2nd dom element if (domElements[1]) domElements[1].id = "marker" + nextId; // rename 2nd dom element

View file

@ -381,29 +381,29 @@ window.Military = (function () {
.text(d => d.icon); .text(d => d.icon);
}; };
const drawRegiment = function (reg, s) { const drawRegiment = function (reg, stateId) {
const size = +armies.attr("box-size"); const size = +armies.attr("box-size");
const w = reg.n ? size * 4 : size * 6; const w = reg.n ? size * 4 : size * 6;
const h = size * 2; const h = size * 2;
const x1 = rn(reg.x - w / 2, 2); const x1 = rn(reg.x - w / 2, 2);
const y1 = rn(reg.y - size, 2); const y1 = rn(reg.y - size, 2);
let army = armies.select("g#army" + s); let army = armies.select("g#army" + stateId);
if (!army.size()) { if (!army.size()) {
const baseColor = pack.states[s].color[0] === "#" ? pack.states[s].color : "#999"; const baseColor = pack.states[stateId].color[0] === "#" ? pack.states[stateId].color : "#999";
const darkerColor = d3.color(army.attr("fill")).darker().hex(); const darkerColor = d3.color(baseColor).darker().hex();
army = armies army = armies
.append("g") .append("g")
.attr("id", "army" + s) .attr("id", "army" + stateId)
.attr("fill", baseColor) .attr("fill", baseColor)
.attr("color", darkerColor); .attr("color", darkerColor);
} }
const g = army const g = army
.append("g") .append("g")
.attr("id", "regiment" + s + "-" + reg.i) .attr("id", "regiment" + stateId + "-" + reg.i)
.attr("data-name", reg.name) .attr("data-name", reg.name)
.attr("data-state", s) .attr("data-state", stateId)
.attr("data-id", reg.i); .attr("data-id", reg.i);
g.append("rect").attr("x", x1).attr("y", y1).attr("width", w).attr("height", h); g.append("rect").attr("x", x1).attr("y", y1).attr("width", w).attr("height", h);
g.append("text").attr("x", reg.x).attr("y", reg.y).text(getTotal(reg)); g.append("text").attr("x", reg.x).attr("y", reg.y).text(getTotal(reg));

View file

@ -4,6 +4,10 @@
function drawStateLabels(list) { function drawStateLabels(list) {
console.time("drawStateLabels"); console.time("drawStateLabels");
// temporary make the labels visible
const layerDisplay = labels.style("display");
labels.style("display", null);
const {cells, states, features} = pack; const {cells, states, features} = pack;
const stateIds = cells.state; const stateIds = cells.state;
@ -17,7 +21,11 @@ function drawStateLabels(list) {
const MAX_ITERATIONS = 100; const MAX_ITERATIONS = 100;
const labelPaths = getLabelPaths(); const labelPaths = getLabelPaths();
drawLabelPath(); const letterLength = checkExampleLetterLength();
drawLabelPath(letterLength);
// restore labels visibility
labels.style("display", layerDisplay);
function getLabelPaths() { function getLabelPaths() {
const labelPaths = []; const labelPaths = [];
@ -110,17 +118,22 @@ function drawStateLabels(list) {
} }
} }
function drawLabelPath() { function checkExampleLetterLength() {
const textGroup = d3.select("g#labels > g#states");
const testLabel = textGroup.append("text").attr("x", 0).attr("y", 0).text("Example");
const letterLength = testLabel.node().getComputedTextLength() / 7; // approximate length of 1 letter
testLabel.remove();
return letterLength;
}
function drawLabelPath(letterLength) {
const mode = options.stateLabelsMode || "auto"; const mode = options.stateLabelsMode || "auto";
const lineGen = d3.line().curve(d3.curveBundle.beta(1)); const lineGen = d3.line().curve(d3.curveBundle.beta(1));
const textGroup = d3.select("g#labels > g#states"); const textGroup = d3.select("g#labels > g#states");
const pathGroup = d3.select("defs > g#deftemp > g#textPaths"); const pathGroup = d3.select("defs > g#deftemp > g#textPaths");
const testLabel = textGroup.append("text").attr("x", 0).attr("y", 0).text("Example");
const letterLength = testLabel.node().getComputedTextLength() / 7; // approximate length of 1 letter
testLabel.remove();
for (const [stateId, pathPoints] of labelPaths) { for (const [stateId, pathPoints] of labelPaths) {
const state = states[stateId]; const state = states[stateId];
if (!state.i || state.removed) throw new Error("State must not be neutral or removed"); if (!state.i || state.removed) throw new Error("State must not be neutral or removed");

View file

@ -21,38 +21,37 @@ function editBurg(id) {
modules.editBurg = true; modules.editBurg = true;
// add listeners // add listeners
document.getElementById("burgGroupShow").addEventListener("click", showGroupSection); byId("burgGroupShow").addEventListener("click", showGroupSection);
document.getElementById("burgGroupHide").addEventListener("click", hideGroupSection); byId("burgGroupHide").addEventListener("click", hideGroupSection);
document.getElementById("burgSelectGroup").addEventListener("change", changeGroup); byId("burgSelectGroup").addEventListener("change", changeGroup);
document.getElementById("burgInputGroup").addEventListener("change", createNewGroup); byId("burgInputGroup").addEventListener("change", createNewGroup);
document.getElementById("burgAddGroup").addEventListener("click", toggleNewGroupInput); byId("burgAddGroup").addEventListener("click", toggleNewGroupInput);
document.getElementById("burgRemoveGroup").addEventListener("click", removeBurgsGroup); byId("burgRemoveGroup").addEventListener("click", removeBurgsGroup);
document.getElementById("burgName").addEventListener("input", changeName); byId("burgName").addEventListener("input", changeName);
document.getElementById("burgNameReRandom").addEventListener("click", generateNameRandom); byId("burgNameReRandom").addEventListener("click", generateNameRandom);
document.getElementById("burgType").addEventListener("input", changeType); byId("burgType").addEventListener("input", changeType);
document.getElementById("burgCulture").addEventListener("input", changeCulture); byId("burgCulture").addEventListener("input", changeCulture);
document.getElementById("burgNameReCulture").addEventListener("click", generateNameCulture); byId("burgNameReCulture").addEventListener("click", generateNameCulture);
document.getElementById("burgPopulation").addEventListener("change", changePopulation); byId("burgPopulation").addEventListener("change", changePopulation);
burgBody.querySelectorAll(".burgFeature").forEach(el => el.addEventListener("click", toggleFeature)); burgBody.querySelectorAll(".burgFeature").forEach(el => el.addEventListener("click", toggleFeature));
document.getElementById("mfcgBurgSeed").addEventListener("change", changeSeed); byId("burgLinkOpen").addEventListener("click", openBurgLink);
document.getElementById("regenerateMFCGBurgSeed").addEventListener("click", randomizeSeed); byId("burgLinkEdit").addEventListener("click", changeBurgLink);
document.getElementById("addCustomMFCGBurgLink").addEventListener("click", addCustomMfcgLink);
document.getElementById("burgStyleShow").addEventListener("click", showStyleSection); byId("burgStyleShow").addEventListener("click", showStyleSection);
document.getElementById("burgStyleHide").addEventListener("click", hideStyleSection); byId("burgStyleHide").addEventListener("click", hideStyleSection);
document.getElementById("burgEditLabelStyle").addEventListener("click", editGroupLabelStyle); byId("burgEditLabelStyle").addEventListener("click", editGroupLabelStyle);
document.getElementById("burgEditIconStyle").addEventListener("click", editGroupIconStyle); byId("burgEditIconStyle").addEventListener("click", editGroupIconStyle);
document.getElementById("burgEditAnchorStyle").addEventListener("click", editGroupAnchorStyle); byId("burgEditAnchorStyle").addEventListener("click", editGroupAnchorStyle);
document.getElementById("burgEmblem").addEventListener("click", openEmblemEdit); byId("burgEmblem").addEventListener("click", openEmblemEdit);
document.getElementById("burgToggleMFCGMap").addEventListener("click", toggleMFCGMap); byId("burgTogglePreview").addEventListener("click", toggleBurgPreview);
document.getElementById("burgEditEmblem").addEventListener("click", openEmblemEdit); byId("burgEditEmblem").addEventListener("click", openEmblemEdit);
document.getElementById("burgRelocate").addEventListener("click", toggleRelocateBurg); byId("burgRelocate").addEventListener("click", toggleRelocateBurg);
document.getElementById("burglLegend").addEventListener("click", editBurgLegend); byId("burglLegend").addEventListener("click", editBurgLegend);
document.getElementById("burgLock").addEventListener("click", toggleBurgLockButton); byId("burgLock").addEventListener("click", toggleBurgLockButton);
document.getElementById("burgRemove").addEventListener("click", removeSelectedBurg); byId("burgRemove").addEventListener("click", removeSelectedBurg);
document.getElementById("burgTemperatureGraph").addEventListener("click", showTemperatureGraph); byId("burgTemperatureGraph").addEventListener("click", showTemperatureGraph);
function updateBurgValues() { function updateBurgValues() {
const id = +elSelected.attr("data-id"); const id = +elSelected.attr("data-id");
@ -60,46 +59,46 @@ function editBurg(id) {
const province = pack.cells.province[b.cell]; const province = pack.cells.province[b.cell];
const provinceName = province ? pack.provinces[province].fullName + ", " : ""; const provinceName = province ? pack.provinces[province].fullName + ", " : "";
const stateName = pack.states[b.state].fullName || pack.states[b.state].name; const stateName = pack.states[b.state].fullName || pack.states[b.state].name;
document.getElementById("burgProvinceAndState").innerHTML = provinceName + stateName; byId("burgProvinceAndState").innerHTML = provinceName + stateName;
document.getElementById("burgName").value = b.name; byId("burgName").value = b.name;
document.getElementById("burgType").value = b.type || "Generic"; byId("burgType").value = b.type || "Generic";
document.getElementById("burgPopulation").value = rn(b.population * populationRate * urbanization); byId("burgPopulation").value = rn(b.population * populationRate * urbanization);
document.getElementById("burgEditAnchorStyle").style.display = +b.port ? "inline-block" : "none"; byId("burgEditAnchorStyle").style.display = +b.port ? "inline-block" : "none";
// update list and select culture // update list and select culture
const cultureSelect = document.getElementById("burgCulture"); const cultureSelect = byId("burgCulture");
cultureSelect.options.length = 0; cultureSelect.options.length = 0;
const cultures = pack.cultures.filter(c => !c.removed); const cultures = pack.cultures.filter(c => !c.removed);
cultures.forEach(c => cultureSelect.options.add(new Option(c.name, c.i, false, c.i === b.culture))); cultures.forEach(c => cultureSelect.options.add(new Option(c.name, c.i, false, c.i === b.culture)));
const temperature = grid.cells.temp[pack.cells.g[b.cell]]; const temperature = grid.cells.temp[pack.cells.g[b.cell]];
document.getElementById("burgTemperature").innerHTML = convertTemperature(temperature); byId("burgTemperature").innerHTML = convertTemperature(temperature);
document.getElementById("burgTemperatureLikeIn").innerHTML = getTemperatureLikeness(temperature); byId("burgTemperatureLikeIn").innerHTML = getTemperatureLikeness(temperature);
document.getElementById("burgElevation").innerHTML = getHeight(pack.cells.h[b.cell]); byId("burgElevation").innerHTML = getHeight(pack.cells.h[b.cell]);
// toggle features // toggle features
if (b.capital) document.getElementById("burgCapital").classList.remove("inactive"); if (b.capital) byId("burgCapital").classList.remove("inactive");
else document.getElementById("burgCapital").classList.add("inactive"); else byId("burgCapital").classList.add("inactive");
if (b.port) document.getElementById("burgPort").classList.remove("inactive"); if (b.port) byId("burgPort").classList.remove("inactive");
else document.getElementById("burgPort").classList.add("inactive"); else byId("burgPort").classList.add("inactive");
if (b.citadel) document.getElementById("burgCitadel").classList.remove("inactive"); if (b.citadel) byId("burgCitadel").classList.remove("inactive");
else document.getElementById("burgCitadel").classList.add("inactive"); else byId("burgCitadel").classList.add("inactive");
if (b.walls) document.getElementById("burgWalls").classList.remove("inactive"); if (b.walls) byId("burgWalls").classList.remove("inactive");
else document.getElementById("burgWalls").classList.add("inactive"); else byId("burgWalls").classList.add("inactive");
if (b.plaza) document.getElementById("burgPlaza").classList.remove("inactive"); if (b.plaza) byId("burgPlaza").classList.remove("inactive");
else document.getElementById("burgPlaza").classList.add("inactive"); else byId("burgPlaza").classList.add("inactive");
if (b.temple) document.getElementById("burgTemple").classList.remove("inactive"); if (b.temple) byId("burgTemple").classList.remove("inactive");
else document.getElementById("burgTemple").classList.add("inactive"); else byId("burgTemple").classList.add("inactive");
if (b.shanty) document.getElementById("burgShanty").classList.remove("inactive"); if (b.shanty) byId("burgShanty").classList.remove("inactive");
else document.getElementById("burgShanty").classList.add("inactive"); else byId("burgShanty").classList.add("inactive");
//toggle lock //toggle lock
updateBurgLockIcon(); updateBurgLockIcon();
// select group // select group
const group = elSelected.node().parentNode.id; const group = elSelected.node().parentNode.id;
const select = document.getElementById("burgSelectGroup"); const select = byId("burgSelectGroup");
select.options.length = 0; // remove all options select.options.length = 0; // remove all options
burgLabels.selectAll("g").each(function () { burgLabels.selectAll("g").each(function () {
@ -109,20 +108,13 @@ function editBurg(id) {
// set emlem image // set emlem image
const coaID = "burgCOA" + id; const coaID = "burgCOA" + id;
COArenderer.trigger(coaID, b.coa); COArenderer.trigger(coaID, b.coa);
document.getElementById("burgEmblem").setAttribute("href", "#" + coaID); byId("burgEmblem").setAttribute("href", "#" + coaID);
if (options.showMFCGMap) { if (options.showBurgPreview) {
document.getElementById("mfcgPreviewSection").style.display = "block"; byId("burgPreviewSection").style.display = "block";
updateMFCGFrame(b); updateBurgPreview(b);
if (b.link) {
document.getElementById("mfcgBurgSeedSection").style.display = "none";
} else { } else {
document.getElementById("mfcgBurgSeedSection").style.display = "inline-block"; byId("burgPreviewSection").style.display = "none";
document.getElementById("mfcgBurgSeed").value = getBurgSeed(b);
}
} else {
document.getElementById("mfcgPreviewSection").style.display = "none";
} }
} }
@ -141,15 +133,15 @@ function editBurg(id) {
function showGroupSection() { function showGroupSection() {
document.querySelectorAll("#burgBottom > button").forEach(el => (el.style.display = "none")); document.querySelectorAll("#burgBottom > button").forEach(el => (el.style.display = "none"));
document.getElementById("burgGroupSection").style.display = "inline-block"; byId("burgGroupSection").style.display = "inline-block";
} }
function hideGroupSection() { function hideGroupSection() {
document.querySelectorAll("#burgBottom > button").forEach(el => (el.style.display = "inline-block")); document.querySelectorAll("#burgBottom > button").forEach(el => (el.style.display = "inline-block"));
document.getElementById("burgGroupSection").style.display = "none"; byId("burgGroupSection").style.display = "none";
document.getElementById("burgInputGroup").style.display = "none"; byId("burgInputGroup").style.display = "none";
document.getElementById("burgInputGroup").value = ""; byId("burgInputGroup").value = "";
document.getElementById("burgSelectGroup").style.display = "inline-block"; byId("burgSelectGroup").style.display = "inline-block";
} }
function changeGroup() { function changeGroup() {
@ -178,7 +170,7 @@ function editBurg(id) {
.replace(/ /g, "_") .replace(/ /g, "_")
.replace(/[^\w\s]/gi, ""); .replace(/[^\w\s]/gi, "");
if (document.getElementById(group)) { if (byId(group)) {
tip("Element with this id already exists. Please provide a unique name", false, "error"); tip("Element with this id already exists. Please provide a unique name", false, "error");
return; return;
} }
@ -206,10 +198,10 @@ function editBurg(id) {
// just rename if only 1 element left // just rename if only 1 element left
const count = elSelected.node().parentNode.childElementCount; const count = elSelected.node().parentNode.childElementCount;
if (oldGroup !== "cities" && oldGroup !== "towns" && count === 1) { if (oldGroup !== "cities" && oldGroup !== "towns" && count === 1) {
document.getElementById("burgSelectGroup").selectedOptions[0].remove(); byId("burgSelectGroup").selectedOptions[0].remove();
document.getElementById("burgSelectGroup").options.add(new Option(group, group, false, true)); byId("burgSelectGroup").options.add(new Option(group, group, false, true));
toggleNewGroupInput(); toggleNewGroupInput();
document.getElementById("burgInputGroup").value = ""; byId("burgInputGroup").value = "";
labelG.id = group; labelG.id = group;
iconG.id = group; iconG.id = group;
if (anchor) anchorG.id = group; if (anchor) anchorG.id = group;
@ -217,9 +209,9 @@ function editBurg(id) {
} }
// create new groups // create new groups
document.getElementById("burgSelectGroup").options.add(new Option(group, group, false, true)); byId("burgSelectGroup").options.add(new Option(group, group, false, true));
toggleNewGroupInput(); toggleNewGroupInput();
document.getElementById("burgInputGroup").value = ""; byId("burgInputGroup").value = "";
addBurgsGroup(group); addBurgsGroup(group);
moveBurgToGroup(id, group); moveBurgToGroup(id, group);
@ -300,7 +292,10 @@ function editBurg(id) {
function changePopulation() { function changePopulation() {
const id = +elSelected.attr("data-id"); const id = +elSelected.attr("data-id");
const burg = pack.burgs[id];
pack.burgs[id].population = rn(burgPopulation.value / populationRate / urbanization, 4); pack.burgs[id].population = rn(burgPopulation.value / populationRate / urbanization, 4);
updateBurgPreview(burg);
} }
function toggleFeature() { function toggleFeature() {
@ -314,9 +309,9 @@ function editBurg(id) {
if (burg[feature]) this.classList.remove("inactive"); if (burg[feature]) this.classList.remove("inactive");
else if (!burg[feature]) this.classList.add("inactive"); else if (!burg[feature]) this.classList.add("inactive");
if (burg.port) document.getElementById("burgEditAnchorStyle").style.display = "inline-block"; if (burg.port) byId("burgEditAnchorStyle").style.display = "inline-block";
else document.getElementById("burgEditAnchorStyle").style.display = "none"; else byId("burgEditAnchorStyle").style.display = "none";
updateMFCGFrame(burg); updateBurgPreview(burg);
} }
function toggleBurgLockButton() { function toggleBurgLockButton() {
@ -331,22 +326,22 @@ function editBurg(id) {
const id = +elSelected.attr("data-id"); const id = +elSelected.attr("data-id");
const b = pack.burgs[id]; const b = pack.burgs[id];
if (b.lock) { if (b.lock) {
document.getElementById("burgLock").classList.remove("icon-lock-open"); byId("burgLock").classList.remove("icon-lock-open");
document.getElementById("burgLock").classList.add("icon-lock"); byId("burgLock").classList.add("icon-lock");
} else { } else {
document.getElementById("burgLock").classList.remove("icon-lock"); byId("burgLock").classList.remove("icon-lock");
document.getElementById("burgLock").classList.add("icon-lock-open"); byId("burgLock").classList.add("icon-lock-open");
} }
} }
function showStyleSection() { function showStyleSection() {
document.querySelectorAll("#burgBottom > button").forEach(el => (el.style.display = "none")); document.querySelectorAll("#burgBottom > button").forEach(el => (el.style.display = "none"));
document.getElementById("burgStyleSection").style.display = "inline-block"; byId("burgStyleSection").style.display = "inline-block";
} }
function hideStyleSection() { function hideStyleSection() {
document.querySelectorAll("#burgBottom > button").forEach(el => (el.style.display = "inline-block")); document.querySelectorAll("#burgBottom > button").forEach(el => (el.style.display = "inline-block"));
document.getElementById("burgStyleSection").style.display = "none"; byId("burgStyleSection").style.display = "none";
} }
function editGroupLabelStyle() { function editGroupLabelStyle() {
@ -364,39 +359,38 @@ function editBurg(id) {
editStyle("anchors", g); editStyle("anchors", g);
} }
function updateMFCGFrame(burg) { function updateBurgPreview(burg) {
const mfcgURL = getMFCGlink(burg); const src = getBurgLink(burg) + "&preview=1";
document.getElementById("mfcgPreview").setAttribute("src", mfcgURL + "&preview=1");
document.getElementById("mfcgLink").setAttribute("href", mfcgURL); // recreate object to force reload (Chrome bug)
const container = byId("burgPreviewObject");
container.innerHTML = "";
const object = document.createElement("object");
object.style.width = "100%";
object.data = src;
container.insertBefore(object, null);
} }
function changeSeed() { function openBurgLink() {
const id = +elSelected.attr("data-id"); const id = +elSelected.attr("data-id");
const burg = pack.burgs[id]; const burg = pack.burgs[id];
const burgSeed = +this.value;
burg.MFCG = burgSeed; openURL(getBurgLink(burg));
updateMFCGFrame(burg);
} }
function randomizeSeed() { function changeBurgLink() {
const id = +elSelected.attr("data-id"); const id = +elSelected.attr("data-id");
const burg = pack.burgs[id]; const burg = pack.burgs[id];
const burgSeed = rand(1e9 - 1);
burg.MFCG = burgSeed;
updateMFCGFrame(burg);
document.getElementById("mfcgBurgSeed").value = burgSeed;
}
function addCustomMfcgLink() { prompt(
const id = +elSelected.attr("data-id"); "Provide custom link to the burg map. It can be a link to Medieval Fantasy City Generator, a different tool, or just an image. Leave empty to use the default map",
const burg = pack.burgs[id]; {default: getBurgLink(burg), required: false},
const message = link => {
"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; if (link) burg.link = link;
else delete burg.link; else delete burg.link;
updateMFCGFrame(burg); updateBurgPreview(burg);
}); }
);
} }
function openEmblemEdit() { function openEmblemEdit() {
@ -405,16 +399,16 @@ function editBurg(id) {
editEmblem("burg", "burgCOA" + id, burg); editEmblem("burg", "burgCOA" + id, burg);
} }
function toggleMFCGMap() { function toggleBurgPreview() {
options.showMFCGMap = !options.showMFCGMap; options.showBurgPreview = !options.showBurgPreview;
document.getElementById("mfcgPreviewSection").style.display = options.showMFCGMap ? "block" : "none"; byId("burgPreviewSection").style.display = options.showBurgPreview ? "block" : "none";
document.getElementById("burgToggleMFCGMap").className = options.showMFCGMap ? "icon-map" : "icon-map-o"; byId("burgTogglePreview").className = options.showBurgPreview ? "icon-map" : "icon-map-o";
} }
function toggleRelocateBurg() { function toggleRelocateBurg() {
const toggler = document.getElementById("toggleCells"); const toggler = byId("toggleCells");
document.getElementById("burgRelocate").classList.toggle("pressed"); byId("burgRelocate").classList.toggle("pressed");
if (document.getElementById("burgRelocate").classList.contains("pressed")) { if (byId("burgRelocate").classList.contains("pressed")) {
viewbox.style("cursor", "crosshair").on("click", relocateBurgOnClick); viewbox.style("cursor", "crosshair").on("click", relocateBurgOnClick);
tip("Click on map to relocate burg. Hold Shift for continuous move", true); tip("Click on map to relocate burg. Hold Shift for continuous move", true);
if (!layerIsOn("toggleCells")) { if (!layerIsOn("toggleCells")) {
@ -534,7 +528,7 @@ function editBurg(id) {
} }
function closeBurgEditor() { function closeBurgEditor() {
document.getElementById("burgRelocate").classList.remove("pressed"); byId("burgRelocate").classList.remove("pressed");
burgLabels.selectAll("text").call(d3.drag().on("drag", null)).classed("draggable", false); burgLabels.selectAll("text").call(d3.drag().on("drag", null)).classed("draggable", false);
unselect(); unselect();
} }

View file

@ -514,7 +514,7 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) {
data += b.temple ? "temple," : ","; data += b.temple ? "temple," : ",";
data += b.shanty ? "shanty town," : ","; data += b.shanty ? "shanty town," : ",";
data += b.coa ? JSON.stringify(b.coa).replace(/"/g, "").replace(/,/g, ";") + "," : ","; data += b.coa ? JSON.stringify(b.coa).replace(/"/g, "").replace(/,/g, ";") + "," : ",";
data += getMFCGlink(b); data += getBurgLink(b);
data += "\n"; data += "\n";
}); });

View file

@ -243,25 +243,22 @@ function removeBurg(id) {
} }
} }
function toggleCapital(burg) { function toggleCapital(burgId) {
const state = pack.burgs[burg].state; const {burgs, states} = pack;
if (!state) { if (burgs[burgId].capital)
tip("Neutral lands cannot have a capital", false, "error"); return tip("To change capital please assign a capital status to another burg of this state", false, "error");
return;
}
if (pack.burgs[burg].capital) {
tip("To change capital please assign a capital status to another burg of this state", false, "error");
return;
}
const old = pack.states[state].capital;
// change statuses const stateId = burgs[burgId].state;
pack.states[state].capital = burg; if (!stateId) return tip("Neutral lands cannot have a capital", false, "error");
pack.states[state].center = pack.burgs[burg].cell;
pack.burgs[burg].capital = 1; const prevCapitalId = states[stateId].capital;
pack.burgs[old].capital = 0; states[stateId].capital = burgId;
moveBurgToGroup(burg, "cities"); states[stateId].center = burgs[burgId].cell;
moveBurgToGroup(old, "towns"); burgs[burgId].capital = 1;
burgs[prevCapitalId].capital = 0;
moveBurgToGroup(burgId, "cities");
moveBurgToGroup(prevCapitalId, "towns");
} }
function togglePort(burg) { function togglePort(burg) {
@ -291,16 +288,20 @@ function togglePort(burg) {
.attr("height", size); .attr("height", size);
} }
function getBurgSeed(burg) { function getBurgLink(burg) {
return burg.MFCG || Number(`${seed}${String(burg.i).padStart(4, 0)}`);
}
function getMFCGlink(burg) {
if (burg.link) return burg.link; if (burg.link) return burg.link;
const population = burg.population * populationRate * urbanization;
if (population >= options.villageMaxPopulation || burg.citadel || burg.walls || burg.temple || burg.shanty)
return createMfcgLink(burg);
return createVillageGeneratorLink(burg);
}
function createMfcgLink(burg) {
const {cells} = pack; const {cells} = pack;
const {i, name, population: burgPopulation, cell} = burg; const {i, name, population: burgPopulation, cell} = burg;
const seed = getBurgSeed(burg); const burgSeed = burg.MFCG || seed + String(burg.i).padStart(4, 0);
const sizeRaw = 2.13 * Math.pow((burgPopulation * populationRate) / urbanDensity, 0.385); const sizeRaw = 2.13 * Math.pow((burgPopulation * populationRate) / urbanDensity, 0.385);
const size = minmax(Math.ceil(sizeRaw), 6, 100); const size = minmax(Math.ceil(sizeRaw), 6, 100);
@ -308,11 +309,19 @@ function getMFCGlink(burg) {
const river = cells.r[cell] ? 1 : 0; const river = cells.r[cell] ? 1 : 0;
const coast = Number(burg.port > 0); const coast = Number(burg.port > 0);
const sea = coast && cells.haven[cell] ? getSeaDirections(cell) : null; const sea = (() => {
if (!coast || !cells.haven[cell]) return null;
// calculate see direction: 0 = south, 0.5 = west, 1 = north, 1.5 = east
const p1 = cells.p[cell];
const p2 = cells.p[cells.haven[cell]];
let deg = (Math.atan2(p2[1] - p1[1], p2[0] - p1[0]) * 180) / Math.PI - 90;
if (deg < 0) deg += 360;
return rn(normalize(deg, 0, 360) * 2, 2);
})();
const biome = cells.biome[cell];
const arableBiomes = river ? [1, 2, 3, 4, 5, 6, 7, 8] : [5, 6, 7, 8]; const arableBiomes = river ? [1, 2, 3, 4, 5, 6, 7, 8] : [5, 6, 7, 8];
const farms = +arableBiomes.includes(biome); const farms = +arableBiomes.includes(cells.biome[cell]);
const citadel = +burg.citadel; const citadel = +burg.citadel;
const urban_castle = +(citadel && each(2)(i)); const urban_castle = +(citadel && each(2)(i));
@ -324,19 +333,12 @@ function getMFCGlink(burg) {
const temple = +burg.temple; const temple = +burg.temple;
const shantytown = +burg.shanty; const shantytown = +burg.shanty;
function getSeaDirections(i) { const url = new URL("https://watabou.github.io/city-generator/");
const p1 = cells.p[i]; url.search = new URLSearchParams({
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;
return rn(normalize(deg, 0, 360) * 2, 2); // 0 = south, 0.5 = west, 1 = north, 1.5 = east
}
const parameters = {
name, name,
population, population,
size, size,
seed, seed: burgSeed,
river, river,
coast, coast,
farms, farms,
@ -348,14 +350,60 @@ function getMFCGlink(burg) {
walls, walls,
shantytown, shantytown,
gates: -1 gates: -1
}; });
const url = new URL("https://watabou.github.io/city-generator/");
url.search = new URLSearchParams(parameters);
if (sea) url.searchParams.append("sea", sea); if (sea) url.searchParams.append("sea", sea);
return url.toString(); return url.toString();
} }
function createVillageGeneratorLink(burg) {
const {cells, features} = pack;
const {i, population, cell} = burg;
const pop = rn(population * populationRate * urbanization);
const burgSeed = seed + String(i).padStart(4, 0);
const tags = [];
if (cells.r[cell] && cells.haven[cell]) tags.push("estuary");
else if (cells.haven[cell] && features[cells.f[cell]].cells === 1) tags.push("island,district");
else if (burg.port) tags.push("coast");
else if (cells.conf[cell]) tags.push("confluence");
else if (cells.r[cell]) tags.push("river");
else if (pop < 200 && each(4)(cell)) tags.push("pond");
const roadsAround = cells.c[cell].filter(c => cells.h[c] >= 20 && cells.road[c]).length;
if (roadsAround > 1) tags.push("highway");
else if (roadsAround === 1) tags.push("dead end");
else tags.push("isolated");
const biome = cells.biome[cell];
const arableBiomes = cells.r[cell] ? [1, 2, 3, 4, 5, 6, 7, 8] : [5, 6, 7, 8];
if (!arableBiomes.includes(biome)) tags.push("uncultivated");
else if (each(6)(cell)) tags.push("farmland");
const temp = grid.cells.temp[cells.g[cell]];
if (temp <= 0 || temp > 28 || (temp > 25 && each(3)(cell))) tags.push("no orchards");
if (!burg.plaza) tags.push("no square");
if (pop < 100) tags.push("sparse");
else if (pop > 300) tags.push("dense");
const width = (() => {
if (pop > 1500) return 1600;
if (pop > 1000) return 1400;
if (pop > 500) return 1000;
if (pop > 200) return 800;
if (pop > 100) return 600;
return 400;
})();
const height = rn(width / 2.2);
const url = new URL("https://watabou.github.io/village-generator/");
url.search = new URLSearchParams({pop, name: "", seed: burgSeed, width, height, tags});
return url.toString();
}
// draw legend box // draw legend box
function drawLegend(name, data) { function drawLegend(name, data) {
legend.selectAll("*").remove(); // fully redraw every time legend.selectAll("*").remove(); // fully redraw every time
@ -1096,12 +1144,12 @@ function selectIcon(initial, callback) {
input.oninput = e => callback(input.value); input.oninput = e => callback(input.value);
table.onclick = e => { table.onclick = e => {
if (e.target.tagName === "TD") { if (e.target.tagName === "TD") {
input.value = e.target.innerHTML; input.value = e.target.textContent;
callback(input.value); callback(input.value);
} }
}; };
table.onmouseover = e => { table.onmouseover = e => {
if (e.target.tagName === "TD") tip(`Click to select ${e.target.innerHTML} icon`); if (e.target.tagName === "TD") tip(`Click to select ${e.target.textContent} icon`);
}; };
$("#iconSelector").dialog({ $("#iconSelector").dialog({
@ -1176,13 +1224,13 @@ function refreshAllEditors() {
// dynamically loaded editors // dynamically loaded editors
async function editStates() { async function editStates() {
if (customization) return; if (customization) return;
const Editor = await import("../dynamic/editors/states-editor.js?v=1.96.00"); const Editor = await import("../dynamic/editors/states-editor.js?v=1.96.06");
Editor.open(); Editor.open();
} }
async function editCultures() { async function editCultures() {
if (customization) return; if (customization) return;
const Editor = await import("../dynamic/editors/cultures-editor.js?v=1.96.00"); const Editor = await import("../dynamic/editors/cultures-editor.js?v=1.96.01");
Editor.open(); Editor.open();
} }

View file

@ -193,8 +193,15 @@ function showElevationProfile(data, routeLen, isRiver) {
.attr("d", "M0,0 V4 L2,2 Z") .attr("d", "M0,0 V4 L2,2 Z")
.attr("fill", "darkgray"); .attr("fill", "darkgray");
let colors = getColorScheme(terrs.attr("scheme")); const colors = getColorScheme("natural");
const landdef = chart.select("defs").append("linearGradient").attr("id", "landdef").attr("x1", "0%").attr("y1", "0%").attr("x2", "0%").attr("y2", "100%"); const landdef = chart
.select("defs")
.append("linearGradient")
.attr("id", "landdef")
.attr("x1", "0%")
.attr("y1", "0%")
.attr("x2", "0%")
.attr("y2", "100%");
if (chartData.mah == chartData.mih) { if (chartData.mah == chartData.mih) {
landdef landdef
@ -247,7 +254,14 @@ function showElevationProfile(data, routeLen, isRiver) {
path += " L" + parseInt(xscale(extra.length) + +xOffset) + "," + parseInt(yscale(0) + +yOffset); path += " L" + parseInt(xscale(extra.length) + +xOffset) + "," + parseInt(yscale(0) + +yOffset);
path += " L" + parseInt(xscale(0) + +xOffset) + "," + parseInt(yscale(0) + +yOffset); path += " L" + parseInt(xscale(0) + +xOffset) + "," + parseInt(yscale(0) + +yOffset);
path += "Z"; path += "Z";
chart.append("g").attr("id", "epland").append("path").attr("d", path).attr("stroke", "purple").attr("stroke-width", "0").attr("fill", "url(#landdef)"); chart
.append("g")
.attr("id", "epland")
.append("path")
.attr("d", path)
.attr("stroke", "purple")
.attr("stroke-width", "0")
.attr("fill", "url(#landdef)");
// biome / heights // biome / heights
let g = chart.append("g").attr("id", "epbiomes"); let g = chart.append("g").attr("id", "epbiomes");
@ -289,7 +303,14 @@ function showElevationProfile(data, routeLen, isRiver) {
chartData.cell[k] + chartData.cell[k] +
")"; ")";
g.append("rect").attr("stroke", c).attr("fill", c).attr("x", x).attr("y", y).attr("width", xscale(1)).attr("height", 15).attr("data-tip", dataTip); g.append("rect")
.attr("stroke", c)
.attr("fill", c)
.attr("x", x)
.attr("y", y)
.attr("width", xscale(1))
.attr("height", 15)
.attr("data-tip", dataTip);
} }
const xAxis = d3 const xAxis = d3
@ -371,7 +392,17 @@ function showElevationProfile(data, routeLen, isRiver) {
// arrow from burg name to graph line // arrow from burg name to graph line
g.append("path") g.append("path")
.attr("id", "eparrow" + b) .attr("id", "eparrow" + b)
.attr("d", "M" + x1.toString() + "," + (y1 + 3).toString() + "L" + x1.toString() + "," + parseInt(chartData.points[k][1] - 3).toString()) .attr(
"d",
"M" +
x1.toString() +
"," +
(y1 + 3).toString() +
"L" +
x1.toString() +
"," +
parseInt(chartData.points[k][1] - 3).toString()
)
.attr("stroke", "darkgray") .attr("stroke", "darkgray")
.attr("fill", "lightgray") .attr("fill", "lightgray")
.attr("stroke-width", "1") .attr("stroke-width", "1")

View file

@ -54,7 +54,9 @@ function overviewMilitary() {
const insert = html => document.getElementById("militaryTotal").insertAdjacentHTML("beforebegin", html); const insert = html => document.getElementById("militaryTotal").insertAdjacentHTML("beforebegin", html);
for (const u of options.military) { for (const u of options.military) {
const label = capitalize(u.name.replace(/_/g, " ")); const label = capitalize(u.name.replace(/_/g, " "));
insert(`<div data-tip="State ${u.name} units number. Click to sort" class="sortable removable" data-sortby="${u.name}">${label}&nbsp;</div>`); insert(
`<div data-tip="State ${u.name} units number. Click to sort" class="sortable removable" data-sortby="${u.name}">${label}&nbsp;</div>`
);
} }
header.querySelectorAll(".removable").forEach(function (e) { header.querySelectorAll(".removable").forEach(function (e) {
e.addEventListener("click", function () { e.addEventListener("click", function () {
@ -76,7 +78,9 @@ function overviewMilitary() {
const rate = (total / population) * 100; const rate = (total / population) * 100;
const sortData = options.military.map(u => `data-${u.name}="${getForces(u)}"`).join(" "); const sortData = options.military.map(u => `data-${u.name}="${getForces(u)}"`).join(" ");
const lineData = options.military.map(u => `<div data-type="${u.name}" data-tip="State ${u.name} units number">${getForces(u)}</div>`).join(" "); const lineData = options.military
.map(u => `<div data-type="${u.name}" data-tip="State ${u.name} units number">${getForces(u)}</div>`)
.join(" ");
lines += /* html */ `<div lines += /* html */ `<div
class="states" class="states"
@ -91,9 +95,14 @@ function overviewMilitary() {
<fill-box data-tip="${s.fullName}" fill="${s.color}" disabled></fill-box> <fill-box data-tip="${s.fullName}" fill="${s.color}" disabled></fill-box>
<input data-tip="${s.fullName}" style="width:6em" value="${s.name}" readonly /> <input data-tip="${s.fullName}" style="width:6em" value="${s.name}" readonly />
${lineData} ${lineData}
<div data-type="total" data-tip="Total state military personnel (considering crew)" style="font-weight: bold">${si(total)}</div> <div data-type="total" data-tip="Total state military personnel (considering crew)" style="font-weight: bold">${si(
total
)}</div>
<div data-type="population" data-tip="State population">${si(population)}</div> <div data-type="population" data-tip="State population">${si(population)}</div>
<div data-type="rate" data-tip="Military personnel rate (% of state population). Depends on war alert">${rn(rate, 2)}%</div> <div data-type="rate" data-tip="Military personnel rate (% of state population). Depends on war alert">${rn(
rate,
2
)}%</div>
<input <input
data-tip="War Alert. Editable modifier to military forces number, depends of political situation" data-tip="War Alert. Editable modifier to military forces number, depends of political situation"
style="width:4.1em" style="width:4.1em"
@ -131,7 +140,9 @@ function overviewMilitary() {
}); });
const getForces = u => s.military.reduce((s, r) => s + (r.u[u.name] || 0), 0); const getForces = u => s.military.reduce((s, r) => s + (r.u[u.name] || 0), 0);
options.military.forEach(u => (line.dataset[u.name] = line.querySelector(`div[data-type='${u.name}']`).innerHTML = getForces(u))); options.military.forEach(
u => (line.dataset[u.name] = line.querySelector(`div[data-type='${u.name}']`).innerHTML = getForces(u))
);
const population = rn((s.rural + s.urban * urbanization) * populationRate); const population = rn((s.rural + s.urban * urbanization) * populationRate);
const total = (line.dataset.total = options.military.reduce((s, u) => s + getForces(u) * u.crew, 0)); const total = (line.dataset.total = options.military.reduce((s, u) => s + getForces(u) * u.crew, 0));
@ -237,7 +248,16 @@ function overviewMilitary() {
position: {my: "center", at: "center", of: "svg"}, position: {my: "center", at: "center", of: "svg"},
buttons: { buttons: {
Apply: applyMilitaryOptions, Apply: applyMilitaryOptions,
Add: () => addUnitLine({icon: "🛡️", name: "custom" + militaryOptionsTable.rows.length, rural: 0.2, urban: 0.5, crew: 1, power: 1, type: "melee"}), Add: () =>
addUnitLine({
icon: "🛡️",
name: "custom" + militaryOptionsTable.rows.length,
rural: 0.2,
urban: 0.5,
crew: 1,
power: 1,
type: "melee"
}),
Restore: restoreDefaultUnits, Restore: restoreDefaultUnits,
Cancel: function () { Cancel: function () {
$(this).dialog("close"); $(this).dialog("close");
@ -262,7 +282,7 @@ function overviewMilitary() {
if (el.tagName !== "BUTTON") return; if (el.tagName !== "BUTTON") return;
const type = el.dataset.type; const type = el.dataset.type;
if (type === "icon") return selectIcon(el.innerHTML, v => (el.innerHTML = v)); if (type === "icon") return selectIcon(el.textContent, v => (el.textContent = v));
if (type === "biomes") { if (type === "biomes") {
const {i, name, color} = biomesData; const {i, name, color} = biomesData;
const biomesArray = Array(i.length).fill(null); const biomesArray = Array(i.length).fill(null);
@ -294,7 +314,9 @@ function overviewMilitary() {
function addUnitLine(unit) { function addUnitLine(unit) {
const {type, icon, name, rural, urban, power, crew, separate} = unit; const {type, icon, name, rural, urban, power, crew, separate} = unit;
const row = document.createElement("tr"); const row = document.createElement("tr");
const typeOptions = types.map(t => `<option ${type === t ? "selected" : ""} value="${t}">${t}</option>`).join(" "); const typeOptions = types
.map(t => `<option ${type === t ? "selected" : ""} value="${t}">${t}</option>`)
.join(" ");
const getLimitButton = attr => const getLimitButton = attr =>
`<button `<button
@ -305,7 +327,9 @@ function overviewMilitary() {
${getLimitText(unit[attr])} ${getLimitText(unit[attr])}
</button>`; </button>`;
row.innerHTML = /* html */ `<td><button data-type="icon" data-tip="Click to select unit icon">${icon || " "}</button></td> row.innerHTML = /* html */ `<td><button data-type="icon" data-tip="Click to select unit icon">${
icon || " "
}</button></td>
<td><input data-tip="Type unit name. If name is changed for existing unit, old unit will be replaced" value="${name}" /></td> <td><input data-tip="Type unit name. If name is changed for existing unit, old unit will be replaced" value="${name}" /></td>
<td>${getLimitButton("biomes")}</td> <td>${getLimitButton("biomes")}</td>
<td>${getLimitButton("states")}</td> <td>${getLimitButton("states")}</td>
@ -344,7 +368,9 @@ function overviewMilitary() {
const lines = filtered.map( const lines = filtered.map(
({i, name, fullName, color}) => ({i, name, fullName, color}) =>
`<tr data-tip="${name}"><td><span style="color:${color}">⬤</span></td> `<tr data-tip="${name}"><td><span style="color:${color}">⬤</span></td>
<td><input data-i="${i}" id="el${i}" type="checkbox" class="checkbox" ${!initial.length || initial.includes(i) ? "checked" : ""} > <td><input data-i="${i}" id="el${i}" type="checkbox" class="checkbox" ${
!initial.length || initial.includes(i) ? "checked" : ""
} >
<label for="el${i}" class="checkbox-label">${fullName || name}</label> <label for="el${i}" class="checkbox-label">${fullName || name}</label>
</td></tr>` </td></tr>`
); );
@ -387,17 +413,16 @@ function overviewMilitary() {
function applyMilitaryOptions() { function applyMilitaryOptions() {
const unitLines = Array.from(tableBody.querySelectorAll("tr")); const unitLines = Array.from(tableBody.querySelectorAll("tr"));
const names = unitLines.map(r => r.querySelector("input").value.replace(/[&\/\\#, +()$~%.'":*?<>{}]/g, "_")); const names = unitLines.map(r => r.querySelector("input").value.replace(/[&\/\\#, +()$~%.'":*?<>{}]/g, "_"));
if (new Set(names).size !== names.length) { if (new Set(names).size !== names.length) return tip("All units should have unique names", false, "error");
tip("All units should have unique names", false, "error");
return;
}
$("#militaryOptions").dialog("close"); $("#militaryOptions").dialog("close");
options.military = unitLines.map((r, i) => { options.military = unitLines.map((r, i) => {
const elements = Array.from(r.querySelectorAll("input, button, select")); const elements = Array.from(r.querySelectorAll("input, button, select"));
const [icon, name, biomes, states, cultures, religions, rural, urban, crew, power, type, separate] = elements.map(el => { const [icon, name, biomes, states, cultures, religions, rural, urban, crew, power, type, separate] =
elements.map(el => {
const {type, value} = el.dataset || {}; const {type, value} = el.dataset || {};
if (type === "icon") return el.innerHTML || ""; if (type === "icon") return el.textContent || "";
if (type) return value ? value.split(",").map(v => parseInt(v)) : null; if (type) return value ? value.split(",").map(v => parseInt(v)) : null;
if (el.type === "number") return +el.value || 0; if (el.type === "number") return +el.value || 0;
if (el.type === "checkbox") return +el.checked || 0; if (el.type === "checkbox") return +el.checked || 0;
@ -419,7 +444,8 @@ function overviewMilitary() {
} }
function militaryRecalculate() { function militaryRecalculate() {
alertMessage.innerHTML = "Are you sure you want to recalculate military forces for all states?<br>Regiments for all states will be regenerated"; alertMessage.innerHTML =
"Are you sure you want to recalculate military forces for all states?<br>Regiments for all states will be regenerated";
$("#alert").dialog({ $("#alert").dialog({
resizable: false, resizable: false,
title: "Remove regiment", title: "Remove regiment",

View file

@ -867,11 +867,9 @@ byId("mapToLoad").addEventListener("change", function () {
}); });
function openExportToPngTiles() { function openExportToPngTiles() {
byId("tileStatus").innerHTML = "";
closeDialogs(); closeDialogs();
updateTilesOptions(); updateTilesOptions();
const status = byId("tileStatus");
status.innerHTML = "";
let loading = null;
const inputs = byId("exportToPngTilesScreen").querySelectorAll("input"); const inputs = byId("exportToPngTilesScreen").querySelectorAll("input");
inputs.forEach(input => input.addEventListener("input", updateTilesOptions)); inputs.forEach(input => input.addEventListener("input", updateTilesOptions));
@ -881,16 +879,7 @@ function openExportToPngTiles() {
title: "Download tiles", title: "Download tiles",
width: "23em", width: "23em",
buttons: { buttons: {
Download: function () { Download: () => exportToPngTiles(),
status.innerHTML = "Preparing for download...";
setTimeout(() => (status.innerHTML = "Downloading. It may take some time."), 1000);
loading = setInterval(() => (status.innerHTML += "."), 1000);
exportToPngTiles().then(() => {
clearInterval(loading);
status.innerHTML = /* html */ `Done. Check file in "Downloads" (crtl + J)`;
setTimeout(() => (status.innerHTML = ""), 8000);
});
},
Cancel: function () { Cancel: function () {
$(this).dialog("close"); $(this).dialog("close");
} }
@ -898,7 +887,6 @@ function openExportToPngTiles() {
close: () => { close: () => {
inputs.forEach(input => input.removeEventListener("input", updateTilesOptions)); inputs.forEach(input => input.removeEventListener("input", updateTilesOptions));
debug.selectAll("*").remove(); debug.selectAll("*").remove();
clearInterval(loading);
} }
}); });
} }
@ -928,18 +916,22 @@ function updateTilesOptions() {
const labels = []; const labels = [];
const tileW = (graphWidth / tilesX) | 0; const tileW = (graphWidth / tilesX) | 0;
const tileH = (graphHeight / tilesY) | 0; const tileH = (graphHeight / tilesY) | 0;
for (let y = 0, i = 0; y + tileH <= graphHeight; y += tileH) {
for (let x = 0; x + tileW <= graphWidth; x += tileW, i++) { const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
for (let y = 0, row = 0; y + tileH <= graphHeight; y += tileH, row++) {
for (let x = 0, column = 1; x + tileW <= graphWidth; x += tileW, column++) {
rects.push(`<rect x=${x} y=${y} width=${tileW} height=${tileH} />`); rects.push(`<rect x=${x} y=${y} width=${tileW} height=${tileH} />`);
labels.push(`<text x=${x + tileW / 2} y=${y + tileH / 2}>${i}</text>`); const label = alphabet[row % alphabet.length] + column;
labels.push(`<text x=${x + tileW / 2} y=${y + tileH / 2}>${label}</text>`);
} }
} }
const rectsG = "<g fill='none' stroke='#000'>" + rects.join("") + "</g>";
const labelsG = debug.html(`
"<g fill='#000' stroke='none' text-anchor='middle' dominant-baseline='central' font-size='24px'>" + <g fill='none' stroke='#000'>${rects.join("")}</g>
labels.join("") + <g fill='#000' stroke='none' text-anchor='middle' dominant-baseline='central' font-size='18px'>${labels.join(
"</g>"; ""
debug.html(rectsG + labelsG); )}</g>
`);
} }
// View mode // View mode

View file

@ -247,13 +247,16 @@ function recreateStates() {
capitalsTree.add([x, y]); capitalsTree.add([x, y]);
// update label id reference // update label id reference
labels byId(`textPath_stateLabel${state.i}`)?.setAttribute("id", `textPath_stateLabel${newId}`);
.select("#states") const $label = byId(`stateLabel${state.i}`);
.select(`#stateLabel${state.i}`) if ($label) {
.attr("id", `stateLabel${newId}`) $label.setAttribute("id", `stateLabel${newId}`);
.select("textPath") const $textPath = $label.querySelector("textPath");
.attr("xlink:href", `#textPath_stateLabel${newId}`); if ($textPath) {
defs.select("#textPaths").select(`#textPath_stateLabel${state.i}`).attr("id", `textPath_stateLabel${newId}`); $textPath.removeAttribute("href");
$textPath.setAttribute("href", `#textPath_stateLabel${newId}`);
}
}
// update emblem id reference // update emblem id reference
byId(`stateCOA${state.i}`)?.setAttribute("id", `stateCOA${newId}`); byId(`stateCOA${state.i}`)?.setAttribute("id", `stateCOA${newId}`);

View file

@ -113,9 +113,11 @@ function editWorld() {
function toKilometer(v) { function toKilometer(v) {
if (unit === "km") return v; if (unit === "km") return v;
else if (unit === "mi") return v * 1.60934; if (unit === "mi") return v * 1.60934;
else if (unit === "lg") return v * 5.556; if (unit === "lg") return v * 4.828;
else if (unit === "vr") return v * 1.0668; if (unit === "vr") return v * 1.0668;
if (unit === "nmi") return v * 1.852;
if (unit === "nlg") return v * 5.556;
return 0; // 0 if distanceUnitInput is a custom unit return 0; // 0 if distanceUnitInput is a custom unit
} }

View file

@ -327,9 +327,10 @@ function drawCellsValue(data) {
// helper function non-used for the main generation // helper function non-used for the main generation
function drawPolygons(data) { function drawPolygons(data) {
const max = d3.max(data), const max = d3.max(data);
min = d3.min(data), const min = d3.min(data);
scheme = getColorScheme(terrs.select("#landHeights").attr("scheme")); const scheme = getColorScheme(terrs.select("#landHeights").attr("scheme"));
data = data.map(d => 1 - normalize(d, min, max)); data = data.map(d => 1 - normalize(d, min, max));
debug.selectAll("polygon").remove(); debug.selectAll("polygon").remove();

View file

@ -1,7 +1,7 @@
"use strict"; "use strict";
// version and caching control // version and caching control
const version = "1.96.00"; // generator version, update each time const version = "1.97.05"; // generator version, update each time
{ {
document.title += " v" + version; document.title += " v" + version;
@ -28,6 +28,7 @@ const version = "1.96.00"; // generator version, update each time
<ul> <ul>
<strong>Latest changes:</strong> <strong>Latest changes:</strong>
<li>Preview villages map</li>
<li>Ability to render ocean heightmap</li> <li>Ability to render ocean heightmap</li>
<li>Scale bar styling features</li> <li>Scale bar styling features</li>
<li>Vignette visual layer and vignette styling options</li> <li>Vignette visual layer and vignette styling options</li>