pull master

This commit is contained in:
mosuzi 2023-07-18 02:08:39 +08:00
commit e6457c4e4e
25 changed files with 930 additions and 447 deletions

View file

@ -1,3 +1,24 @@
:root {
--monospace: Consolas, monospace;
--serif: Georgia, serif;
--sans-serif: Helvetica, Arial, sans-serif;
}
/* hide Google translate header */
body > .skiptranslate {
display: none;
}
/* hide Google translate in-progress widget */
body > .skiptranslate + div {
display: none;
}
/* make translated text wrapper non-blocking */
font {
pointer-events: none;
}
input,
select,
button {
@ -119,7 +140,7 @@ a {
}
#temperature {
font-family: sans-serif;
font-family: var(--sans-serif);
font-weight: 700;
text-anchor: middle;
dominant-baseline: central;
@ -160,7 +181,7 @@ t,
text-shadow: 0 0 4px #000;
dominant-baseline: central;
text-anchor: middle;
font-family: Helvetica;
font-family: var(--sans-serif);
fill-opacity: 1;
}
@ -249,7 +270,7 @@ i.icon-lock {
.chartInfo {
text-align: center;
font-family: sans-serif;
font-family: var(--sans-serif);
font-style: italic;
font-size: 12px;
}
@ -356,7 +377,7 @@ text.drag {
#options {
position: absolute;
font-family: Consolas, monospace;
font-family: var(--monospace);
border: solid 1px #5e4fa2;
margin: 10px;
padding-bottom: 0.3em;
@ -366,7 +387,7 @@ text.drag {
#options input,
#options select,
#options button {
font-family: Consolas, monospace;
font-family: var(--monospace);
}
#collapsible {
@ -387,7 +408,7 @@ text.drag {
div.tab > button#optionsHide {
width: auto;
font-family: Arial;
font-family: var(--sans-serif);
padding: 0.6em 0.45em;
}
@ -421,6 +442,7 @@ button.options:hover {
#aboutContent p {
font-weight: normal;
font-style: normal;
}
#aboutContent a {
@ -590,6 +612,11 @@ input[type="color"]::-webkit-color-swatch-wrapper {
width: 100%;
}
#loadGoogleTranslateButton {
font-size: smaller;
padding: 0.4em 0.5em;
}
#options input[type="color"] {
width: 2em;
padding: 1px;
@ -657,7 +684,7 @@ input[type="color"]::-webkit-color-swatch-wrapper {
border: none;
padding: 0.45em 0.75em;
margin: 0.4em 0;
font-family: Consolas, monospace;
font-family: var(--monospace);
animation: glowing 2s infinite;
}
@ -690,6 +717,11 @@ input[type="color"]::-webkit-color-swatch-wrapper {
margin: 0.35em 0;
transition: 0.1s;
font-size: 1em;
text-transform: capitalize;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.tabcontent button.pressed {
@ -743,6 +775,11 @@ fieldset {
float: left;
width: 28%;
text-align: center;
text-transform: capitalize;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.tabcontent .buttonoff {
@ -936,7 +973,7 @@ fieldset {
padding: 0.1em 0.5em;
float: left;
font-size: 1.2em;
font-family: monospace;
font-family: var(--monospace);
}
#brushesButtons > button {
@ -995,7 +1032,7 @@ fieldset {
background-color: #ffffff95;
color: #05044d;
font-style: italic;
font-family: monospace;
font-family: var(--monospace);
}
#templateBody select {
@ -1694,7 +1731,7 @@ div.states > div.biomeArea {
#emblemUploadControl,
#emblemDownloadControl {
margin-top: 0.3em;
text-align: center;
width: 100%;
}
div.editorLine {
@ -1721,7 +1758,7 @@ div.editorLine {
fill: #f8ffff;
font-size: 12px;
font-weight: bold;
font-family: Arial, Helvetica, sans-serif;
font-family: var(--sans-serif);
cursor: move !important;
}
@ -1734,7 +1771,7 @@ div.editorLine {
#pickerCloseText {
fill: #f8ffff;
font-size: 10px;
font-family: Arial, Helvetica, sans-serif;
font-family: var(--sans-serif);
pointer-events: none;
}
@ -1869,7 +1906,7 @@ div.editorLine {
}
#ruler text {
font-family: "Georgia";
font-family: var(--serif);
fill: #3d3d3d;
text-anchor: middle;
text-shadow: 0 0 4px white;
@ -1892,7 +1929,7 @@ div.editorLine {
#scaleBar text {
fill: #353540;
text-anchor: middle;
font-family: Georgia;
font-family: var(--serif);
}
#militaryOptionsTable select {
@ -1918,7 +1955,7 @@ div.editorLine {
#coordinateLabels {
fill: #333333;
font-family: monospace;
font-family: var(--monospace);
text-shadow: 0 0 4px white;
stroke-width: 0;
dominant-baseline: central;
@ -1973,7 +2010,7 @@ input[type="checkbox"] {
div.textual select,
div.textual textarea,
div.textual input {
font-family: monospace;
font-family: var(--monospace);
}
div.textual fieldset {
@ -1988,13 +2025,13 @@ div.textual span,
}
#namesbaseExamples {
font-family: monospace;
font-family: var(--monospace);
cursor: pointer;
}
#markers {
cursor: pointer;
font-family: monospace;
font-family: var(--monospace);
user-select: none;
text-anchor: middle;
dominant-baseline: central;
@ -2054,7 +2091,7 @@ div.textual span,
outline: 0;
overflow-y: auto;
padding: 0.6em;
font-family: monospace;
font-family: var(--monospace);
background-color: #fff;
border: 1px solid #dedede;
color: #000;
@ -2184,14 +2221,14 @@ svg.button {
#globaAxisLabels {
font-style: italic;
font-size: 9px;
font-family: monospace;
font-family: var(--monospace);
stroke: none;
fill: #001754;
}
#globeLatLabels {
font-size: 12px;
font-family: monospace;
font-family: var(--monospace);
stroke: none;
fill: #001754;
}
@ -2252,7 +2289,7 @@ svg.button {
#errorBox {
font-size: 0.9em;
font-family: Consolas, monospace;
font-family: var(--monospace);
color: #920303;
background-color: #dabdbd91;
padding: 2px;
@ -2287,7 +2324,7 @@ svg.button {
#promptText {
padding: 0 0 0.6em 0;
font-weight: bold;
font-family: sans-serif;
font-family: var(--sans-serif);
}
#mapOverlay {

View file

@ -40,6 +40,7 @@
<style type="text/css">
body {
margin: 0;
top: 0 !important;
font-size: 10px;
overflow: hidden;
}
@ -127,7 +128,7 @@
}
</style>
<link rel="preload" href="index.css?v=1.89.13" as="style" onload="this.onload=null; this.rel='stylesheet'" />
<link rel="preload" href="index.css?v=1.89.21" as="style" onload="this.onload=null; this.rel='stylesheet'" />
<link rel="preload" href="icons.css" as="style" onload="this.onload=null; this.rel='stylesheet'" />
<link rel="preload" href="libs/jquery-ui.css" as="style" onload="this.onload=null; this.rel='stylesheet'" />
</head>
@ -362,10 +363,10 @@
<use href="#rose" x="50%" y="50%" />
</svg>
<div id="loading-typography">
<div id="titleName"><t data-t="titleName">Azgaar's</t></div>
<div id="title"><t data-t="title">Fantasy Map Generator</t></div>
<div id="versionText"><t data-t="version">v</t><span id="version"></span></div>
<p id="loading-text"><t data-t="loading">LOADING</t><span>.</span><span>.</span><span>.</span></p>
<div id="titleName">Azgaar's</div>
<div id="title">Fantasy Map Generator</div>
<div id="versionText"></div>
<p id="loading-text">LOADING<span>.</span><span>.</span><span>.</span></p>
</div>
</div>
@ -373,7 +374,6 @@
<div id="collapsible">
<button
id="optionsTrigger"
data-t="tipOptionsTrigger"
data-tip="Click to show the Menu"
data-shortcut="Tab"
class="options glow"
@ -383,24 +383,22 @@
</button>
<button
id="regenerate"
data-t="tipRegenerate"
data-tip="Click to generate a new map"
data-shortcut="F2"
onclick="regeneratePrompt()"
class="options"
style="display: none"
>
<t data-t="newMap">New Map!</t>
New Map!
</button>
</div>
<div id="options" style="display: none">
<div class="drag-trigger" data-t="optionsDragTrigger" data-tip="Drag to move the Menu"></div>
<div class="drag-trigger" data-tip="Drag to move the Menu"></div>
<div class="tab">
<button
id="optionsHide"
data-t="optionsHide"
data-tip="Click to hide the Menu"
data-shortcut="Tab or Esc"
class="options"
@ -408,26 +406,11 @@
>
</button>
<button id="layersTab" data-t="layersTab" data-tip="Click to change map layers" class="options active">
<t data-t="layers">Layers</t>
</button>
<button id="styleTab" data-t="styleTab" data-tip="Click to open style editor" class="options">
<t data-t="style">Style</t>
</button>
<button
id="optionsTab"
data-t="optionsTab"
data-tip="Click to change generation and UI options"
class="options"
>
<t data-t="options">Options</t>
</button>
<button id="toolsTab" data-t="toolsTab" data-tip="Click to open tools menu" class="options">
<t data-t="tools">Tools</t>
</button>
<button id="aboutTab" data-t="aboutTab" data-tip="Click to see Generator info" class="options">
<t data-t="about">About</t>
</button>
<button id="layersTab" data-tip="Click to change map layers" class="options active">Layers</button>
<button id="styleTab" data-tip="Click to open style editor" class="options">Style</button>
<button id="optionsTab" data-tip="Click to change generation and UI options" class="options">Options</button>
<button id="toolsTab" data-tip="Click to open tools menu" class="options">Tools</button>
<button id="aboutTab" data-tip="Click to see Generator info" class="options">About</button>
</div>
<div id="layersContent" class="tabcontent" style="display: block">
@ -1707,18 +1690,6 @@
Generator settings:
</p>
<table>
<tr data-tip="Set what Generator should do on opening">
<td></td>
<td>Onload behavior</td>
<td>
<select id="onloadMap" data-stored="onloadMap">
<option value="random" selected>Generate random map</option>
<option value="saved">Open last saved map</option>
</select>
</td>
<td></td>
</tr>
<tr
data-tip="Set user interface size. Please note browser zoom also affects interface size (Ctrl + or Ctrl - to change)"
>
@ -1767,6 +1738,33 @@
</td>
</tr>
<tr data-tip="Set autosave interval in minutes. Set 0 to disable autosave. Map is saved to browser memory">
<td></td>
<td>Autosave interval</td>
<td>
<input
id="autosaveIntervalInput"
data-stored="autosaveInterval"
type="range"
min="0"
max="60"
step="1"
value="15"
/>
</td>
<td>
<input
id="autosaveIntervalOutput"
data-stored="autosaveInterval"
type="number"
min="0"
max="60"
step="1"
value="15"
/>
</td>
</tr>
<tr data-tip="Select speech synthesis voice to pronounce generated names">
<td></td>
<td>Speaker voice</td>
@ -1905,17 +1903,19 @@
<td></td>
</tr>
<!-- <tr data-tip="Select language (not all languages are fully supported). Reload the page to apply">
<td></td>
<tr
data-tip="Load Google Translate and select language. Note that automatic translation can break some page functional. In this case reset the language back to English or refresh the page"
>
<td>
<i data-tip="Reset language to English" id="resetLanguage" class="icon-ccw"></i>
</td>
<td>Language</td>
<td>
<select id="selectLanguage" data-stored="lang">
<option value="en" selected>English (100%)</option>
<option value="ru">Русский (1%)</option>
</select>
<button id="loadGoogleTranslateButton">Init Google Translate</button>
<div id="google_translate_element"></div>
</td>
<td></td>
</tr> -->
</tr>
</table>
<div>
@ -2057,7 +2057,7 @@
>
Military
</button>
<button id="regenerateIce" data-tip="Click to icebergs and glaciers">Ice</button>
<button id="regenerateIce" data-tip="Click to regenerate icebergs and glaciers">Ice</button>
<button id="regenerateMarkers" data-tip="Click to regenerate unlocked markers">
Markers <i id="configRegenerateMarkers" class="icon-cog" data-tip="Click to set number multiplier"></i>
</button>
@ -2257,7 +2257,8 @@
<a data-tip="Click to see list of supporters" onclick="showSupporters()">all supporters</a> on Patreon!
</p>
<!-- <div style="display: flex; justify-content: center; padding: 0.4em">
<!-- <div style="display: flex; justify-content: center; padding: 0.8em 0.4em 0.4em; font-family: cursive">
<a
href="https://war.ukraine.ua/support-ukraine"
style="width: 80%"
@ -3254,7 +3255,7 @@
style="margin-left: 0.1em"
></i>
</div>
<iframe id="mfcgPreview" sandbox="allow-scripts allow-same-origin"></iframe>
<iframe id="mfcgPreview" sandbox="allow-scripts"></iframe>
</div>
</div>
@ -4917,7 +4918,10 @@
>
Any image
</button>
<button id="emblemsUploadSVG" data-tip="Upload prepared SVG image, e.g. SVG from Armoria">
<button
id="emblemsUploadSVG"
data-tip="Upload prepared SVG image (SVG from Armoria or SVG processed with 'Optimize vector' tool)"
>
Prepared SVG
</button>
<a
@ -5809,7 +5813,7 @@
</button>
<button
onclick="quickSave()"
data-tip="Save the project to browser storage. It can be unreliable"
data-tip="Save the project to browser storage. It will overwrite the latest autosave"
data-shortcut="F6"
>
browser
@ -7838,9 +7842,10 @@
<script src="libs/d3.min.js"></script>
<script src="libs/priority-queue.min.js"></script>
<script src="libs/delaunator.min.js"></script>
<script src="libs/indexedDB.js?v=1.89.32"></script>
<script src="utils/shorthands.js"></script>
<script src="utils/commonUtils.js"></script>
<script src="utils/commonUtils.js?v=1.89.29"></script>
<script src="utils/arrayUtils.js"></script>
<script src="utils/colorUtils.js"></script>
<script src="utils/graphUtils.js?v=1.88.02"></script>
@ -7863,32 +7868,32 @@
<script src="modules/cultures-generator.js?v=1.89.10"></script>
<script src="modules/burgs-and-states.js?v=1.89.07"></script>
<script src="modules/routes-generator.js"></script>
<script src="modules/religions-generator.js?v=1.89.15"></script>
<script src="modules/religions-generator.js?v=1.89.28"></script>
<script src="modules/military-generator.js"></script>
<script src="modules/markers-generator.js?v=1.87.13"></script>
<script src="modules/markers-generator.js?v=1.89.23"></script>
<script src="modules/coa-generator.js"></script>
<script src="modules/submap.js?v=1.88.05"></script>
<script src="libs/polylabel.min.js"></script>
<script src="libs/lineclip.min.js"></script>
<script src="libs/alea.min.js"></script>
<script src="modules/fonts.js"></script>
<script src="modules/fonts.js?v=1.89.18"></script>
<script src="modules/ui/layers.js"></script>
<script src="modules/ui/measurers.js?v=1.87.02"></script>
<script src="modules/ui/stylePresets.js?v=1.89.11"></script>
<script src="modules/ui/general.js?v=1.87.03"></script>
<script src="modules/ui/options.js?v=1.88.14"></script>
<script src="main.js?v=1.89.12"></script>
<script src="modules/ui/options.js?v=1.89.19"></script>
<script src="main.js?v=1.89.32"></script>
<script defer src="modules/relief-icons.js"></script>
<script defer src="modules/ui/style.js"></script>
<script defer src="modules/ui/editors.js?v=1.89.12"></script>
<script defer src="modules/ui/tools.js?v=1.89.13"></script>
<script defer src="modules/ui/tools.js?v=1.89.27"></script>
<script defer src="modules/ui/world-configurator.js"></script>
<script defer src="modules/ui/heightmap-editor.js?v=1.89.06"></script>
<script defer src="modules/ui/provinces-editor.js?v=1.89.00"></script>
<script defer src="modules/ui/biomes-editor.js"></script>
<script defer src="modules/ui/namesbase-editor.js?v=1.87.10"></script>
<script defer src="modules/ui/namesbase-editor.js?v=1.89.26"></script>
<script defer src="modules/ui/elevation-profile.js"></script>
<script defer src="modules/ui/temperature-graph.js"></script>
<script defer src="modules/ui/routes-editor.js?v=1.89.04"></script>
@ -7904,14 +7909,14 @@
<script defer src="modules/ui/notes-editor.js?v=1.89.03"></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/burgs-overview.js"></script>
<script defer src="modules/ui/burgs-overview.js?v=1.89.20"></script>
<script defer src="modules/ui/rivers-overview.js"></script>
<script defer src="modules/ui/military-overview.js"></script>
<script defer src="modules/ui/regiments-overview.js"></script>
<script defer src="modules/ui/markers-overview.js"></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.20"></script>
<script defer src="modules/ui/regiment-editor.js"></script>
<script defer src="modules/ui/battle-screen.js"></script>
<script defer src="modules/ui/emblems-editor.js"></script>
<script defer src="modules/ui/emblems-editor.js?v=1.89.21"></script>
<script defer src="modules/ui/markers-editor.js"></script>
<script defer src="modules/ui/3d.js"></script>
<script defer src="modules/ui/submap.js"></script>
@ -7920,10 +7925,10 @@
<script defer src="libs/rgbquant.min.js"></script>
<script defer src="libs/jquery.ui.touch-punch.min.js"></script>
<script defer src="modules/io/save.js"></script>
<script defer src="modules/io/load.js?v=1.88.05"></script>
<script defer src="modules/io/save.js?v=1.89.29"></script>
<script defer src="modules/io/load.js?v=1.89.30"></script>
<script defer src="modules/io/cloud.js"></script>
<script defer src="modules/io/export.js"></script>
<script defer src="modules/io/export.js?v=1.89.17"></script>
<script defer src="modules/io/formats.js"></script>
<!-- Web Components -->

72
libs/indexedDB.js Normal file
View file

@ -0,0 +1,72 @@
let db;
const DATABASE_NAME = "d2";
const STORE_NAME = "s";
const KEY_PATH = "key";
const openDatabase = () => {
return new Promise((resolve, reject) => {
if (db) {
resolve();
} else {
const request = window.indexedDB.open(DATABASE_NAME, 1);
request.onsuccess = event => {
db = event.target.result;
resolve();
};
request.onerror = event => {
console.error("indexedDB request error");
console.log(event);
reject();
};
request.onupgradeneeded = event => {
db = event.target.result;
const objectStore = db.createObjectStore(STORE_NAME, {keyPath: KEY_PATH});
objectStore.transaction.oncomplete = () => {
db = event.target.result;
};
};
}
});
};
const ldb = {
get: key => {
return new Promise((resolve, reject) => {
if (!window.indexedDB) return reject("indexedDB not supported");
openDatabase().then(() => {
const hasStore = Array.from(db.objectStoreNames).includes(STORE_NAME);
if (!hasStore) return reject("no store found");
const transaction = db.transaction(STORE_NAME, "readonly");
const objectStore = transaction.objectStore(STORE_NAME);
const getRequest = objectStore.get(key);
getRequest.onsuccess = event => {
const result = event.target.result?.value || null;
resolve(result);
};
});
});
},
set: (key, value) => {
return new Promise((resolve, reject) => {
if (!window.indexedDB) return reject("indexedDB not supported");
openDatabase().then(() => {
const transaction = db.transaction(STORE_NAME, "readwrite");
const objectStore = transaction.objectStore(STORE_NAME);
const putRequest = objectStore.put({key, value});
putRequest.onsuccess = () => {
resolve();
};
});
});
}
};

4
libs/jquery-ui.css vendored
View file

@ -421,13 +421,13 @@ body .ui-dialog {
/* Component containers
----------------------------------*/
.ui-widget {
font-family: Arial, Helvetica, sans-serif;
font-family: var(--sans-serif);
}
.ui-widget input,
.ui-widget select,
.ui-widget textarea,
.ui-widget button {
font-family: Arial, Helvetica, sans-serif;
font-family: var(--sans-serif);
font-size: 1em;
}
.ui-widget button[class^="icon-"] {

42
main.js
View file

@ -29,7 +29,7 @@ if (PRODUCTION && "serviceWorker" in navigator) {
"beforeinstallprompt",
async event => {
event.preventDefault();
const Installation = await import("./modules/dynamic/installation.js");
const Installation = await import("./modules/dynamic/installation.js?v=1.89.19");
Installation.init(event);
},
{once: true}
@ -221,8 +221,7 @@ oceanLayers
document.addEventListener("DOMContentLoaded", async () => {
if (!location.hostname) {
const wiki = "https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Run-FMG-locally";
alertMessage.innerHTML = /* html */ `Fantasy Map Generator cannot run serverless. Follow the <a href="${wiki}" target="_blank">instructions</a> on how you can
easily run a local web-server`;
alertMessage.innerHTML = /* html */ `Fantasy Map Generator cannot run serverless. Follow the <a href="${wiki}" target="_blank">instructions</a> on how you can easily run a local web-server`;
$("#alert").dialog({
resizable: false,
@ -240,6 +239,7 @@ document.addEventListener("DOMContentLoaded", async () => {
await checkLoadParameters();
}
restoreDefaultEvents(); // apply default viewbox events
initiateAutosave();
});
function hideLoading() {
@ -280,36 +280,20 @@ async function checkLoadParameters() {
return;
}
// open latest map if option is active and map is stored
const loadLastMap = () =>
new Promise((resolve, reject) => {
ldb.get("lastMap", blob => {
// check if there is a map saved to indexedDB
try {
const blob = await ldb.get("lastMap");
if (blob) {
WARN && console.warn("Load last saved map");
try {
WARN && console.warn("Loading last stored map");
uploadMap(blob);
resolve();
return;
}
} catch (error) {
reject(error);
console.error(error);
}
} else {
reject("No map stored");
}
});
});
if (onloadMap.value === "saved") {
try {
await loadLastMap();
} catch (error) {
ERROR && console.error(error);
WARN && console.warn("Cannot load stored map, random map to be generated");
await generateMapOnLoad();
}
} else {
WARN && console.warn("Generate random map");
await generateMapOnLoad();
}
generateMapOnLoad();
}
async function generateMapOnLoad() {
@ -909,7 +893,7 @@ function addLakesInDeepDepressions() {
TIME && console.timeEnd("addLakesInDeepDepressions");
}
// near sea lakes usually get a lot of water inflow, most of them should brake threshold and flow out to sea (see Ancylus Lake)
// near sea lakes usually get a lot of water inflow, most of them should break threshold and flow out to sea (see Ancylus Lake)
function openNearSeaLakes() {
if (byId("templateInput").value === "Atoll") return; // no need for Atolls
@ -924,7 +908,7 @@ function openNearSeaLakes() {
if (features[lake].type !== "lake") continue; // not a lake cell
check_neighbours: for (const c of cells.c[i]) {
if (cells.t[c] !== 1 || cells.h[c] > LIMIT) continue; // water cannot brake this
if (cells.t[c] !== 1 || cells.h[c] > LIMIT) continue; // water cannot break this
for (const n of cells.c[c]) {
const ocean = cells.f[n];

View file

@ -129,7 +129,7 @@ window.Cultures = (function () {
if (def.every(d => d.odd === 1)) return def.splice(0, culturesNumber);
}
for (let culture, rnd, i = 0; cultures.length < culturesNumber && def.length > 0;) {
for (let culture, rnd, i = 0; cultures.length < culturesNumber && def.length > 0; ) {
do {
rnd = rand(def.length - 1);
culture = def[rnd];
@ -511,56 +511,61 @@ window.Cultures = (function () {
TIME && console.time("expandCultures");
const {cells, cultures} = pack;
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const queue = new PriorityQueue({comparator: (a, b) => a.priority - b.priority});
const cost = [];
const neutralRate = byId("neutralRate")?.valueAsNumber || 1;
const neutral = cells.i.length * 0.6 * neutralRate; // limit cost for culture growth
const maxExpansionCost = cells.i.length * 0.6 * neutralRate; // limit cost for culture growth
// remove culture from all cells except of locked
const hasLocked = cultures.some(c => !c.removed && c.lock);
if (hasLocked) {
for (const cellId of cells.i) {
const culture = cultures[cells.culture[cellId]];
if (culture.lock) continue;
cells.culture[cellId] = 0;
}
} else {
cells.culture = new Uint16Array(cells.i.length);
}
for (const culture of cultures) {
if (!culture.i || culture.removed) continue;
queue.queue({e: culture.center, p: 0, c: culture.i});
if (!culture.i || culture.removed || culture.lock) continue;
queue.queue({cellId: culture.center, cultureId: culture.i, priority: 0});
}
while (queue.length) {
const {e, p, c} = queue.dequeue();
const {type} = pack.cultures[c];
const {cellId, priority, cultureId} = queue.dequeue();
const {type, expansionism} = cultures[cultureId];
cells.c[e].forEach(e => {
const culture = cells.culture[e];
if (culture?.lock) return; // do not overwrite cell of locked culture
cells.c[cellId].forEach(neibCellId => {
if (hasLocked) {
const neibCultureId = cells.culture[neibCellId];
if (neibCultureId && cultures[neibCultureId].lock) return; // do not overwrite cell of locked culture
}
const biome = cells.biome[e];
const biomeCost = getBiomeCost(c, biome, type);
const biomeChangeCost = biome === cells.biome[e] ? 0 : 20; // penalty on biome change
const heightCost = getHeightCost(e, cells.h[e], type);
const riverCost = getRiverCost(cells.r[e], e, type);
const typeCost = getTypeCost(cells.t[e], type);
const totalCost =
p + (biomeCost + biomeChangeCost + heightCost + riverCost + typeCost) / pack.cultures[c].expansionism;
const biome = cells.biome[neibCellId];
const biomeCost = getBiomeCost(cultureId, biome, type);
const biomeChangeCost = biome === cells.biome[neibCellId] ? 0 : 20; // penalty on biome change
const heightCost = getHeightCost(neibCellId, cells.h[neibCellId], type);
const riverCost = getRiverCost(cells.r[neibCellId], neibCellId, type);
const typeCost = getTypeCost(cells.t[neibCellId], type);
if (totalCost > neutral) return;
const cellCost = (biomeCost + biomeChangeCost + heightCost + riverCost + typeCost) / expansionism;
const totalCost = priority + cellCost;
if (!cost[e] || totalCost < cost[e]) {
if (cells.s[e] > 0) cells.culture[e] = c; // assign culture to populated cell
cost[e] = totalCost;
queue.queue({e, p: totalCost, c});
if (totalCost > maxExpansionCost) return;
if (!cost[neibCellId] || totalCost < cost[neibCellId]) {
if (cells.pop[neibCellId] > 0) cells.culture[neibCellId] = cultureId; // assign culture to populated cell
cost[neibCellId] = totalCost;
queue.queue({cellId: neibCellId, cultureId, priority: totalCost});
}
});
}
TIME && console.timeEnd("expandCultures");
};
function getBiomeCost(c, biome, type) {
if (cells.biome[pack.cultures[c].center] === biome) return 10; // tiny penalty for native biome
if (cells.biome[cultures[c].center] === biome) return 10; // tiny penalty for native biome
if (type === "Hunting") return biomesData.cost[biome] * 5; // non-native biome penalty for hunters
if (type === "Nomadic" && biome > 4 && biome < 10) return biomesData.cost[biome] * 10; // forest biome penalty for nomads
return biomesData.cost[biome] * 2; // general non-native biome penalty
@ -581,10 +586,10 @@ window.Cultures = (function () {
return 0;
}
function getRiverCost(r, i, type) {
if (type === "River") return r ? 0 : 100; // penalty for river cultures
if (!r) return 0; // no penalty for others if there is no river
return minmax(cells.fl[i] / 10, 20, 100); // river penalty from 20 to 100 based on flux
function getRiverCost(riverId, cellId, type) {
if (type === "River") return riverId ? 0 : 100; // penalty for river cultures
if (!riverId) return 0; // no penalty for others if there is no river
return minmax(cells.fl[cellId] / 10, 20, 100); // river penalty from 20 to 100 based on flux
}
function getTypeCost(t, type) {
@ -594,6 +599,9 @@ window.Cultures = (function () {
return 0;
}
TIME && console.timeEnd("expandCultures");
};
const getRandomShield = function () {
const type = rw(COA.shields.types);
return rw(COA.shields[type]);

View file

@ -670,13 +670,13 @@ async function showHierarchy() {
});
}
function recalculateCultures(must) {
if (!must && !culturesAutoChange.checked) return;
function recalculateCultures(force) {
if (force || culturesAutoChange.checked) {
Cultures.expand();
drawCultures();
pack.burgs.forEach(b => (b.culture = pack.cells.culture[b.cell]));
refreshCulturesEditor();
}
}
function enterCultureManualAssignent() {

View file

@ -28,7 +28,7 @@ function createButton() {
button.innerHTML = "Install";
button.onclick = openDialog;
button.onmouseenter = () => tip("Install the Application");
document.querySelector("body").appendChild(button);
document.getElementById("optionsContainer").appendChild(button);
return button;
}

View file

@ -426,19 +426,18 @@ function renderChart({id, entity, plotBy, groupBy, sorting, type}) {
})
.flat();
const sortedData = sortData(chartData, sorting);
const colors = getColors();
const {offset, formatX = formatTicks} = plotTypeMap[type];
const $chart = createStackedBarChart(chartData, {sorting, colors, tooltip, offset, formatX});
insertChart(id, $chart, title);
const $chart = createStackedBarChart(sortedData, {colors, tooltip, offset, formatX});
insertChart(id, sortedData, $chart, title);
byId("chartsOverview__charts").lastChild.scrollIntoView();
}
// based on observablehq.com/@d3/stacked-horizontal-bar-chart
function createStackedBarChart(data, {sorting, colors, tooltip, offset, formatX}) {
const sortedData = sortData(data, sorting);
function createStackedBarChart(sortedData, {colors, tooltip, offset, formatX}) {
const X = sortedData.map(d => d.value);
const Y = sortedData.map(d => d.name);
const Z = sortedData.map(d => d.group);
@ -568,7 +567,7 @@ function createStackedBarChart(data, {sorting, colors, tooltip, offset, formatX}
return svg.node();
}
function insertChart(id, $chart, title) {
function insertChart(id, sortedData, $chart, title) {
const $chartContainer = byId("chartsOverview__charts");
const $figure = document.createElement("figure");
@ -580,7 +579,8 @@ function insertChart(id, $chart, title) {
<strong>Figure ${figureNo}</strong>. ${title}
</div>
<div>
<button data-tip="Download the chart in svg format (can open in browser or Inkscape)" class="icon-download"></button>
<button data-tip="Download chart data as a text file (.csv)" class="icon-download"></button>
<button data-tip="Download the chart in svg format (can open in browser or Inkscape)" class="icon-chart-bar"></button>
<button data-tip="Remove the chart" class="icon-trash"></button>
</div>
`;
@ -589,7 +589,14 @@ function insertChart(id, $chart, title) {
$figure.appendChild($caption);
$chartContainer.appendChild($figure);
const downloadChart = () => {
const downloadChartData = () => {
const name = `${getFileName(title)}.csv`;
const headers = "Name,Group,Value\n";
const values = sortedData.map(({name, group, value}) => `${name},${group},${value}`).join("\n");
downloadFile(headers + values, name);
};
const downloadChartSvg = () => {
const name = `${getFileName(title)}.svg`;
downloadFile($chart.outerHTML, name);
};
@ -600,7 +607,8 @@ function insertChart(id, $chart, title) {
updateDialogPosition();
};
$figure.querySelector("button.icon-download").on("click", downloadChart);
$figure.querySelector("button.icon-download").on("click", downloadChartData);
$figure.querySelector("button.icon-chart-bar").on("click", downloadChartSvg);
$figure.querySelector("button.icon-trash").on("click", removeChart);
}

View file

@ -478,4 +478,44 @@ Rei
Fondue
Paavi1
Wil Sisney
David Patterson`;
David Patterson
L
Justin Scheffers
Commieboo
Garrison Wood
Emsiron
Frosty
John Joseph Adams
The_Lone_Wanderer
Andrew Stein
Groonfish
soup
Bruno Haack Vilar
Ian Burke
Tentacle Shogun
Andrew Chandler
Fritz Wulfram
Doom Chupacabra
Zakharov
Dylan Fox
Alfred Piccioni
Avery Vreeland
Kennedy
Zack Wolf
Matjam
Jeff Johnston
Hunter Hawthorne
Sunsette
Travis Love
Dakian Delomast
Kyle
Davis Walker
Naomi
Clément D
Jake Herr
ReV0LT
Jack Dawson
Queso y Libertad
RadioJay21H
NEO
Crecs`;

View file

@ -2,15 +2,19 @@
const fonts = [
{family: "Arial"},
{family: "Times New Roman"},
{family: "Georgia"},
{family: "Garamond"},
{family: "Lucida Sans Unicode"},
{family: "Courier New"},
{family: "Verdana"},
{family: "Impact"},
{family: "Brush Script MT"},
{family: "Century Gothic"},
{family: "Comic Sans MS"},
{family: "Copperplate"},
{family: "Courier New"},
{family: "Garamond"},
{family: "Georgia"},
{family: "Herculanum"},
{family: "Impact"},
{family: "Papyrus"},
{family: "Party LET"},
{family: "Times New Roman"},
{family: "Verdana"},
{
family: "Almendra SC",
src: "url(https://fonts.gstatic.com/s/almendrasc/v13/Iure6Yx284eebowr7hbyTaZOrLQ.woff2)",
@ -38,12 +42,14 @@ const fonts = [
{
family: "Architects Daughter",
src: "url(https://fonts.gstatic.com/s/architectsdaughter/v8/RXTgOOQ9AAtaVOHxx0IUBM3t7GjCYufj5TXV5VnA2p8.woff2)",
unicodeRange: "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
},
{
family: "Bitter",
src: "url(https://fonts.gstatic.com/s/bitter/v12/zfs6I-5mjWQ3nxqccMoL2A.woff2)",
unicodeRange: "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
},
{
family: "Caesar Dressing",
@ -54,12 +60,14 @@ const fonts = [
{
family: "Cinzel",
src: "url(https://fonts.gstatic.com/s/cinzel/v7/zOdksD_UUTk1LJF9z4tURA.woff2)",
unicodeRange: "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
},
{
family: "Dancing Script",
src: "url(https://fonts.gstatic.com/s/dancingscript/v9/KGBfwabt0ZRLA5W1ywjowUHdOuSHeh0r6jGTOGdAKHA.woff2)",
unicodeRange: "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
},
{
family: "Faster One",
@ -82,12 +90,14 @@ const fonts = [
{
family: "Gloria Hallelujah",
src: "url(https://fonts.gstatic.com/s/gloriahallelujah/v9/CA1k7SlXcY5kvI81M_R28cNDay8z-hHR7F16xrcXsJw.woff2)",
unicodeRange: "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
},
{
family: "Great Vibes",
src: "url(https://fonts.gstatic.com/s/greatvibes/v5/6q1c0ofG6NKsEhAc2eh-3Y4P5ICox8Kq3LLUNMylGO4.woff2)",
unicodeRange: "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
},
{
family: "Henny Penny",
@ -98,7 +108,8 @@ const fonts = [
{
family: "IM Fell English",
src: "url(https://fonts.gstatic.com/s/imfellenglish/v7/xwIisCqGFi8pff-oa9uSVAkYLEKE0CJQa8tfZYc_plY.woff2)",
unicodeRange: "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
},
{
family: "Kelly Slab",
@ -121,7 +132,8 @@ const fonts = [
{
family: "Kaushan Script",
src: "url(https://fonts.gstatic.com/s/kaushanscript/v6/qx1LSqts-NtiKcLw4N03IEd0sm1ffa_JvZxsF_BEwQk.woff2)",
unicodeRange: "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
},
{
family: "Macondo",
@ -150,7 +162,8 @@ const fonts = [
{
family: "Montez",
src: "url(https://fonts.gstatic.com/s/montez/v8/aq8el3-0osHIcFK6bXAPkw.woff2)",
unicodeRange: "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
},
{
family: "Nova Script",
@ -161,7 +174,8 @@ const fonts = [
{
family: "Orbitron",
src: "url(https://fonts.gstatic.com/s/orbitron/v9/HmnHiRzvcnQr8CjBje6GQvesZW2xOQ-xsNqO47m55DA.woff2)",
unicodeRange: "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
},
{
family: "Oregano",
@ -184,12 +198,14 @@ const fonts = [
{
family: "Satisfy",
src: "url(https://fonts.gstatic.com/s/satisfy/v8/2OzALGYfHwQjkPYWELy-cw.woff2)",
unicodeRange: "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
},
{
family: "Shadows Into Light",
src: "url(https://fonts.gstatic.com/s/shadowsintolight/v7/clhLqOv7MXn459PTh0gXYFK2TSYBz0eNcHnp4YqE4Ts.woff2)",
unicodeRange: "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
},
{
family: "Tapestry",
@ -218,7 +234,8 @@ const fonts = [
{
family: "Yellowtail",
src: "url(https://fonts.gstatic.com/s/yellowtail/v8/GcIHC9QEwVkrA19LJU1qlPk_vArhqVIZ0nv9q090hN8.woff2)",
unicodeRange: "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
}
];

View file

@ -10,7 +10,12 @@ async function saveSVG() {
link.href = url;
link.click();
tip(`${link.download} is saved. Open "Downloads" screen (crtl + J) to check. You can set image scale in options`, true, "success", 5000);
tip(
`${link.download} is saved. Open "Downloads" screen (crtl + J) to check. You can set image scale in options`,
true,
"success",
5000
);
TIME && console.timeEnd("saveSVG");
}
@ -36,7 +41,12 @@ async function savePNG() {
window.setTimeout(function () {
canvas.remove();
window.URL.revokeObjectURL(link.href);
tip(`${link.download} is saved. Open "Downloads" screen (crtl + J) to check. You can set image scale in options`, true, "success", 5000);
tip(
`${link.download} is saved. Open "Downloads" screen (crtl + J) to check. You can set image scale in options`,
true,
"success",
5000
);
}, 1000);
});
};
@ -140,7 +150,15 @@ async function saveTiles() {
// parse map svg to object url
async function getMapURL(type, options = {}) {
const {debug = false, globe = false, noLabels = false, noWater = false, noScaleBar = false, noIce = false, fullMap = false} = options;
const {
debug = false,
globe = false,
noLabels = false,
noWater = false,
noScaleBar = false,
noIce = false,
fullMap = false
} = options;
if (fullMap) drawScaleBar(1);
@ -222,6 +240,7 @@ async function getMapURL(type, options = {}) {
if (location.hostname && cloneEl.getElementById("oceanicPattern")) {
const el = cloneEl.getElementById("oceanicPattern");
const url = el.getAttribute("href");
if (url) {
await new Promise(resolve => {
getBase64(url, base64 => {
el.setAttribute("href", base64);
@ -229,6 +248,7 @@ async function getMapURL(type, options = {}) {
});
});
}
}
// add relief icons
if (cloneEl.getElementById("terrain")) {
@ -315,7 +335,8 @@ async function getMapURL(type, options = {}) {
clone.remove();
const serialized = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>` + new XMLSerializer().serializeToString(cloneEl);
const serialized =
`<?xml version="1.0" encoding="UTF-8" standalone="no"?>` + new XMLSerializer().serializeToString(cloneEl);
const blob = new Blob([serialized], {type: "image/svg+xml;charset=utf-8"});
const url = window.URL.createObjectURL(blob);
window.setTimeout(() => window.URL.revokeObjectURL(url), 5000);
@ -461,7 +482,7 @@ function saveGeoJSON_Markers() {
const coordinates = getCoordinates(x, y, 4);
const id = `marker${i}`;
const note = notes.find(note => note.id === id);
const properties = {id, type, icon, ...note, size, fill, stroke};
const properties = {id, type, icon, x, y, ...note, size, fill, stroke};
return {type: "Feature", geometry: {type: "Point", coordinates}, properties};
});

View file

@ -1,15 +1,13 @@
"use strict";
// Functions to load and parse .map files
function quickLoad() {
ldb.get("lastMap", blob => {
if (blob) {
loadMapPrompt(blob);
} else {
tip("No map stored. Save map to storage first", true, "error", 2000);
async function quickLoad() {
const blob = await ldb.get("lastMap");
if (blob) loadMapPrompt(blob);
else {
tip("No map stored. Save map to browser storage first", true, "error", 2000);
ERROR && console.error("No map stored");
}
});
}
async function loadFromDropbox() {

View file

@ -3,8 +3,6 @@
// prepare map data for saving
function getMapData() {
TIME && console.time("createMapData");
const date = new Date();
const dateString = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate();
const license = "File can be loaded in azgaar.github.io/Fantasy-Map-Generator";
@ -116,13 +114,13 @@ function getMapData() {
fonts,
markers
].join("\r\n");
TIME && console.timeEnd("createMapData");
return mapData;
}
// Download .map file
function dowloadMap() {
if (customization) return tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error");
if (customization)
return tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error");
closeDialogs("#alert");
const mapData = getMapData();
@ -137,7 +135,8 @@ function dowloadMap() {
}
async function saveToDropbox() {
if (customization) return tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error");
if (customization)
return tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error");
closeDialogs("#alert");
const mapData = getMapData();
const filename = getFileName() + ".map";
@ -150,12 +149,36 @@ async function saveToDropbox() {
}
}
function quickSave() {
if (customization) return tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error");
async function initiateAutosave() {
const MINUTE = 60000; // munite in milliseconds
let lastSavedAt = Date.now();
async function autosave() {
const timeoutMinutes = byId("autosaveIntervalOutput").valueAsNumber;
if (!timeoutMinutes) return;
const diffInMinutes = (Date.now() - lastSavedAt) / MINUTE;
if (diffInMinutes < timeoutMinutes) return;
if (customization) return tip("Autosave: map cannot be saved in edit mode", false, "warning", 2000);
tip("Autosave: saving map...", false, "warning", 3000);
const mapData = getMapData();
const blob = new Blob([mapData], {type: "text/plain"});
await ldb.set("lastMap", blob);
console.log("Autosaved at", new Date().toLocaleTimeString());
lastSavedAt = Date.now();
}
setInterval(autosave, MINUTE / 2);
}
async function quickSave() {
if (customization)
return tip("Map cannot be saved when edit mode is active, please exit the mode first", false, "error");
const mapData = getMapData();
const blob = new Blob([mapData], {type: "text/plain"});
if (blob) ldb.set("lastMap", blob); // auto-save map
await ldb.set("lastMap", blob); // auto-save map
tip("Map is saved to browser memory. Please also save as .map file to secure progress", true, "success", 2000);
}

View file

@ -24,6 +24,7 @@ window.Markers = (function () {
return [
{type: "volcanoes", icon: "🌋", dx: 52, px: 13, min: 10, each: 500, multiplier: 1, list: listVolcanoes, add: addVolcano},
{type: "hot-springs", icon: "♨️", dy: 52, min: 30, each: 1200, multiplier: 1, list: listHotSprings, add: addHotSpring},
{type: "water-sources", icon: "💧", min: 1, each: 1000, multiplier: 1, list: listWaterSources, add: addWaterSource},
{type: "mines", icon: "⛏️", dx: 48, px: 13, min: 1, each: 15, multiplier: 1, list: listMines, add: addMine},
{type: "bridges", icon: "🌉", px: 14, min: 1, each: 5, multiplier: 1, list: listBridges, add: addBridge},
{type: "inns", icon: "🍻", px: 14, min: 1, each: 100, multiplier: 1, list: listInns, add: addInn},
@ -42,15 +43,19 @@ window.Markers = (function () {
{type: "pirates", icon: "🏴‍☠️", dx: 51, min: 40, each: 300, multiplier: 1, list: listPirates, add: addPirates},
{type: "statues", icon: "🗿", min: 80, each: 1200, multiplier: 1, list: listStatues, add: addStatue},
{type: "ruins", icon: "🏺", min: 80, each: 1200, multiplier: 1, list: listRuins, add: addRuins},
{type: "circuses", icon: "🎪", min: 80, each: 1000, multiplier: 1, list: listCircuses, add: addCircuses},
{type: "jousts", icon: "🤺", dx: 48, min: 5, each: 500, multiplier: 1, list: listJousts, add: addJousts},
{type: "canoes", icon: "🛶", min: 1000, each: 2000, multiplier: 1, list: listCanoes, add: addCanoes},
{type: "migration", icon: "🐗", min: 20, each: 1000, multiplier: 1, list: listMigrations, add: addMigrations},
{type: "dances", icon: "💃🏽", min: 5, each: 60, multiplier: 1, list: listDances, add: addDances},
{type: "libraries", icon: "📚", min: 10, each: 1200, multiplier: 1, list: listLibraries, add: addLibrary},
{type: "circuses", icon: "🎪", min: 80, each: 1000, multiplier: 1, list: listCircuses, add: addCircuse},
{type: "jousts", icon: "🤺", dx: 48, min: 5, each: 500, multiplier: 1, list: listJousts, add: addJoust},
{type: "fairs", icon: "🎠", min: 50, each: 1000, multiplier: 1, list: listFairs, add: addFair},
{type: "canoes", icon: "🛶", min: 500, each: 2000, multiplier: 1, list: listCanoes, add: addCanoe},
{type: "migration", icon: "🐗", min: 20, each: 1000, multiplier: 1, list: listMigrations, add: addMigration},
{type: "dances", icon: "💃🏽", min: 50, each: 1000, multiplier: 1, list: listDances, add: addDances},
{type: "mirage", icon: "💦", min: 10, each: 400, multiplier: 1, list: listMirage, add: addMirage},
{type: "caves", icon:"🦇", min: 60, each: 1000, multiplier: 1, list: listCaves, add: addCaves},
{type: "caves", icon:"🦇", min: 60, each: 1000, multiplier: 1, list: listCaves, add: addCave},
{type: "portals", icon: "🌀", px: 14, min: 16, each: 8, multiplier: +isFantasy, list: listPortals, add: addPortal},
{type: "rifts", icon: "🎆", min: 1, each: 3000, multiplier: +isFantasy, list: listRifts, add: addRifts}
{type: "rifts", icon: "🎆", min: 5, each: 3000, multiplier: +isFantasy, list: listRifts, add: addRift},
{type: "disturbed-burials", icon: "💀", min: 20, each: 3000, multiplier: +isFantasy, list: listDisturbedBurial, add: addDisturbedBurial},
{type: "necropolises", icon: "🪦", min: 20, each: 1000, multiplier: 1, list: listNecropolis, add: addNecropolis},
];
}
@ -172,7 +177,7 @@ window.Markers = (function () {
}
function listHotSprings({cells}) {
return cells.i.filter(i => !occupied[i] && cells.h[i] > 50);
return cells.i.filter(i => !occupied[i] && cells.h[i] > 50 && cells.culture[i]);
}
function addHotSpring(id, cell) {
@ -180,12 +185,37 @@ window.Markers = (function () {
const proper = Names.getCulture(cells.culture[cell]);
const temp = convertTemperature(gauss(35, 15, 20, 100));
const status = P(0.6) ? "geothermal" : P(0.4) ? "springwater" : "natural";
notes.push({
id,
name: proper + " Hot Springs",
legend: `A ${status} hot springs area. Average temperature: ${temp}.`
const name = P(0.3) ? "Hot Springs of " + proper : P(0.7) ? proper + " Hot Springs" : proper;
const legend = `A geothermal springs with naturally heated water that provide relaxation and medicinal benefits. Average temperature is ${temp}.`;
notes.push({id, name, legend});
}
function listWaterSources({cells}) {
return cells.i.filter(i => !occupied[i] && cells.h[i] > 30 && cells.r[i]);
}
function addWaterSource(id, cell) {
const {cells} = pack;
const type = rw({
"Healing Spring": 5,
"Purifying Well": 2,
"Enchanted Reservoir": 1,
"Creek of Luck": 1,
"Fountain of Youth": 1,
"Wisdom Spring": 1,
"Spring of Life": 1,
"Spring of Youth": 1,
"Healing Stream": 1
});
const proper = Names.getCulture(cells.culture[cell]);
const name = `${proper} ${type}`;
const legend =
"This legendary water source is whispered about in ancient tales and believed to possess mystical properties. The spring emanates crystal-clear water, shimmering with an otherworldly iridescence that sparkles even in the dimmest light.";
notes.push({id, name, legend});
}
function listMines({cells}) {
@ -728,7 +758,7 @@ window.Markers = (function () {
}
function addSacredForest(id, cell) {
const {cells, cultures, religions} = pack;
const {cells, religions} = pack;
const culture = cells.culture[cell];
const religion = cells.religion[cell];
@ -743,7 +773,7 @@ window.Markers = (function () {
}
function addSacredPinery(id, cell) {
const {cells, cultures, religions} = pack;
const {cells, religions} = pack;
const culture = cells.culture[cell];
const religion = cells.religion[cell];
@ -766,7 +796,7 @@ window.Markers = (function () {
}
function addSacredPalmGrove(id, cell) {
const {cells, cultures, religions} = pack;
const {cells, religions} = pack;
const culture = cells.culture[cell];
const religion = cells.religion[cell];
@ -842,8 +872,8 @@ window.Markers = (function () {
}
function addPirates(id, cell) {
const name = `Pirates`;
const legend = `Pirate ships have been spotted in these waters.`;
const name = "Pirates";
const legend = "Pirate ships have been spotted in these waters.";
notes.push({id, name, legend});
}
@ -917,11 +947,25 @@ window.Markers = (function () {
notes.push({id, name, legend});
}
function listLibraries({cells}) {
return cells.i.filter(i => !occupied[i] && cells.culture[i] && cells.burg[i] && cells.pop[i] > 10);
}
function addLibrary(id, cell) {
const {cells} = pack;
const type = rw({Library: 3, Archive: 1, Collection: 1});
const name = `${Names.getCulture(cells.culture[cell])} ${type}`;
const legend = "A vast collection of knowledge, including many rare and ancient tomes.";
notes.push({id, name, legend});
}
function listCircuses({cells}) {
return cells.i.filter(i => !occupied[i] && cells.culture[i] && cells.h[i] >= 20 && pack.cells.road[i]);
}
function addCircuses(id, cell) {
function addCircuse(id, cell) {
const adjectives = [
"Fantastical",
"Wonderous",
@ -943,7 +987,7 @@ window.Markers = (function () {
return cells.i.filter(i => !occupied[i] && cells.burg[i] && burgs[cells.burg[i]].population > 20);
}
function addJousts(id, cell) {
function addJoust(id, cell) {
const {cells, burgs} = pack;
const types = ["Joust", "Competition", "Melee", "Tournament", "Contest"];
const virtues = ["cunning", "might", "speed", "the greats", "acumen", "brutality"];
@ -958,11 +1002,29 @@ window.Markers = (function () {
notes.push({id, name, legend});
}
function listFairs({cells, burgs}) {
return cells.i.filter(
i => !occupied[i] && cells.burg[i] && burgs[cells.burg[i]].population < 20 && burgs[cells.burg[i]].population < 5
);
}
function addFair(id, cell) {
const {cells, burgs} = pack;
if (!cells.burg[cell]) return;
const burgName = burgs[cells.burg[cell]].name;
const type = "Fair";
const name = `${burgName} ${type}`;
const legend = `A fair is being held in ${burgName}, with all manner of local and foreign goods and services on offer.`;
notes.push({id, name, legend});
}
function listCanoes({cells}) {
return cells.i.filter(i => !occupied[i] && cells.r[i]);
}
function addCanoes(id, cell) {
function addCanoe(id, cell) {
const river = pack.rivers.find(r => r.i === pack.cells.r[cell]);
const name = `Minor Jetty`;
@ -975,7 +1037,7 @@ window.Markers = (function () {
return cells.i.filter(i => !occupied[i] && cells.h[i] >= 20 && cells.pop[i] <= 2);
}
function addMigrations(id, cell) {
function addMigration(id, cell) {
const animals = [
"Antelopes",
"Apes",
@ -1053,7 +1115,10 @@ window.Markers = (function () {
"exhibition",
"carnival",
"festival",
"jubilee"
"jubilee",
"celebration",
"gathering",
"fete"
];
const people = [
"great and the good",
@ -1089,7 +1154,7 @@ window.Markers = (function () {
return cells.i.filter(i => !occupied[i] && cells.h[i] >= 50 && cells.pop[i]);
}
function addCaves(id, cell) {
function addCave(id, cell) {
const {cells} = pack;
const formations = {
@ -1144,7 +1209,7 @@ window.Markers = (function () {
return cells.i.filter(i => !occupied[i] && pack.cells.pop[i] <= 3 && biomesData.habitability[pack.cells.biome[i]]);
}
function addRifts(id, cell) {
function addRift(id, cell) {
const types = ["Demonic", "Interdimensional", "Abyssal", "Cosmic", "Cataclysmic", "Subterranean", "Ancient"];
const descriptions = [
@ -1161,5 +1226,49 @@ window.Markers = (function () {
notes.push({id, name, legend});
}
function listDisturbedBurial({cells}) {
return cells.i.filter(i => !occupied[i] && cells.h[i] >= 20 && cells.pop[i] > 2);
}
function addDisturbedBurial(id, cell) {
const name = "Disturbed Burial";
const legend = "A burial site has been disturbed in this area, causing the dead to rise and attack the living.";
notes.push({id, name, legend});
}
function listNecropolis({cells}) {
return cells.i.filter(i => !occupied[i] && cells.h[i] >= 20 && cells.pop[i] < 2);
}
function addNecropolis(id, cell) {
const {cells} = pack;
const toponym = Names.getCulture(cells.culture[cell]);
const type = rw({
Necropolis: 5,
Crypt: 2,
Tomb: 2,
Graveyard: 1,
Cemetery: 2,
Mausoleum: 1,
Sepulchre: 1
});
const name = `${toponym} ${type}`;
const legend = ra([
"A foreboding necropolis shrouded in perpetual darkness, where eerie whispers echo through the winding corridors and spectral guardians stand watch over the tombs of long-forgotten souls",
"A towering necropolis adorned with macabre sculptures and guarded by formidable undead sentinels. Its ancient halls house the remains of fallen heroes, entombed alongside their cherished relics",
"This ethereal necropolis seems suspended between the realms of the living and the dead. Wisps of mist dance around the tombstones, while haunting melodies linger in the air, commemorating the departed",
"Rising from the desolate landscape, this sinister necropolis is a testament to necromantic power. Its skeletal spires cast ominous shadows, concealing forbidden knowledge and arcane secrets",
"An eerie necropolis where nature intertwines with death. Overgrown tombstones are entwined by thorny vines, and mournful spirits wander among the fading petals of once-vibrant flowers",
"A labyrinthine necropolis where each step echoes with haunting murmurs. The walls are adorned with ancient runes, and restless spirits guide or hinder those who dare to delve into its depths",
"This cursed necropolis is veiled in perpetual twilight, perpetuating a sense of impending doom. Dark enchantments shroud the tombs, and the moans of anguished souls resound through its crumbling halls",
"A sprawling necropolis built within a labyrinthine network of catacombs. Its halls are lined with countless alcoves, each housing the remains of the departed, while the distant sound of rattling bones fills the air",
"A desolate necropolis where an eerie stillness reigns. Time seems frozen amidst the decaying mausoleums, and the silence is broken only by the whispers of the wind and the rustle of tattered banners",
"A foreboding necropolis perched atop a jagged cliff, overlooking a desolate wasteland. Its towering walls harbor restless spirits, and the imposing gates bear the marks of countless battles and ancient curses"
]);
notes.push({id, name, legend});
}
return {add, generate, regenerate, getConfig, setConfig, deleteMarker};
})();

View file

@ -32,12 +32,16 @@ window.Religions = (function () {
being: [
"Ancestor",
"Ancient",
"Avatar",
"Brother",
"Champion",
"Chief",
"Council",
"Creator",
"Deity",
"Divine One",
"Elder",
"Enlightened Being",
"Father",
"Forebear",
"Forefather",
@ -45,17 +49,25 @@ window.Religions = (function () {
"God",
"Goddess",
"Guardian",
"Guide",
"Hierach",
"Lady",
"Lord",
"Maker",
"Master",
"Mother",
"Numen",
"Oracle",
"Overlord",
"Protector",
"Reaper",
"Ruler",
"Sage",
"Seer",
"Sister",
"Spirit",
"Supreme Being",
"Transcendent",
"Virgin"
],
animal: [
@ -71,36 +83,47 @@ window.Religions = (function () {
"Camel",
"Cat",
"Centaur",
"Cerberus",
"Chimera",
"Cobra",
"Cockatrice",
"Crane",
"Crocodile",
"Crow",
"Cyclope",
"Deer",
"Dog",
"Direwolf",
"Drake",
"Dragon",
"Eagle",
"Elephant",
"Elk",
"Falcon",
"Fox",
"Goat",
"Goose",
"Gorgon",
"Gryphon",
"Hare",
"Hawk",
"Heron",
"Hippogriff",
"Horse",
"Hound",
"Hyena",
"Ibis",
"Jackal",
"Jaguar",
"Kitsune",
"Kraken",
"Lark",
"Leopard",
"Lion",
"Manticore",
"Mantis",
"Marten",
"Minotaur",
"Moose",
"Mule",
"Narwhal",
@ -109,8 +132,10 @@ window.Religions = (function () {
"Panther",
"Pegasus",
"Phoenix",
"Python",
"Rat",
"Raven",
"Roc",
"Rook",
"Scorpion",
"Serpent",
@ -129,7 +154,8 @@ window.Religions = (function () {
"Wolf",
"Wolverine",
"Worm",
"Wyvern"
"Wyvern",
"Yeti"
],
adjective: [
"Aggressive",
@ -146,6 +172,7 @@ window.Religions = (function () {
"Brutal",
"Burning",
"Calm",
"Celestial",
"Cheerful",
"Crazy",
"Cruel",
@ -157,6 +184,10 @@ window.Religions = (function () {
"Divine",
"Dying",
"Eternal",
"Ethernal",
"Empyreal",
"Enigmatic",
"Enlightened",
"Evil",
"Explicit",
"Fair",
@ -177,7 +208,9 @@ window.Religions = (function () {
"Honest",
"Huge",
"Hungry",
"Illustrious",
"Immutable",
"Ineffable",
"Infallible",
"Inherent",
"Last",
@ -190,31 +223,46 @@ window.Religions = (function () {
"Main",
"Major",
"Marine",
"Mythical",
"Mystical",
"Naval",
"New",
"Noble",
"Old",
"Otherworldly",
"Patient",
"Peaceful",
"Pregnant",
"Prime",
"Proud",
"Pure",
"Radiant",
"Resplendent",
"Sacred",
"Sacrosanct",
"Sad",
"Scary",
"Secret",
"Selected",
"Serene",
"Severe",
"Silent",
"Sleeping",
"Slumbering",
"Sovereign",
"Strong",
"Sunny",
"Superior",
"Supernatural",
"Sustainable",
"Transcendent",
"Transcendental",
"Troubled",
"Unearthly",
"Unfathomable",
"Unhappy",
"Unknown",
"Unseen",
"Waking",
"Wild",
"Wise",
@ -282,26 +330,61 @@ window.Religions = (function () {
"Black",
"Blue",
"Bright",
"Bronze",
"Brown",
"Coral",
"Crimson",
"Dark",
"Emerald",
"Golden",
"Green",
"Grey",
"Indigo",
"Lavender",
"Light",
"Magenta",
"Maroon",
"Orange",
"Pink",
"Plum",
"Purple",
"Red",
"Ruby",
"Sapphire",
"Teal",
"Turquoise",
"White",
"Yellow"
]
};
const forms = {
Folk: {Shamanism: 2, Animism: 2, "Ancestor worship": 1, Polytheism: 2},
Organized: {Polytheism: 5, Dualism: 1, Monotheism: 4, "Non-theism": 1},
Cult: {Cult: 1, "Dark Cult": 1},
Heresy: {Heresy: 1}
Folk: {
Shamanism: 4,
Animism: 4,
Polytheism: 4,
Totemism: 2,
Druidism: 1,
"Ancestor Worship": 1,
"Nature Worship": 1
},
Organized: {
Polytheism: 14,
Monotheism: 12,
Dualism: 6,
Pantheism: 6,
"Non-theism": 4,
Henotheism: 1,
Panentheism: 1
},
Cult: {
Cult: 2,
"Dark Cult": 2,
Sect: 1
},
Heresy: {
Heresy: 1
}
};
const namingMethods = {

View file

@ -93,7 +93,9 @@ function overviewBurgs() {
data-type="${type}"
>
<span data-tip="Click to zoom into view" class="icon-dot-circled pointer"></span>
<input data-tip="Burg name. Click and type to change" class="burgName" value="${b.name}" autocorrect="off" spellcheck="false" />
<input data-tip="Burg name. Click and type to change" class="burgName" value="${
b.name
}" autocorrect="off" spellcheck="false" />
<input data-tip="Burg province" class="burgState" value="${province}" disabled />
<input data-tip="Burg state" class="burgState" value="${state}" disabled />
<select data-tip="Dominant culture. Click to change burg culture (to change cell culture use Cultures Editor)" class="stateCulture">
@ -106,10 +108,14 @@ function overviewBurgs() {
data-tip="${b.capital ? " This burg is a state capital" : "Click to assign a capital status"}"
class="icon-star-empty${b.capital ? "" : " inactive pointer"}"
></span>
<span data-tip="Click to toggle port status" class="icon-anchor pointer${b.port ? "" : " inactive"}" style="font-size:.9em"></span>
<span data-tip="Click to toggle port status" class="icon-anchor pointer${
b.port ? "" : " inactive"
}" style="font-size:.9em"></span>
</div>
<span data-tip="Edit burg" class="icon-pencil"></span>
<span class="locks pointer ${b.lock ? "icon-lock" : "icon-lock-open inactive"}" onmouseover="showElementLockTip(event)"></span>
<span class="locks pointer ${
b.lock ? "icon-lock" : "icon-lock-open inactive"
}" onmouseover="showElementLockTip(event)"></span>
<span data-tip="Remove burg" class="icon-trash-empty"></span>
</div>`;
}
@ -125,8 +131,12 @@ function overviewBurgs() {
body.querySelectorAll("div > input.burgName").forEach(el => el.addEventListener("input", changeBurgName));
body.querySelectorAll("div > span.icon-dot-circled").forEach(el => el.addEventListener("click", zoomIntoBurg));
body.querySelectorAll("div > select.stateCulture").forEach(el => el.addEventListener("change", changeBurgCulture));
body.querySelectorAll("div > input.burgPopulation").forEach(el => el.addEventListener("change", changeBurgPopulation));
body.querySelectorAll("div > span.icon-star-empty").forEach(el => el.addEventListener("click", toggleCapitalStatus));
body
.querySelectorAll("div > input.burgPopulation")
.forEach(el => el.addEventListener("change", changeBurgPopulation));
body
.querySelectorAll("div > span.icon-star-empty")
.forEach(el => el.addEventListener("click", toggleCapitalStatus));
body.querySelectorAll("div > span.icon-anchor").forEach(el => el.addEventListener("click", togglePortStatus));
body.querySelectorAll("div > span.locks").forEach(el => el.addEventListener("click", toggleBurgLockStatus));
body.querySelectorAll("div > span.icon-pencil").forEach(el => el.addEventListener("click", openBurgEditor));
@ -137,7 +147,9 @@ function overviewBurgs() {
function getCultureOptions(culture) {
let options = "";
pack.cultures.filter(c => !c.removed).forEach(c => (options += `<option ${c.i === culture ? "selected" : ""} value="${c.i}">${c.name}</option>`));
pack.cultures
.filter(c => !c.removed)
.forEach(c => (options += `<option ${c.i === culture ? "selected" : ""} value="${c.i}">${c.name}</option>`));
return options;
}
@ -228,7 +240,8 @@ function overviewBurgs() {
function triggerBurgRemove() {
const burg = +this.parentNode.dataset.id;
if (pack.burgs[burg].capital) return tip("You cannot remove the capital. Please change the capital first", false, "error");
if (pack.burgs[burg].capital)
return tip("You cannot remove the capital. Please change the capital first", false, "error");
confirmationDialog({
title: "Remove burg",
@ -266,8 +279,10 @@ function overviewBurgs() {
function addBurgOnClick() {
const point = d3.mouse(this);
const cell = findCell(point[0], point[1]);
if (pack.cells.h[cell] < 20) return tip("You cannot place state into the water. Please click on a land cell", false, "error");
if (pack.cells.burg[cell]) return tip("There is already a burg in this cell. Please select a free cell", false, "error");
if (pack.cells.h[cell] < 20)
return tip("You cannot place state into the water. Please click on a land cell", false, "error");
if (pack.cells.burg[cell])
return tip("There is already a burg in this cell. Please select a free cell", false, "error");
addBurg(point); // add new burg
@ -301,7 +316,19 @@ function overviewBurgs() {
const capital = b.capital;
const province = pack.cells.province[b.cell];
const parent = province ? province + states.length - 1 : b.state;
return {id, i: b.i, state: b.state, culture: b.culture, province, parent, name: b.name, population, capital, x: b.x, y: b.y};
return {
id,
i: b.i,
state: b.state,
culture: b.culture,
province,
parent,
name: b.name,
population,
capital,
x: b.x,
y: b.y
};
});
const data = states.concat(burgs);
if (data.length < 2) return tip("No burgs to show", false, "error");
@ -452,7 +479,7 @@ function overviewBurgs() {
}
function downloadBurgsData() {
let data = `Id,Burg,Province,Province Full Name,State,State Full Name,Culture,Religion,Population,Latitude,Longitude,Elevation (${heightUnit.value}),Capital,Port,Citadel,Walls,Plaza,Temple,Shanty Town`; // headers
let data = `Id,Burg,Province,Province Full Name,State,State Full Name,Culture,Religion,Population,X,Y,Latitude,Longitude,Elevation (${heightUnit.value}),Capital,Port,Citadel,Walls,Plaza,Temple,Shanty Town`; // headers
if (options.showMFCGMap) data += `,City Generator Link`;
data += "\n";
@ -471,6 +498,8 @@ function overviewBurgs() {
data += rn(b.population * populationRate * urbanization) + ",";
// add geography data
data += b.x + ",";
data += b.y + ",";
data += getLatitude(b.y, 2) + ",";
data += getLongitude(b.x, 2) + ",";
data += parseInt(getHeight(pack.cells.h[b.cell])) + ",";

View file

@ -44,7 +44,12 @@ function editEmblem(type, id, el) {
function defineEmblemData(e) {
const parent = e.target.parentNode;
const [g, t] = parent.id === "burgEmblems" ? [pack.burgs, "burg"] : parent.id === "provinceEmblems" ? [pack.provinces, "province"] : [pack.states, "state"];
const [g, t] =
parent.id === "burgEmblems"
? [pack.burgs, "burg"]
: parent.id === "provinceEmblems"
? [pack.provinces, "province"]
: [pack.states, "state"];
const i = +e.target.dataset.i;
type = t;
id = type + "COA" + i;
@ -88,8 +93,12 @@ function editEmblem(type, id, el) {
emblemBurgs.options.length = 0;
emblemBurgs.options.add(new Option("", 0, false, !burg));
const burgList = validBurgs.filter(burg => (province ? pack.cells.province[burg.cell] === province : burg.state === state));
burgList.forEach(b => emblemBurgs.options.add(new Option(b.capital ? "👑 " + b.name : b.name, b.i, false, b.i === burg)));
const burgList = validBurgs.filter(burg =>
province ? pack.cells.province[burg.cell] === province : burg.state === state
);
burgList.forEach(b =>
emblemBurgs.options.add(new Option(b.capital ? "👑 " + b.name : b.name, b.i, false, b.i === burg))
);
emblemBurgs.options[0].disabled = true;
COArenderer.trigger(id, el.coa);
@ -224,12 +233,15 @@ function editEmblem(type, id, el) {
}
function upload(type) {
const input = type === "image" ? document.getElementById("emblemImageToLoad") : document.getElementById("emblemSVGToLoad");
const input =
type === "image" ? document.getElementById("emblemImageToLoad") : document.getElementById("emblemSVGToLoad");
const file = input.files[0];
input.value = "";
if (file.size > 500000) {
tip(`File is too big, please optimize file size up to 500kB and re-upload. Recommended size is 200x200 px and up to 100kB`, true, "error", 5000);
const message =
"File is too big, please optimize file size up to 500kB and re-upload. Recommended size is 200x200 px and up to 100kB";
tip(message, true, "error", 5000);
return;
}
@ -238,36 +250,37 @@ function editEmblem(type, id, el) {
reader.onload = function (readerEvent) {
const result = readerEvent.target.result;
const defs = document.getElementById("defs-emblems");
const coa = document.getElementById(id); // old emblem
const oldEmblem = document.getElementById(id);
if (type === "image") {
const svg = `<svg id="${id}" xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200"><image x="0" y="0" width="200" height="200" href="${result}"/></svg>`;
defs.insertAdjacentHTML("beforeend", svg);
} else {
let href = result; // raster images
if (type === "svg") {
const el = document.createElement("html");
el.innerHTML = result;
// remove sodipodi and inkscape attributes
el.querySelectorAll("*").forEach(el => {
const attributes = el.getAttributeNames();
attributes.forEach(attr => {
if (el.id === "adobe_illustrator_pgf") el.remove(); // remove Adobe Illustrator inner data
el.getAttributeNames().forEach(attr => {
// remove sodipodi and inkscape attributes
if (attr.includes("inkscape") || attr.includes("sodipodi")) el.removeAttribute(attr);
});
});
const svg = el.querySelector("svg");
if (!svg) {
tip("The file should be prepated for load to FMG. Please use Armoria or other relevant tools", false, "error");
const message = "The file is not a valid SVG. Please use Armoria or other relevant tools";
tip(message, false, "error");
return;
}
const newEmblem = defs.appendChild(svg);
newEmblem.id = id;
newEmblem.setAttribute("width", 200);
newEmblem.setAttribute("height", 200);
const serialized = new XMLSerializer().serializeToString(svg);
href = "data:image/svg+xml;base64," + window.btoa(serialized);
}
if (coa) coa.remove(); // remove old emblem
const svg = `<svg id="${id}" viewBox="0 0 200 200"><image width="200" height="200" href="${href}"/></svg>`;
defs.insertAdjacentHTML("beforeend", svg);
if (oldEmblem) oldEmblem.remove();
el.coa = "custom";
emblemShapeSelector.disabled = true;
};
@ -351,7 +364,9 @@ function editEmblem(type, id, el) {
validStates
.map(state => {
const el = document.getElementById("stateCOA" + state.i);
return `<figure id="state_${state.i}"><a href="#provinces_${state.i}"><figcaption>${state.fullName}</figcaption>${getSVG(el, 200)}</a></figure>`;
return `<figure id="state_${state.i}"><a href="#provinces_${state.i}"><figcaption>${
state.fullName
}</figcaption>${getSVG(el, 200)}</a></figure>`;
})
.join("") +
`</div>`;
@ -362,13 +377,14 @@ function editEmblem(type, id, el) {
const figures = stateProvinces
.map(province => {
const el = document.getElementById("provinceCOA" + province.i);
return `<figure id="province_${province.i}"><a href="#burgs_${province.i}"><figcaption>${province.fullName}</figcaption>${getSVG(
el,
200
)}</a></figure>`;
return `<figure id="province_${province.i}"><a href="#burgs_${province.i}"><figcaption>${
province.fullName
}</figcaption>${getSVG(el, 200)}</a></figure>`;
})
.join("");
return stateProvinces.length ? `<div id="provinces_${state.i}">${back}<h2>${state.fullName} provinces</h2>${figures}</div>` : "";
return stateProvinces.length
? `<div id="provinces_${state.i}">${back}<h2>${state.fullName} provinces</h2>${figures}</div>`
: "";
})
.join("");
@ -385,7 +401,9 @@ function editEmblem(type, id, el) {
return `<figure id="burg_${burg.i}"><figcaption>${burg.name}</figcaption>${getSVG(el, 200)}</figure>`;
})
.join("");
return provinceBurgs.length ? `<div id="burgs_${province.i}">${back}<h2>${province.fullName} burgs</h2>${provinceBurgFigures}</div>` : "";
return provinceBurgs.length
? `<div id="burgs_${province.i}">${back}<h2>${province.fullName} burgs</h2>${provinceBurgFigures}</div>`
: "";
})
.join("");
@ -464,7 +482,7 @@ function editEmblem(type, id, el) {
}
div > a {
float: right;
font-family: monospace;
font-family: var(--monospace);
margin-top: 0.8em;
}
</style>

View file

@ -54,8 +54,12 @@ function overviewMarkers() {
<div data-tip="Marker icon and type" style="width:12em">${icon} ${type}</div>
<span style="padding-right:.1em" data-tip="Edit marker" class="icon-pencil"></span>
<span style="padding-right:.1em" data-tip="Focus on marker position" class="icon-dot-circled pointer"></span>
<span style="padding-right:.1em" data-tip="Pin marker (display only pinned markers)" class="icon-pin ${pinned ? "" : "inactive"}" pointer"></span>
<span style="padding-right:.1em" class="locks pointer ${lock ? "icon-lock" : "icon-lock-open inactive"}" onmouseover="showElementLockTip(event)"></span>
<span style="padding-right:.1em" data-tip="Pin marker (display only pinned markers)" class="icon-pin ${
pinned ? "" : "inactive"
}" pointer"></span>
<span style="padding-right:.1em" class="locks pointer ${
lock ? "icon-lock" : "icon-lock-open inactive"
}" onmouseover="showElementLockTip(event)"></span>
<span data-tip="Remove marker" class="icon-trash-empty"></span>
</div>`;
})
@ -170,16 +174,20 @@ function overviewMarkers() {
}
function exportMarkers() {
const headers = "Id,Type,Icon,Name,Note,X,Y\n";
const headers = "Id,Type,Icon,Name,Note,X,Y,Latitude,Longitude\n";
const quote = s => '"' + s.replaceAll('"', '""') + '"';
const body = pack.markers.map(marker => {
const {i, type, icon, x, y} = marker;
const id = `marker${i}`;
const note = notes.find(note => note.id === id);
const name = note ? quote(note.name) : 'Unknown';
const legend = note ? quote(note.legend) : '';
return [id, type, icon, name, legend, x, y].join(",");
const name = note ? quote(note.name) : "Unknown";
const legend = note ? quote(note.legend) : "";
const lat = getLatitude(y, 2);
const lon = getLongitude(x, 2);
return [id, type, icon, name, legend, x, y, lat, lon].join(",");
});
const data = headers + body.join("\n");

View file

@ -23,23 +23,11 @@ function editNamesbase() {
const uploader = document.getElementById("namesbaseToLoad");
document.getElementById("namesbaseUpload").addEventListener("click", () => {
uploader.addEventListener(
"change",
function (event) {
uploadFile(event.target, d => namesbaseUpload(d, true));
},
{once: true}
);
uploader.addEventListener("change", e => uploadFile(e.target, d => namesbaseUpload(d, true)), {once: true});
uploader.click();
});
document.getElementById("namesbaseUploadExtend").addEventListener("click", () => {
uploader.addEventListener(
"change",
function (event) {
uploadFile(event.target, d => namesbaseUpload(d, false));
},
{once: true}
);
uploader.addEventListener("change", e => uploadFile(e.target, d => namesbaseUpload(d, false)), {once: true});
uploader.click();
});
@ -92,11 +80,13 @@ function editNamesbase() {
function updateNamesData() {
const base = +document.getElementById("namesbaseSelect").value;
const rawInput = document.getElementById("namesbaseTextarea").value;
if (rawInput.split(",").length < 3) return tip("The names data provided is too short of incorrect", false, "error");
const input = document.getElementById("namesbaseTextarea");
if (input.value.split(",").length < 3)
return tip("The names data provided is too short of incorrect", false, "error");
const namesData = rawInput.replace(/[/|]/g, "");
nameBases[base].b = namesData;
const securedNamesData = input.value.replace(/[/|]/g, "");
nameBases[base].b = securedNamesData;
input.value = securedNamesData;
Names.updateChain(base);
}
@ -113,19 +103,13 @@ function editNamesbase() {
function updateBaseMin() {
const base = +document.getElementById("namesbaseSelect").value;
if (+this.value > nameBases[base].max) {
tip("Minimal length cannot be greater than maximal", false, "error");
return;
}
if (+this.value > nameBases[base].max) return tip("Minimal length cannot be greater than maximal", false, "error");
nameBases[base].min = +this.value;
}
function updateBaseMax() {
const base = +document.getElementById("namesbaseSelect").value;
if (+this.value < nameBases[base].min) {
tip("Maximal length should be greater than minimal", false, "error");
return;
}
if (+this.value < nameBases[base].min) return tip("Maximal length should be greater than minimal", false, "error");
nameBases[base].max = +this.value;
}
@ -256,16 +240,15 @@ function editNamesbase() {
function namesbaseUpload(dataLoaded, override = true) {
const data = dataLoaded.split("\r\n");
if (!data || !data[0]) {
tip("Cannot load a namesbase. Please check the data format", false, "error");
return;
}
if (!data || !data[0]) return tip("Cannot load a namesbase. Please check the data format", false, "error");
Names.clearChains();
if (override) nameBases = [];
data.forEach(d => {
const e = d.split("|");
nameBases.push({name: e[0], min: e[1], max: e[2], d: e[3], m: e[4], b: e[5]});
data.forEach(base => {
const [name, min, max, d, m, names] = base.split("|");
const secureNames = names.replace(/[/|]/g, "");
nameBases.push({name, min, max, d, m, b: secureNames});
});
createBasesList();

View file

@ -76,7 +76,7 @@ document
// show popup with a list of Patreon supportes (updated manually)
async function showSupporters() {
const {supporters} = await import("../dynamic/supporters.js?v=19062022");
const {supporters} = await import("../dynamic/supporters.js?v=1.89.15");
const list = supporters.split("\n").sort();
const columns = window.innerWidth < 800 ? 2 : 5;
@ -157,6 +157,8 @@ optionsContent.addEventListener("click", function (event) {
else if (id === "translateExtent") toggleTranslateExtent(event.target);
else if (id === "speakerTest") testSpeaker();
else if (id === "themeColorRestore") restoreDefaultThemeColor();
else if (id === "loadGoogleTranslateButton") loadGoogleTranslate();
else if (id === "resetLanguage") resetLanguage();
});
function mapSizeInputChange() {
@ -474,6 +476,44 @@ function changeDialogsTheme(themeColor, transparency) {
});
}
function loadGoogleTranslate() {
const script = document.createElement("script");
script.src = "https://translate.google.com/translate_a/element.js?cb=initGoogleTranslate";
script.onload = () => {
document.getElementById("loadGoogleTranslateButton")?.remove();
// replace mapLayers underline <u> with bare text to avoid translation issue
document
.getElementById("mapLayers")
.querySelectorAll("li")
.forEach(el => {
const text = el.innerHTML.replace(/<u>(.+)<\/u>/g, "$1");
el.innerHTML = text;
});
};
document.head.appendChild(script);
}
function initGoogleTranslate() {
new google.translate.TranslateElement(
{pageLanguage: "en", layout: google.translate.TranslateElement.InlineLayout.VERTICAL},
"google_translate_element"
);
}
function resetLanguage() {
const languageSelect = document.querySelector("#google_translate_element select");
if (!languageSelect.value) return;
languageSelect.value = "en";
languageSelect.dispatchEvent(new Event("change"));
// do once again to actually reset the language
languageSelect.value = "en";
languageSelect.dispatchEvent(new Event("change"));
}
function changeZoomExtent(value) {
if (+zoomExtentMin.value > +zoomExtentMax.value) {
[zoomExtentMin.value, zoomExtentMax.value] = [zoomExtentMax.value, zoomExtentMin.value];

View file

@ -37,7 +37,9 @@ function overviewRegiments(state) {
const insert = html => document.getElementById("regimentsTotal").insertAdjacentHTML("beforebegin", html);
for (const u of options.military) {
const label = capitalize(u.name.replace(/_/g, " "));
insert(`<div data-tip="Regiment ${u.name} units number. Click to sort" class="sortable removable" data-sortby="${u.name}">${label}&nbsp;</div>`);
insert(
`<div data-tip="Regiment ${u.name} units number. Click to sort" class="sortable removable" data-sortby="${u.name}">${label}&nbsp;</div>`
);
}
header.querySelectorAll(".removable").forEach(function (e) {
e.addEventListener("click", function () {
@ -60,10 +62,12 @@ function overviewRegiments(state) {
for (const r of s.military) {
const sortData = options.military.map(u => `data-${u.name}=${r.u[u.name] || 0}`).join(" ");
const lineData = options.military
.map(u => `<div data-type="${u.name}" data-tip="${capitalize(u.name)} units number">${r.u[u.name] || 0}</div>`)
.map(
u => `<div data-type="${u.name}" data-tip="${capitalize(u.name)} units number">${r.u[u.name] || 0}</div>`
)
.join(" ");
lines += /* html */ `<div class="states" data-id=${r.i} data-s="${s.i}" data-state="${s.name}" data-name="${r.name}" ${sortData} data-total="${r.a}">
lines += /* html */ `<div class="states" data-id="${r.i}" data-s="${s.i}" data-state="${s.name}" data-name="${r.name}" ${sortData} data-total="${r.a}">
<fill-box data-tip="${s.fullName}" fill="${s.color}" disabled></fill-box>
<input data-tip="${s.fullName}" style="width:6em" value="${s.name}" readonly />
<span data-tip="Regiment's emblem" style="width:1em">${r.icon}</span>
@ -79,7 +83,9 @@ function overviewRegiments(state) {
lines += /* html */ `<div id="regimentsTotalLine" class="totalLine" data-tip="Total of all displayed regiments">
<div style="width: 21em; margin-left: 1em">Regiments: ${regiments.length}</div>
${options.military.map(u => `<div style="width:5em">${si(d3.sum(regiments.map(r => r.u[u.name] || 0)))}</div>`).join(" ")}
${options.military
.map(u => `<div style="width:5em">${si(d3.sum(regiments.map(r => r.u[u.name] || 0)))}</div>`)
.join(" ")}
<div style="width:5em">${si(d3.sum(regiments.map(r => r.a)))}</div>
</div>`;
@ -92,7 +98,9 @@ function overviewRegiments(state) {
// add listeners
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => regimentHighlightOn(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => regimentHighlightOff(ev)));
body
.querySelectorAll("div.states")
.forEach(el => el.addEventListener("mouseleave", ev => regimentHighlightOff(ev)));
}
function updateFilter(state) {
@ -158,10 +166,7 @@ function overviewRegiments(state) {
function addRegimentOnClick() {
const state = +regimentsFilter.value;
if (state === -1) {
tip("Please select state from the list", false, "error");
return;
}
if (state === -1) return tip("Please select state from the list", false, "error");
const point = d3.mouse(this);
const cell = findCell(point[0], point[1]);
@ -180,15 +185,32 @@ function overviewRegiments(state) {
function downloadRegimentsData() {
const units = options.military.map(u => u.name);
let data = "State,Id,Name," + units.map(u => capitalize(u)).join(",") + ",Total\n"; // headers
let data =
"State,Id,Icon,Name," +
units.map(u => capitalize(u)).join(",") +
",X,Y,Latitude,Longitude,Base X,Base Y,Base Latitude,Base Longitude\n"; // headers
body.querySelectorAll(":scope > div:not(.totalLine)").forEach(function (el) {
data += el.dataset.state + ",";
data += el.dataset.id + ",";
data += el.dataset.name + ",";
data += units.map(u => el.dataset[u]).join(",") + ",";
data += el.dataset.total + "\n";
});
for (const s of pack.states) {
if (!s.i || s.removed || !s.military.length) continue;
for (const r of s.military) {
data += s.name + ",";
data += r.i + ",";
data += r.icon + ",";
data += r.name + ",";
data += units.map(unit => r.u[unit]).join(",") + ",";
data += r.x + ",";
data += r.y + ",";
data += getLatitude(r.y, 2) + ",";
data += getLongitude(r.x, 2) + ",";
data += r.bx + ",";
data += r.by + ",";
data += getLatitude(r.by, 2) + ",";
data += getLongitude(r.bx, 2) + "\n";
}
}
const name = getFileName("Regiments") + ".csv";
downloadFile(data, name);

View file

@ -137,7 +137,10 @@ function recalculatePopulation() {
}
function regenerateStates() {
recreateStates();
const newStates = recreateStates();
if (!newStates) return;
pack.states = newStates;
BurgsAndStates.expandStates();
BurgsAndStates.normalizeStates();
BurgsAndStates.collectStatistics();
@ -165,21 +168,32 @@ function recreateStates() {
Math.random = aleaPRNG(localSeed);
const statesCount = +regionsOutput.value;
if (!statesCount) {
tip(`<i>States Number</i> option value is zero. No counties are generated`, false, "error");
return null;
}
const validBurgs = pack.burgs.filter(b => b.i && !b.removed);
if (!validBurgs.length) {
tip("There are no any burgs to generate states. Please create burgs first", false, "error");
return null;
}
if (!validBurgs.length)
return tip("There are no any burgs to generate states. Please create burgs first", false, "error");
if (validBurgs.length < statesCount)
tip(
`Not enough burgs to generate ${statesCount} states. Will generate only ${validBurgs.length} states`,
false,
"warn"
);
if (validBurgs.length < statesCount) {
const message = `Not enough burgs to generate ${statesCount} states. Will generate only ${validBurgs.length} states`;
tip(message, false, "warn");
}
const lockedStates = pack.states.filter(s => s.i && !s.removed && s.lock);
const validStates = pack.states.filter(s => s.i && !s.removed);
const lockedStates = validStates.filter(s => s.lock);
const lockedStatesIds = lockedStates.map(s => s.i);
const lockedStatesCapitals = lockedStates.map(s => s.capital);
if (lockedStates.length === validStates.length) {
tip("Unable to regenerate as all states are locked", false, "error");
return null;
}
// turn all old capitals into towns, except for the capitals of locked states
for (const burg of validBurgs) {
if (!burg.capital) continue;
@ -229,7 +243,7 @@ function recreateStates() {
// restore locked states
lockedStates.forEach(state => {
const newId = newStates.length;
const {x, y} = validBurgs[state.capital];
const {x, y} = pack.burgs[state.capital];
capitalsTree.add([x, y]);
// update label id reference
@ -300,9 +314,7 @@ function recreateStates() {
newStates.push({i, name, type, capital: capital.i, center: capital.cell, culture, expansionism, coa});
}
if (!statesCount) tip(`<i>States Number</i> option is set to zero. No counties are generated`, false, "warn");
pack.states = newStates;
return newStates;
}
function regenerateProvinces() {
@ -920,6 +932,6 @@ function viewCellDetails() {
}
async function overviewCharts() {
const Overview = await import("../dynamic/overview/charts-overview.js?v=1.87.03");
const Overview = await import("../dynamic/overview/charts-overview.js?v=1.89.24");
Overview.open();
}

View file

@ -134,7 +134,11 @@ function isCtrlClick(event) {
}
function generateDate(from = 100, to = 1000) {
return new Date(rand(from, to), rand(12), rand(31)).toLocaleDateString("en", {year: "numeric", month: "long", day: "numeric"});
return new Date(rand(from, to), rand(12), rand(31)).toLocaleDateString("en", {
year: "numeric",
month: "long",
day: "numeric"
});
}
function getLongitude(x, decimals = 2) {
@ -158,7 +162,8 @@ void (function () {
const defaultOptions = {default: 1, step: 0.01, min: 0, max: 100, required: true};
window.prompt = function (promptText = defaultText, options = defaultOptions, callback) {
if (options.default === undefined) return ERROR && console.error("Prompt: options object does not have default value defined");
if (options.default === undefined)
return ERROR && console.error("Prompt: options object does not have default value defined");
const input = prompt.querySelector("#promptInput");
prompt.querySelector("#promptText").innerHTML = promptText;
@ -192,41 +197,3 @@ void (function () {
prompt.style.display = "none";
});
})();
// indexedDB; ldb object
void (function () {
function e(t, o) {
return n
? void (n.transaction("s").objectStore("s").get(t).onsuccess = function (e) {
var t = (e.target.result && e.target.result.v) || null;
o(t);
})
: void setTimeout(function () {
e(t, o);
}, 100);
}
var t = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
if (!t) return void ERROR && console.error("indexedDB not supported");
var n,
o = {k: "", v: ""},
r = t.open("d2", 1);
(r.onsuccess = function (e) {
n = this.result;
}),
(r.onerror = function (e) {
ERROR && console.error("indexedDB request error"), INFO && console.log(e);
}),
(r.onupgradeneeded = function (e) {
n = null;
var t = e.target.result.createObjectStore("s", {keyPath: "k"});
t.transaction.oncomplete = function (e) {
n = e.target.db;
};
}),
(window.ldb = {
get: e,
set: function (e, t) {
(o.k = e), (o.v = t), n.transaction("s", "readwrite").objectStore("s").put(o);
}
});
})();

View file

@ -1,12 +1,12 @@
"use strict";
// version and caching control
const version = "1.89.15"; // generator version, update each time
const version = "1.89.33"; // generator version, update each time
{
document.title += " v" + version;
const loadingScreenVersion = document.getElementById("version");
if (loadingScreenVersion) loadingScreenVersion.innerHTML = version;
const loadingScreenVersion = document.getElementById("versionText");
if (loadingScreenVersion) loadingScreenVersion.innerText = `v${version}`;
const versionNumber = parseFloat(version);
const storedVersion = localStorage.getItem("version") ? parseFloat(localStorage.getItem("version")) : 0;
@ -28,15 +28,14 @@ const version = "1.89.15"; // generator version, update each time
<ul>
<strong>Latest changes:</strong>
<li>Autosave feature (in Options)</li>
<li>Google translation support (in Options)</li>
<li>Religions can be edited and redrawn like cultures</li>
<li>Lock states, provinces, cultures, and religions from regeneration</li>
<li>Heightmap brushes: linear edit option</li>
<li>Data Charts screen</li>
<li>Сultures and religions can have multiple parents in hierarchy tree</li>
<li>Heightmap selection screen</li>
<li>Dialogs optimization for mobile</li>
<li>New heightmap template: Fractious</li>
<li>Template Editor: mask and invert tools</li>
</ul>
<p>Join our <a href="${discord}" target="_blank">Discord server</a> and <a href="${reddit}" target="_blank">Reddit community</a> to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.</p>