Merge branch 'Azgaar:master' into master

This commit is contained in:
Gergely Mészáros, Ph.D 2021-09-07 17:31:54 +02:00 committed by GitHub
commit abace06175
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 513 additions and 349 deletions

View file

@ -1,10 +1,10 @@
# Fantasy Map Generator
Azgaar's _Fantasy Map Generator_ is a free client-side web application generating interactive and highly customizable svg maps based on voronoi diagram.
Azgaar's _Fantasy Map Generator_ is a free web application generating interactive and highly customizable svg maps based on voronoi diagram.
Project is under development, the current version is available on [Github Pages](https://azgaar.github.io/Fantasy-Map-Generator).
Refer to the [project wiki](https://github.com/Azgaar/Fantasy-Map-Generator/wiki) for a guidance. Some details are covered in my old blog [_Fantasy Maps for fun and glory_](https://azgaar.wordpress.com), you may also keep an eye on my [Trello devboard](https://trello.com/b/7x832DG4/fantasy-map-generator).
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)
@ -12,18 +12,20 @@ Refer to the [project wiki](https://github.com/Azgaar/Fantasy-Map-Generator/wiki
[![preview](https://cdn.discordapp.com/attachments/587406457725779968/594840632296734720/preview3.png)](https://cdn.discordapp.com/attachments/515359096925454350/593891237984206848/The_Wichin_Island_-_diplomacy.png)
Join our [Reddit community](https://www.reddit.com/r/FantasyMapGenerator) and [Discord server](https://discordapp.com/invite/X7E84HU) to share the created maps, discuss the Generator, suggest ideas and get a most recent updates. You may also contact me directly via [email](mailto:azgaar.fmg@yandex.by). For bug reports please use the project [issues page](https://github.com/Azgaar/Fantasy-Map-Generator/issues) or Discord "Bugs" channel. If you are facing performance issues, please read [the tips](https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Tips#performance-tips).
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).
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 welcomed. The Tool codebase is messy and requires re-design, but I will appreciate if you start with minor changes.
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).
_Inspiration:_
* Martin O'Leary's [_Generating fantasy maps_](https://mewo2.com/notes/terrain)
- Martin O'Leary's [_Generating fantasy maps_](https://mewo2.com/notes/terrain)
* Amit Patel's [_Polygonal Map Generation for Games_](http://www-cs-students.stanford.edu/~amitp/game-programming/polygon-map-generation)
- Amit Patel's [_Polygonal Map Generation for Games_](http://www-cs-students.stanford.edu/~amitp/game-programming/polygon-map-generation)
* Scott Turner's [_Here Dragons Abound_](https://heredragonsabound.blogspot.com)
- Scott Turner's [_Here Dragons Abound_](https://heredragonsabound.blogspot.com)

View file

@ -1,5 +1,9 @@
Azgaar's Fantasy Map Generator
This is an open-source software available under MIT license
Developed by Azgaar (azgaar.fmg@yandex.com) and contributors
Minsk, 2017-2021. MIT License
https://github.com/Azgaar/Fantasy-Map-Generator
To run the tool unzip ALL files and open index.html in browser

View file

@ -358,14 +358,11 @@ div.tab > button#optionsHide {
}
#options {
margin: 10px;
font-family: Consolas, monospace;
position: absolute;
font-family: Consolas, monospace;
border: solid 1px #5e4fa2;
width: 300px;
background-position: center;
background-size: cover;
background-blend-mode: color-dodge;
margin: 10px;
padding-bottom: 0.3em;
}
#options input,
@ -576,14 +573,16 @@ input[type="color"]::-webkit-color-swatch-wrapper {
padding-left: 2.5px;
}
#sticked {
display: flex;
justify-content: space-evenly;
width: 100%;
}
#sticked button {
background-color: #997c8900;
padding: 0;
margin-bottom: 2px;
width: 22%;
font-size: 1em;
border: 0;
background-color: transparent;
font-weight: bold;
border: 0;
}
#sticked button:hover {
@ -985,16 +984,16 @@ body button.noicon {
#controlPoints > path {
fill: none;
stroke: #000000;
stroke: #0a0909;
stroke-width: 2;
opacity: 0.4;
cursor: pointer;
}
#controlCells > .current {
fill: #82c8ff40;
stroke: #82c8ff;
stroke-width: 0.4;
#controlCells {
pointer-events: none;
fill: #82c8ff80;
stroke: "none";
}
#vertices > circle {
@ -1801,16 +1800,13 @@ div.editorLine {
width: 7em;
}
#unitsBody > div > select,
#unitsBody > div > input[type="text"] {
width: 11.2em;
}
#unitsBody > div > select {
width: 11.32em;
width: 12em;
}
#unitsBody > div > input[type="number"] {
width: 3.4em;
width: 4.35em;
}
#unitsBody > div > input,

View file

@ -234,7 +234,7 @@
<div id="loading">
<div id="titleName"><t data-t="titleName">Azgaar's</t></div>
<div id="title"><t data-t="title">Fantasy Map Generator</t></div>
<div id="version"><t data-t="version">v. </t>1.65</div>
<div id="version"><t data-t="version">v. </t>1.66</div>
<p id="loading-text"><t data-t="loading">LOADING</t><span>.</span><span>.</span><span>.</span></p>
</div>
@ -1447,11 +1447,11 @@
<div id="sticked">
<button id="newMapButton" data-tip="Generate a new map based on options. Shortcut: F2">New Map</button>
<button id="saveButton" data-tip="Select format to save map">Save</button>
<button id="loadButton" data-tip="Load fully functional map in a .map format">Load</button>
<button id="exportButton" data-tip="Select format to download image or export map data">Export</button>
<button id="saveButton" data-tip="Save fully-functional map in .map format">Save</button>
<button id="loadButton" data-tip="Load fully-functional map in .map format">Load</button>
<button id="zoomReset" data-tip="Reset map zoom. Shortcut: 0">Reset Zoom</button>
</div>
</div>
</div>
@ -3106,8 +3106,8 @@
<div data-tip="Set height exponent, i.e. a value for altitude change sharpness. Altitude affects temperature and hence biomes">
<div>Exponent:</div>
<input id="heightExponentOutput" type="range" min=1.5 max=2.1 value=1.8 step=.01>
<input id="heightExponentInput" data-stored="heightExponent" type="number" min=1.5 max=2.1 value=1.8 step=.01>
<input id="heightExponentOutput" type="range" min=1.5 max=2.2 value=2 step=.01>
<input id="heightExponentInput" data-stored="heightExponent" type="number" min=1.5 max=2.2 value=2 step=.01>
</div>
<div class="unitsHeader" data-tip="Select Temperature scale">
@ -3164,8 +3164,8 @@
<div data-tip="Set how many people are in one population point">
<div>1 population point =</div>
<input id="populationRateOutput" data-stored="populationRate" type="range" min=10 max=9990 step=10 value=1000 style="width:6em">
<input id="populationRateInput" data-stored="populationRate" type="number" min=10 max=9990 step=10 value=1000 style="width:4.5em">
<input id="populationRateOutput" data-stored="populationRate" type="range" min=10 max=9990 step=10 value=1000 />
<input id="populationRateInput" data-stored="populationRate" type="number" min=10 max=9990 step=10 value=1000 />
</div>
<div data-tip="Set urbanization rate: burgs population relative to all population">
@ -3449,34 +3449,64 @@
<div id="preview3d" class="dialog stable" style="display: none; padding: 0px"></div>
<div id="saveMapData" style="display: none" class="dialog">
<div style="margin-bottom: .3em; font-weight: bold">Please select saving method:</div>
<div id="exportMapData" style="display: none" class="dialog">
<div style="margin-bottom: .3em; font-weight: bold">Download image</div>
<div>
<button onclick="saveMap()" data-tip="Download the project in internal .map format (reliable). Then open via 'Load' in menu. Shortcut: Ctrl + S">.map</button>
<button onclick="saveSVG()" data-tip="Download the map as vector image (open in browser or Inkscape)">.svg</button>
<button onclick="saveSVG()" data-tip="Download the map as vector image (open directly in browser or Inkscape)">.svg</button>
<button onclick="savePNG()" data-tip="Download visible part of the map as .png (lossless compressed)">.png</button>
<button onclick="saveJPEG()" data-tip="Download visible part of the map as .jpeg (lossy compressed) image">.jpeg</button>
<button onclick="openSaveTiles()" data-tip="Split map into smaller png tiles and download as zip archive">tiles</button>
<button onclick="saveGeoJSON()" data-tip="Download map data in GeoJSON format">.json</button>
<button onclick="quickSave()" data-tip="Save the project to browser storage (unreliable). Shortcut: F6">storage</button>
<span data-tip="Check to not allow system to automatically hide labels">
<input id="showLabels" class="checkbox" type="checkbox" onchange="hideLabels.checked = !this.checked; invokeActiveZooming()" checked="">
<label for="showLabels" class="checkbox-label">Show all labels</label>
</span>
</div>
<p style="font-style: italic">Keep noted that the only reliable project saving method is having .map file stored on your machine. There is no way to restore map if .map file is lost. We don't keep any data on our side.</p>
<p style="font-style: italic">Generator uses pop-up window to download files. Please ensure your browser does not block popups.</p>
<div data-tip="Define scale of a saved png/jpeg image (e.g. 5x). Saving big images is slow and may cause a browser crash!" style="margin-bottom: .3em">
PNG / JPEG scale:
<input id="pngResolutionInput" data-stored="pngResolution" type="range" min=1 max=8 value=1 style="width: 14em">
<input id="pngResolutionInput" data-stored="pngResolution" type="range" min=1 max=8 value=1 style="width: 10em">
<input id="pngResolutionOutput" data-stored="pngResolution" type="number" min=1 max=8 value=1>
</div>
<div data-tip="Check to not allow system to automatically hide labels">
<input id="showLabels" class="checkbox" type="checkbox" onchange="hideLabels.checked = !this.checked; invokeActiveZooming()" checked="">
<label for="showLabels" class="checkbox-label">Show all labels</label>
<div style="margin: 1em 0 .3em; font-weight: bold">Export to GeoJSON</div>
<div>
<button onclick="saveGeoJSON_Cells()" data-tip="Download cells data in GeoJSON format">cells</button>
<button onclick="saveGeoJSON_Routes()" data-tip="Download routes data in GeoJSON format">routes</button>
<button onclick="saveGeoJSON_Rivers()" data-tip="Download rivers data in GeoJSON format">rivers</button>
<button onclick="saveGeoJSON_Markers()" data-tip="Download markers data in GeoJSON format">markers</button>
</div>
<p style="font-style: italic">GeoJSON format is used in GIS tools such as QGIS. Check out <a href="https://github.com/Azgaar/Fantasy-Map-Generator/wiki/GIS-data-export" target="_blank">wiki-page</a> for guidance</p>
<p style="font-style: italic">Generator uses pop-up window to download files. Please ensure your browser does not block popups.</p>
<p style="font-style: italic">It's also possible to export map to Foundry VTT, see <a href="https://github.com/Ethck/azgaar-foundry" target="_blank">the module.</a></p>
</div>
<div id="saveMapData" style="display: none" class="dialog">
<div style="margin-top: .3em">
<strong>Save map to</strong>
<button onclick="dowloadMap()" data-tip="Download .map file to your local disk. Shortcut: Ctrl + S">machine</button>
<button onclick="saveToDropbox()" data-tip="Save .map file to your Dropbox">dropbox</button>
<button onclick="quickSave()" data-tip="Save the project to browser storage (quick save). It can be unreliable. Shortcut: F6">browser</button>
</div>
<p style="font-style: italic">Maps are saved in <i>.map</i> format, that can be loaded back via 'Load' in menu. Please keep noted that we do not keep any data on our side. There is no way to restore the progress if .map file is lost. Please keep old .map files on your machine or cloud storage as backups.</p>
<p style="font-style: italic">Saving to Dropbox may not be allowed for big files. In this case open <a href="https://www.dropbox.com/home/FMG" target="_blank">Dropbox</a>, download the .map file and upload it manually.</p>
<div style="margin-top: .3em" data-tip="Select .map file on dropbox and share a sharable link">
<strong>Create sharable link</strong>
<button onclick="createSharableDropboxLink()">select file</button>
<div id="sharableLinkContainer" style="display: none">
<a id="sharableLink" target="_blank"></a>
<i data-tip="Copy link to the clipboard" onclick="copyLinkToClickboard()" class="icon-clone pointer"></i>
</div>
</div>
</div>
<div id="loadMapData" style="display: none" class="dialog">
<div style="margin-bottom: .3em">Load map from:</div>
<div style="margin-bottom: .3em">Load map from</div>
<div>
<button onclick="mapToLoad.click()" data-tip="Load .map file from local disk">local disk</button>
<button onclick="loadFromDropbox()" data-tip="Load .map file from your Dropbox">Dropbox</button>
<button onclick="loadURL()" data-tip="Load .map file from URL (server should allow CORS)">URL</button>
<button onclick="quickLoad()" data-tip="Load map from browser storage (if saved before)">storage</button>
</div>
@ -4265,5 +4295,6 @@
<script defer src="libs/jquery.ui.touch-punch.min.js"></script>
<script defer src="libs/pell.min.js"></script>
<script defer src="libs/jszip.min.js"></script>
<script defer src="https://www.dropbox.com/static/api/2/dropins.js" id="dropboxjs" data-app-key="pdr9ae64ip0qno4"></script>
</body>
</html>

102
main.js
View file

@ -2,7 +2,7 @@
// https://github.com/Azgaar/Fantasy-Map-Generator
"use strict";
const version = "1.652"; // generator version1
const version = "1.66"; // generator version1
document.title += " v" + version;
// Switches to disable/enable logging features
@ -219,37 +219,6 @@ void (function checkLoadParameters() {
generateMapOnLoad();
})();
function loadMapFromURL(maplink, random) {
const URL = decodeURIComponent(maplink);
fetch(URL, {method: "GET", mode: "cors"})
.then(response => {
if (response.ok) return response.blob();
throw new Error("Cannot load map from URL");
})
.then(blob => uploadMap(blob))
.catch(error => {
showUploadErrorMessage(error.message, URL, random);
if (random) generateMapOnLoad();
});
}
function showUploadErrorMessage(error, URL, random) {
ERROR && console.error(error);
alertMessage.innerHTML = `Cannot load map from the ${link(URL, "link provided")}.
${random ? `A new random map is generated. ` : ""}
Please ensure the linked file is reachable and CORS is allowed on server side`;
$("#alert").dialog({
title: "Loading error",
width: "32em",
buttons: {
OK: function () {
$(this).dialog("close");
}
}
});
}
function generateMapOnLoad() {
applyStyleOnLoad(); // apply default of previously selected style
generate(); // generate map
@ -262,10 +231,12 @@ function focusOn() {
const url = new URL(window.location.href);
const params = url.searchParams;
if (params.get("from") === "MFCG" && document.referrer) {
const fromMGCG = params.get("from") === "MFCG" && document.referrer;
if (fromMGCG) {
if (params.get("seed").length === 13) {
// show back burg from MFCG
params.set("burg", params.get("seed").slice(-4));
const burgSeed = params.get("seed").slice(-4);
params.set("burg", burgSeed);
} else {
// select burg for MFCG
findBurgForMFCG(params);
@ -273,23 +244,33 @@ function focusOn() {
}
}
const s = +params.get("scale") || 8;
let x = +params.get("x");
let y = +params.get("y");
const scaleParam = params.get("scale");
const cellParam = params.get("cell");
const burgParam = params.get("burg");
const c = +params.get("cell");
if (c) {
x = pack.cells.p[c][0];
y = pack.cells.p[c][1];
if (scaleParam || cellParam || burgParam) {
const scale = +scaleParam || 8;
if (cellParam) {
const cell = +params.get("cell");
const [x, y] = pack.cells.p[cell];
zoomTo(x, y, scale, 1600);
return;
}
if (burgParam) {
const burg = isNaN(+burgParam) ? pack.burgs.find(burg => burg.name === burgParam) : pack.burgs[+burgParam];
if (!burg) return;
const {x, y} = burg;
zoomTo(x, y, scale, 1600);
return;
}
const x = +params.get("x") || graphWidth / 2;
const y = +params.get("y") || graphHeight / 2;
zoomTo(x, y, scale, 1600);
}
const b = +params.get("burg");
if (b && pack.burgs[b]) {
x = pack.burgs[b].x;
y = pack.burgs[b].y;
}
if (x && y) zoomTo(x, y, s, 1600);
}
// find burg for MFCG and focus on it
@ -389,7 +370,7 @@ function applyDefaultBiomesSystem() {
}
function showWelcomeMessage() {
const changelog = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "previous version");
const changelog = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "previous versions");
const reddit = link("https://www.reddit.com/r/FantasyMapGenerator", "Reddit community");
const discord = link("https://discordapp.com/invite/X7E84HU", "Discord server");
const patreon = link("https://www.patreon.com/azgaar", "Patreon");
@ -397,9 +378,10 @@ function showWelcomeMessage() {
alertMessage.innerHTML = `The Fantasy Map Generator is updated up to version <b>${version}</b>.
This version is compatible with ${changelog}, loaded <i>.map</i> files will be auto-updated.
<ul>Main changes:
<li>Ability to add river selecting its cells</li>
<li>Keep river course on edit</li>
<li>Refactor river rendering code</li>
<li>Save and load <i>.map</i> files to Dropbox</li>
<li>Ability to add control points on river edit</li>
<li>New heightmap template: Taklamakan</li>
<li>Option to not scale labels on zoom</li>
</ul>
<p>Join our ${discord} and ${reddit} to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.</p>
@ -880,8 +862,8 @@ function openNearSeaLakes() {
function defineMapSize() {
const [size, latitude] = getSizeAndLatitude();
const randomize = new URL(window.location.href).searchParams.get("options") === "default"; // ignore stored options
if (randomize || !locked("mapSize")) mapSizeOutput.value = mapSizeInput.value = size;
if (randomize || !locked("latitude")) latitudeOutput.value = latitudeInput.value = latitude;
if (randomize || !locked("mapSize")) mapSizeOutput.value = mapSizeInput.value = rn(size);
if (randomize || !locked("latitude")) latitudeOutput.value = latitudeInput.value = rn(latitude);
function getSizeAndLatitude() {
const template = document.getElementById("templateInput").value; // heightmap template
@ -914,11 +896,11 @@ function calculateMapCoordinates() {
const size = +document.getElementById("mapSizeOutput").value;
const latShift = +document.getElementById("latitudeOutput").value;
const latT = (size / 100) * 180;
const latN = 90 - ((180 - latT) * latShift) / 100;
const latS = latN - latT;
const latT = rn((size / 100) * 180, 1);
const latN = rn(90 - ((180 - latT) * latShift) / 100, 1);
const latS = rn(latN - latT, 1);
const lon = Math.min(((graphWidth / graphHeight) * latT) / 2, 180);
const lon = rn(Math.min(((graphWidth / graphHeight) * latT) / 2, 180));
mapCoordinates = {latT, latN, latS, lonT: lon * 2, lonW: -lon, lonE: lon};
}
@ -1405,7 +1387,7 @@ function defineBiomes() {
function getBiomeId(moisture, temperature, height) {
if (height < 20) return 0; // marine biome: all water cells
if (temperature < -5) return 11; // permafrost biome
if (moisture > 40 && temperature > -2 && (height < 25 || (moisture > 24 && height > 24))) return 12; // wetland biome
if (moisture > 40 && temperature > -2 && (height < 25 || (moisture > 24 && height > 24 && height < 60))) return 12; // wetland biome
const moistureBand = Math.min((moisture / 5) | 0, 4); // [0-4]
const temperatureBand = Math.min(Math.max(20 - temperature, 0), 25); // [0-25]

View file

@ -379,14 +379,16 @@ window.HeightmapGenerator = (function () {
const modify = function (range, add, mult, power) {
const min = range === "land" ? 20 : range === "all" ? 0 : +range.split("-")[0];
const max = range === "land" || range === "all" ? 100 : +range.split("-")[1];
grid.cells.h = grid.cells.h.map(h => (h >= min && h <= max ? mod(h) : h));
const isLand = min === 20;
function mod(v) {
if (add) v = min === 20 ? Math.max(v + add, 20) : v + add;
if (mult !== 1) v = min === 20 ? (v - 20) * mult + 20 : v * mult;
if (power) v = min === 20 ? (v - 20) ** power + 20 : v ** power;
return lim(v);
}
grid.cells.h = grid.cells.h.map(h => {
if (h < min || h > max) return h;
if (add) h = isLand ? Math.max(h + add, 20) : h + add;
if (mult !== 1) h = isLand ? (h - 20) * mult + 20 : h * mult;
if (power) h = isLand ? (h - 20) ** power + 20 : h ** power;
return lim(h);
});
};
const smooth = function (fr = 2, add = 0) {

View file

@ -12,6 +12,18 @@ function quickLoad() {
});
}
function loadFromDropbox() {
const options = {
success: function (files) {
const url = files[0].link;
loadMapFromURL(url);
},
linkType: "direct",
extensions: [".map"]
};
Dropbox.choose(options);
}
function loadMapPrompt(blob) {
const workingTime = (Date.now() - last(mapHistory).created) / 60000; // minutes
if (workingTime < 5) {
@ -46,54 +58,111 @@ function loadMapPrompt(blob) {
}
}
function loadMapFromURL(maplink, random) {
const URL = decodeURIComponent(maplink);
fetch(URL, {method: "GET", mode: "cors"})
.then(response => {
if (response.ok) return response.blob();
throw new Error("Cannot load map from URL");
})
.then(blob => uploadMap(blob))
.catch(error => {
showUploadErrorMessage(error.message, URL, random);
if (random) generateMapOnLoad();
});
}
function showUploadErrorMessage(error, URL, random) {
ERROR && console.error(error);
alertMessage.innerHTML = `Cannot load map from the ${link(URL, "link provided")}.
${random ? `A new random map is generated. ` : ""}
Please ensure the linked file is reachable and CORS is allowed on server side`;
$("#alert").dialog({
title: "Loading error",
width: "32em",
buttons: {
OK: function () {
$(this).dialog("close");
}
}
});
}
function uploadMap(file, callback) {
uploadMap.timeStart = performance.now();
const OLDEST_SUPPORTED_VERSION = 0.7;
const currentVersion = parseFloat(version);
const fileReader = new FileReader();
fileReader.onload = function (fileLoadedEvent) {
if (callback) callback();
document.getElementById("coas").innerHTML = ""; // remove auto-generated emblems
const result = fileLoadedEvent.target.result;
const [mapData, mapVersion] = parseLoadedResult(result);
const dataLoaded = fileLoadedEvent.target.result;
const data = dataLoaded.split("\r\n");
const isInvalid = !mapData || isNaN(mapVersion) || mapData.length < 26 || !mapData[5];
const isUpdated = mapVersion === currentVersion;
const isAncient = mapVersion < OLDEST_SUPPORTED_VERSION;
const isNewer = mapVersion > currentVersion;
const isOutdated = mapVersion < currentVersion;
const mapVersion = data[0].split("|")[0] || data[0];
if (mapVersion === version) {
parseLoadedData(data);
return;
}
const archive = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "archived version");
const parsed = parseFloat(mapVersion);
let message = "",
load = false;
if (isNaN(parsed) || data.length < 26 || !data[5]) {
message = `The file you are trying to load is outdated or not a valid .map file.
<br>Please try to open it using an ${archive}`;
} else if (parsed < 0.7) {
message = `The map version you are trying to load (${mapVersion}) is too old and cannot be updated to the current version.
<br>Please keep using an ${archive}`;
} else {
load = true;
message = `The map version (${mapVersion}) does not match the Generator version (${version}).
<br>Click OK to get map <b>auto-updated</b>. In case of issues please keep using an ${archive} of the Generator`;
}
alertMessage.innerHTML = message;
$("#alert").dialog({
title: "Version conflict",
width: "38em",
buttons: {
OK: function () {
$(this).dialog("close");
if (load) parseLoadedData(data);
}
}
});
if (isInvalid) return showUploadMessage("invalid", mapData, mapVersion);
if (isUpdated) return parseLoadedData(mapData);
if (isAncient) return showUploadMessage("ancient", mapData, mapVersion);
if (isNewer) return showUploadMessage("newer", mapData, mapVersion);
if (isOutdated) return showUploadMessage("outdated", mapData, mapVersion);
};
fileReader.readAsText(file, "UTF-8");
}
function parseLoadedResult(result) {
try {
// data can be in FMG internal format or base64 encoded
const isDelimited = result.substr(0, 10).includes("|");
const decoded = isDelimited ? result : decodeURIComponent(atob(result));
const mapData = decoded.split("\r\n");
const mapVersion = parseFloat(mapData[0].split("|")[0] || mapData[0]);
return [mapData, mapVersion];
} catch (error) {
console.error(error);
return [null, null];
}
}
function showUploadMessage(type, mapData, mapVersion) {
const archive = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "archived version");
let message, title, canBeLoaded;
if (type === "invalid") {
message = `The file does not look like a valid <i>.map</i> file.<br>Please check the data format`;
title = "Invalid file";
canBeLoaded = false;
} else if (type === "ancient") {
message = `The map version you are trying to load (${mapVersion}) is too old and cannot be updated to the current version.<br>Please keep using an ${archive}`;
title = "Ancient file";
canBeLoaded = false;
} else if (type === "newer") {
message = `The map version you are trying to load (${mapVersion}) is newer than the current version.<br>Please load the file in the appropriate version`;
title = "Newer file";
canBeLoaded = false;
} else if (type === "outdated") {
message = `The map version (${mapVersion}) does not match the Generator version (${version}).<br>Click OK to get map <b>auto-updated</b>.<br>In case of issues please keep using an ${archive} of the Generator`;
title = "Outdated file";
canBeLoaded = true;
}
alertMessage.innerHTML = message;
const buttons = {
OK: function () {
$(this).dialog("close");
if (canBeLoaded) parseLoadedData(mapData);
}
};
$("#alert").dialog({title, buttons});
}
function parseLoadedData(data) {
try {
// exit customization
@ -710,29 +779,40 @@ function parseLoadedData(data) {
if (version < 1.65) {
// v 1.65 changed rivers data
rivers.attr("style", null); // remove style to unhide layer
d3.select("#rivers").attr("style", null); // remove style to unhide layer
const {cells, rivers} = pack;
for (const river of pack.rivers) {
for (const river of rivers) {
const node = document.getElementById("river" + river.i);
if (node && !river.cells) {
const riverCells = new Set();
const riverCells = [];
const riverPoints = [];
const length = node.getTotalLength() / 2;
const segments = Math.ceil(length / 6);
const increment = length / segments;
for (let i = increment * segments, c = i; i >= 0; i -= increment, c += increment) {
const p1 = node.getPointAtLength(i);
const p2 = node.getPointAtLength(c);
const x = (p1.x + p2.x) / 2;
const y = (p1.y + p2.y) / 2;
const cell = findCell(x, y, 6);
if (cell) riverCells.add(cell);
for (let i = 0; i <= segments; i++) {
const shift = increment * i;
const {x: x1, y: y1} = node.getPointAtLength(length + shift);
const {x: x2, y: y2} = node.getPointAtLength(length - shift);
const x = rn((x1 + x2) / 2, 1);
const y = rn((y1 + y2) / 2, 1);
const cell = findCell(x, y);
riverPoints.push([x, y]);
riverCells.push(cell);
}
river.cells = Array.from(riverCells);
river.cells = riverCells;
river.points = riverPoints;
}
pack.cells.i.forEach(i => {
if (pack.cells.r[i] && pack.cells.h[i] < 20) pack.cells.r[i] = 0;
river.widthFactor = 1;
cells.i.forEach(i => {
const riverInWater = cells.r[i] && cells.h[i] < 20;
if (riverInWater) cells.r[i] = 0;
});
}
}

View file

@ -319,9 +319,10 @@ window.Rivers = (function () {
};
const getRiverPoints = (riverCells, riverPoints) => {
if (riverPoints) return riverPoints;
const {p} = pack.cells;
return riverCells.map((cell, i) => {
if (riverPoints && riverPoints[i]) return riverPoints[i];
if (cell === -1) return getBorderPoint(riverCells[i - 1]);
return p[cell];
});

View file

@ -367,68 +367,65 @@ function inlineStyle(clone) {
// prepare map data for saving
function getMapData() {
TIME && console.time("createMapDataBlob");
TIME && console.time("createMapData");
return new Promise(resolve => {
const date = new Date();
const dateString = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate();
const license = "File can be loaded in azgaar.github.io/Fantasy-Map-Generator";
const params = [version, license, dateString, seed, graphWidth, graphHeight, mapId].join("|");
const settings = [distanceUnitInput.value, distanceScaleInput.value, areaUnit.value, heightUnit.value, heightExponentInput.value, temperatureScale.value, barSizeInput.value, barLabel.value, barBackOpacity.value, barBackColor.value, barPosX.value, barPosY.value, populationRate, urbanization, mapSizeOutput.value, latitudeOutput.value, temperatureEquatorOutput.value, temperaturePoleOutput.value, precOutput.value, JSON.stringify(options), mapName.value, +hideLabels.checked, stylePreset.value, +rescaleLabels.checked].join("|");
const coords = JSON.stringify(mapCoordinates);
const biomes = [biomesData.color, biomesData.habitability, biomesData.name].join("|");
const notesData = JSON.stringify(notes);
const rulersString = rulers.toString();
const date = new Date();
const dateString = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate();
const license = "File can be loaded in azgaar.github.io/Fantasy-Map-Generator";
const params = [version, license, dateString, seed, graphWidth, graphHeight, mapId].join("|");
const settings = [distanceUnitInput.value, distanceScaleInput.value, areaUnit.value, heightUnit.value, heightExponentInput.value, temperatureScale.value, barSizeInput.value, barLabel.value, barBackOpacity.value, barBackColor.value, barPosX.value, barPosY.value, populationRate, urbanization, mapSizeOutput.value, latitudeOutput.value, temperatureEquatorOutput.value, temperaturePoleOutput.value, precOutput.value, JSON.stringify(options), mapName.value, +hideLabels.checked, stylePreset.value, +rescaleLabels.checked].join("|");
const coords = JSON.stringify(mapCoordinates);
const biomes = [biomesData.color, biomesData.habitability, biomesData.name].join("|");
const notesData = JSON.stringify(notes);
const rulersString = rulers.toString();
// clone svg
const cloneEl = document.getElementById("map").cloneNode(true);
// save svg
const cloneEl = document.getElementById("map").cloneNode(true);
// set transform values to default
cloneEl.setAttribute("width", graphWidth);
cloneEl.setAttribute("height", graphHeight);
cloneEl.querySelector("#viewbox").removeAttribute("transform");
// reset transform values to default
cloneEl.setAttribute("width", graphWidth);
cloneEl.setAttribute("height", graphHeight);
cloneEl.querySelector("#viewbox").removeAttribute("transform");
// always remove rulers
cloneEl.querySelector("#ruler").innerHTML = "";
cloneEl.querySelector("#ruler").innerHTML = ""; // always remove rulers
const svg_xml = new XMLSerializer().serializeToString(cloneEl);
const serializedSVG = new XMLSerializer().serializeToString(cloneEl);
const gridGeneral = JSON.stringify({spacing: grid.spacing, cellsX: grid.cellsX, cellsY: grid.cellsY, boundary: grid.boundary, points: grid.points, features: grid.features});
const features = JSON.stringify(pack.features);
const cultures = JSON.stringify(pack.cultures);
const states = JSON.stringify(pack.states);
const burgs = JSON.stringify(pack.burgs);
const religions = JSON.stringify(pack.religions);
const provinces = JSON.stringify(pack.provinces);
const rivers = JSON.stringify(pack.rivers);
const {spacing, cellsX, cellsY, boundary, points, features} = grid;
const gridGeneral = JSON.stringify({spacing, cellsX, cellsY, boundary, points, features});
const packFeatures = JSON.stringify(pack.features);
const cultures = JSON.stringify(pack.cultures);
const states = JSON.stringify(pack.states);
const burgs = JSON.stringify(pack.burgs);
const religions = JSON.stringify(pack.religions);
const provinces = JSON.stringify(pack.provinces);
const rivers = JSON.stringify(pack.rivers);
// store name array only if it is not the same as default
const defaultNB = Names.getNameBases();
const namesData = nameBases
.map((b, i) => {
const names = defaultNB[i] && defaultNB[i].b === b.b ? "" : b.b;
return `${b.name}|${b.min}|${b.max}|${b.d}|${b.m}|${names}`;
})
.join("/");
// store name array only if not the same as default
const defaultNB = Names.getNameBases();
const namesData = nameBases
.map((b, i) => {
const names = defaultNB[i] && defaultNB[i].b === b.b ? "" : b.b;
return `${b.name}|${b.min}|${b.max}|${b.d}|${b.m}|${names}`;
})
.join("/");
// round population to save resources
const pop = Array.from(pack.cells.pop).map(p => rn(p, 4));
// round population to save space
const pop = Array.from(pack.cells.pop).map(p => rn(p, 4));
// data format as below
const data = [params, settings, coords, biomes, notesData, svg_xml, gridGeneral, grid.cells.h, grid.cells.prec, grid.cells.f, grid.cells.t, grid.cells.temp, features, cultures, states, burgs, pack.cells.biome, pack.cells.burg, pack.cells.conf, pack.cells.culture, pack.cells.fl, pop, pack.cells.r, pack.cells.road, pack.cells.s, pack.cells.state, pack.cells.religion, pack.cells.province, pack.cells.crossroad, religions, provinces, namesData, rivers, rulersString].join("\r\n");
const blob = new Blob([data], {type: "text/plain"});
TIME && console.timeEnd("createMapDataBlob");
resolve(blob);
});
// data format as below
const mapData = [params, settings, coords, biomes, notesData, serializedSVG, gridGeneral, grid.cells.h, grid.cells.prec, grid.cells.f, grid.cells.t, grid.cells.temp, packFeatures, cultures, states, burgs, pack.cells.biome, pack.cells.burg, pack.cells.conf, pack.cells.culture, pack.cells.fl, pop, pack.cells.r, pack.cells.road, pack.cells.s, pack.cells.state, pack.cells.religion, pack.cells.province, pack.cells.crossroad, religions, provinces, namesData, rivers, rulersString].join("\r\n");
TIME && console.timeEnd("createMapData");
return mapData;
}
// Download .map file
async function saveMap() {
function dowloadMap() {
if (customization) return tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error");
closeDialogs("#alert");
const blob = await getMapData();
const mapData = getMapData();
const blob = new Blob([mapData], {type: "text/plain"});
const URL = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.download = getFileName() + ".map";
@ -438,6 +435,44 @@ async function saveMap() {
window.URL.revokeObjectURL(URL);
}
function saveToDropbox() {
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 URL = "data:text/plain; base64," + btoa(encodeURIComponent(mapData));
const filename = getFileName() + ".map";
const options = {
success: () => tip("Map is saved to your Dropbox", true, "success", 8000),
error: function (errorMessage) {
tip("Cannot save .map to your Dropbox", true, "error", 8000);
console.error(errorMessage);
}
};
Dropbox.save(URL, filename, options);
}
function createSharableDropboxLink() {
const sharableLink = document.getElementById("sharableLink");
const sharableLinkContainer = document.getElementById("sharableLinkContainer");
const options = {
success: function (files) {
const url = files[0].link;
const fmg = window.location.href.split("?")[0];
const link = `${fmg}/?maplink=${url}`;
const shortLink = link.slice(0, 50) + "...";
sharableLinkContainer.style.display = "block";
sharableLink.innerText = shortLink;
sharableLink.setAttribute("href", link);
},
linkType: "direct",
extensions: [".map"]
};
Dropbox.choose(options);
}
function saveGeoJSON_Cells() {
const json = {type: "FeatureCollection", features: []};
const cells = pack.cells;
@ -556,9 +591,11 @@ function getRiverPoints(node) {
return points;
}
async function quickSave() {
function quickSave() {
if (customization) return tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error");
const blob = await getMapData();
const mapData = getMapData();
const blob = new Blob([mapData], {type: "text/plain"});
if (blob) 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

@ -7,7 +7,7 @@ function editHeightmap() {
<p><i>Erase</i> mode also allows you Convert an Image into a heightmap or use Template Editor.</p>
<p>You can <i>keep</i> the data, but you won't be able to change the coastline.</p>
<p>Try <i>risk</i> mode to change the coastline and keep the data. The data will be restored as much as possible, but it can cause unpredictable errors.</p>
<p>Please <span class="pseudoLink" onclick=saveMap(); editHeightmap();>save the map</span> before editing the heightmap!</p>
<p>Please <span class="pseudoLink" onclick=dowloadMap(); editHeightmap();>save the map</span> before editing the heightmap!</p>
<p>Check out ${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Heightmap-customization", "wiki")} for guidance.</p>`;
$("#alert").dialog({
@ -837,31 +837,27 @@ function editHeightmap() {
const steps = body.querySelectorAll("#templateBody > div");
if (!steps.length) return;
const {addHill, addPit, addRange, addTrough, addStrait, modify, smooth} = HeightmapGenerator;
grid.cells.h = new Uint8Array(grid.cells.i.length); // clean all heights
for (const s of steps) {
if (s.style.opacity == 0.5) continue;
const type = s.dataset.type;
for (const step of steps) {
if (step.style.opacity === "0.5") continue;
const type = step.dataset.type;
const elCount = s.querySelector(".templateCount") || "";
const elHeight = s.querySelector(".templateHeight") || "";
const count = step.querySelector(".templateCount")?.value || "";
const height = step.querySelector(".templateHeight")?.value || "";
const dist = step.querySelector(".templateDist")?.value || null;
const x = step.querySelector(".templateX")?.value || null;
const y = step.querySelector(".templateY")?.value || null;
const elDist = s.querySelector(".templateDist");
const dist = elDist ? elDist.value : null;
const templateX = s.querySelector(".templateX");
const x = templateX ? templateX.value : null;
const templateY = s.querySelector(".templateY");
const y = templateY ? templateY.value : null;
if (type === "Hill") HeightmapGenerator.addHill(elCount.value, elHeight.value, x, y);
else if (type === "Pit") HeightmapGenerator.addPit(elCount.value, elHeight.value, x, y);
else if (type === "Range") HeightmapGenerator.addRange(elCount.value, elHeight.value, x, y);
else if (type === "Trough") HeightmapGenerator.addTrough(elCount.value, elHeight.value, x, y);
else if (type === "Strait") HeightmapGenerator.addStrait(elCount.value, dist);
else if (type === "Add") HeightmapGenerator.modify(dist, +elCount.value, 1);
else if (type === "Multiply") HeightmapGenerator.modify(dist, 0, +elCount.value);
else if (type === "Smooth") HeightmapGenerator.smooth(+elCount.value);
if (type === "Hill") addHill(count, height, x, y);
else if (type === "Pit") addPit(count, height, x, y);
else if (type === "Range") addRange(count, height, x, y);
else if (type === "Trough") addTrough(count, height, x, y);
else if (type === "Strait") addStrait(count, dist);
else if (type === "Add") modify(dist, +count, 1);
else if (type === "Multiply") modify(dist, 0, +count);
else if (type === "Smooth") smooth(+count);
updateHistory("noStat"); // update history every step
}
@ -880,17 +876,13 @@ function editHeightmap() {
let data = "";
for (const s of steps) {
if (s.style.opacity == 0.5) continue;
if (s.style.opacity === "0.5") continue;
const type = s.getAttribute("data-type");
const elCount = s.querySelector(".templateCount");
const count = elCount ? elCount.value : "0";
const elHeight = s.querySelector(".templateHeight");
const elDist = s.querySelector(".templateDist");
const arg3 = elHeight ? elHeight.value : elDist ? elDist.value : "0";
const templateX = s.querySelector(".templateX");
const x = templateX ? templateX.value : "0";
const templateY = s.querySelector(".templateY");
const y = templateY ? templateY.value : "0";
const count = s.querySelector(".templateCount")?.value || "0";
const arg3 = s.querySelector(".templateHeight")?.value || s.querySelector(".templateDist")?.value || "0";
const x = s.querySelector(".templateX")?.value || "0";
const y = s.querySelector(".templateY")?.value || "0";
data += `${type} ${count} ${arg3} ${x} ${y}\r\n`;
}

View file

@ -1456,6 +1456,8 @@ function toggleRivers(event) {
function drawRivers() {
TIME && console.time("drawRivers");
rivers.selectAll("*").remove();
const {addMeandering, getRiverPath} = Rivers;
lineGen.curve(d3.curveCatmullRom.alpha(0.1));

View file

@ -99,7 +99,9 @@ function showSupporters() {
Dust Bunny,Adrian Wright,Eric Alexander Cartaya,GameNight,Thomas Mortensen Hansen,Zklaus,Drinarius,Ed Wright,Lon Varnadore,Crys Cain,Heaven N Lee,
Jeffrey Henning,Lazer Elf,Jordan Bellah,Alex Beard,Kass Frisson,Petro Lombaard,Emanuel Pietri,Rox,PinkEvil,Gavin Madrigal,Martin Lorber,Prince of Morgoth,
Jaryd Armstrong,Andrew Pirkola,ThyHolyDevil,Gary Smith,Tyshaun Wise,Ethan Cook,Jon Stroman,Nobody679,良义 ,Chris Gray,Phoenix Boatwright,Mackenzie,
"Milo Cohen,Jason Matthew Wuerfel,Rasmus Legêne,Andrew Hines,Wexxler,Espen Sæverud,Binks,Dominick Ormsby,Linn Browning,Václav Švec,Alan Buehne,George J.Lekkas"`;
Milo Cohen,Jason Matthew Wuerfel,Rasmus Legêne,Andrew Hines,Wexxler,Espen Sæverud,Binks,Dominick Ormsby,Linn Browning,Václav Švec,Alan Buehne,
George J.Lekkas,Alexandre Boivin,Tommy Mayfield,Skylar Mangum-Turner,Karen Blythe,Stefan Gugerel,Mike Conley,Xavier privé,Hope You're Well,
Mark Sprietsma,Robert Landry,Nick Mowry"`;
const array = supporters
.replace(/(?:\r\n|\r|\n)/g, "")
@ -621,6 +623,7 @@ document.getElementById("sticked").addEventListener("click", function (event) {
const id = event.target.id;
if (id === "newMapButton") regeneratePrompt();
else if (id === "saveButton") showSavePane();
else if (id === "exportButton") showExportPane();
else if (id === "loadButton") showLoadPane();
else if (id === "zoomReset") resetZoom(1000);
});
@ -654,12 +657,13 @@ function regeneratePrompt() {
}
function showSavePane() {
document.getElementById("showLabels").checked = !hideLabels.checked;
const sharableLinkContainer = document.getElementById("sharableLinkContainer");
sharableLinkContainer.style.display = "none";
$("#saveMapData").dialog({
title: "Save map",
resizable: false,
width: "30em",
width: "27em",
position: {my: "center", at: "center", of: "svg"},
buttons: {
Close: function () {
@ -669,21 +673,21 @@ function showSavePane() {
});
}
// download map data as GeoJSON
function saveGeoJSON() {
alertMessage.innerHTML = `You can export map data in GeoJSON format used in GIS tools such as QGIS.
Check out ${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/GIS-data-export", "wiki-page")} for guidance`;
function copyLinkToClickboard() {
const shrableLink = document.getElementById("sharableLink");
const link = shrableLink.getAttribute("href");
navigator.clipboard.writeText(link).then(() => tip("Link is copied to the clipboard", true, "success", 8000));
}
$("#alert").dialog({
title: "GIS data export",
function showExportPane() {
document.getElementById("showLabels").checked = !hideLabels.checked;
$("#exportMapData").dialog({
title: "Export map data",
resizable: false,
width: "35em",
width: "26em",
position: {my: "center", at: "center", of: "svg"},
buttons: {
Cells: saveGeoJSON_Cells,
Routes: saveGeoJSON_Routes,
Rivers: saveGeoJSON_Rivers,
Markers: saveGeoJSON_Markers,
Close: function () {
$(this).dialog("close");
}
@ -695,7 +699,7 @@ function showLoadPane() {
$("#loadMapData").dialog({
title: "Load map",
resizable: false,
width: "17em",
width: "22em",
position: {my: "center", at: "center", of: "svg"},
buttons: {
Close: function () {

View file

@ -8,9 +8,9 @@ function editRiver(id) {
document.getElementById("toggleCells").dataset.forced = +!layerIsOn("toggleCells");
if (!layerIsOn("toggleCells")) toggleCells();
elSelected = d3.select("#" + id);
elSelected = d3.select("#" + id).on("click", addControlPoint);
tip("Drag control points to change the river course. For major changes please create a new river instead", true);
tip("Drag control points to change the river course. Click on point to remove it. Click on river to add additional control point. For major changes please create a new river instead", true);
debug.append("g").attr("id", "controlCells");
debug.append("g").attr("id", "controlPoints");
@ -19,8 +19,8 @@ function editRiver(id) {
const river = getRiver();
const {cells, points} = river;
const riverPoints = Rivers.getRiverPoints(cells, points);
drawControlPoints(riverPoints, cells);
drawCells(cells, "current");
drawControlPoints(riverPoints);
drawCells(cells);
$("#riverEditor").dialog({
title: "Edit River",
@ -92,37 +92,35 @@ function editRiver(id) {
document.getElementById("riverWidth").value = width;
}
function drawControlPoints(points, cells) {
function drawControlPoints(points) {
debug
.select("#controlPoints")
.selectAll("circle")
.data(points)
.enter()
.append("circle")
.join("circle")
.attr("cx", d => d[0])
.attr("cy", d => d[1])
.attr("r", 0.6)
.attr("data-cell", (d, i) => cells[i])
.attr("data-i", (d, i) => i)
.call(d3.drag().on("start", dragControlPoint));
.call(d3.drag().on("start", dragControlPoint))
.on("click", removeControlPoint);
}
function drawCells(cells, type) {
function drawCells(cells) {
const validCells = [...new Set(cells)].filter(i => pack.cells.i[i]);
debug
.select("#controlCells")
.selectAll(`polygon.${type}`)
.data(cells.filter(i => pack.cells.i[i]))
.selectAll(`polygon`)
.data(validCells)
.join("polygon")
.attr("points", d => getPackPolygon(d))
.attr("class", type);
.attr("points", d => getPackPolygon(d));
}
function dragControlPoint() {
const {i, r, fl} = pack.cells;
const {r, fl} = pack.cells;
const river = getRiver();
const initCell = +this.dataset.cell;
const index = +this.dataset.i;
const {x: x0, y: y0} = d3.event;
const initCell = findCell(x0, y0);
let movedToCell = null;
@ -136,22 +134,18 @@ function editRiver(id) {
this.setAttribute("cy", y);
this.__data__ = [rn(x, 1), rn(y, 1)];
redrawRiver();
drawCells(river.cells);
});
d3.event.on("end", () => {
if (movedToCell) {
this.dataset.cell = movedToCell;
river.cells[index] = movedToCell;
drawCells(river.cells, "current");
if (!r[movedToCell]) {
// swap river data
r[initCell] = 0;
r[movedToCell] = river.i;
const sourceFlux = fl[initCell];
fl[initCell] = fl[movedToCell];
fl[movedToCell] = sourceFlux;
}
if (movedToCell && !r[movedToCell]) {
// swap river data
r[initCell] = 0;
r[movedToCell] = river.i;
const sourceFlux = fl[initCell];
fl[initCell] = fl[movedToCell];
fl[movedToCell] = sourceFlux;
redrawRiver();
}
});
}
@ -159,8 +153,10 @@ function editRiver(id) {
function redrawRiver() {
const river = getRiver();
river.points = debug.selectAll("#controlPoints > *").data();
const {cells, widthFactor, sourceWidth} = river;
const meanderedPoints = Rivers.addMeandering(cells, river.points);
river.cells = river.points.map(([x, y]) => findCell(x, y));
const {widthFactor, sourceWidth} = river;
const meanderedPoints = Rivers.addMeandering(river.cells, river.points);
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
const path = Rivers.getRiverPath(meanderedPoints, widthFactor, sourceWidth);
@ -170,6 +166,27 @@ function editRiver(id) {
if (modules.elevation) showEPForRiver(elSelected.node());
}
function addControlPoint() {
const [x, y] = d3.mouse(this);
const point = [rn(x, 1), rn(y, 1)];
const river = getRiver();
if (!river.points) river.points = debug.selectAll("#controlPoints > *").data();
const index = getSegmentId(river.points, point, 2);
river.points.splice(index, 0, point);
drawControlPoints(river.points);
redrawRiver();
}
function removeControlPoint() {
this.remove();
redrawRiver();
const {cells} = getRiver();
drawCells(cells);
}
function changeName() {
getRiver().name = this.value;
}
@ -244,6 +261,8 @@ function editRiver(id) {
function closeRiverEditor() {
debug.select("#controlPoints").remove();
debug.select("#controlCells").remove();
elSelected.on("click", null);
unselect();
clearMainTip();

View file

@ -136,21 +136,11 @@ function recalculatePopulation() {
function regenerateStates() {
const localSeed = Math.floor(Math.random() * 1e9); // new random seed
Math.random = aleaPRNG(localSeed);
const burgs = pack.burgs.filter(b => b.i && !b.removed);
if (!burgs.length) {
tip("No burgs to generate states. Please create burgs first", false, "error");
return;
}
if (burgs.length < +regionsInput.value) {
tip(`Not enough burgs to generate ${regionsInput.value} states. Will generate only ${burgs.length} states`, false, "warn");
}
// burg local ids sorted by a bit randomized population:
const sorted = burgs
.map((b, i) => [i, b.population * Math.random()])
.sort((a, b) => b[1] - a[1])
.map(b => b[0]);
const capitalsTree = d3.quadtree();
const statesCount = +regionsInput.value;
const burgs = pack.burgs.filter(b => b.i && !b.removed);
if (!burgs.length) return tip("There are no any burgs to generate states. Please create burgs first", false, "error");
if (burgs.length < statesCount) tip(`Not enough burgs to generate ${statesCount} states. Will generate only ${burgs.length} states`, false, "warn");
// turn all old capitals into towns
burgs
@ -167,8 +157,7 @@ function regenerateStates() {
unfog();
// if desired states number is 0
if (regionsInput.value == 0) {
if (!statesCount) {
tip(`Cannot generate zero states. Please check the <i>States Number</i> option`, false, "warn");
pack.states = pack.states.slice(0, 1); // remove all except of neutrals
pack.states[0].diplomacy = []; // clear diplomacy
@ -184,26 +173,34 @@ function regenerateStates() {
return;
}
const neutral = pack.states[0].name;
const count = Math.min(+regionsInput.value, burgs.length);
// burg local ids sorted by a bit randomized population:
const sortedBurgs = burgs
.map((b, i) => [b, b.population * Math.random()])
.sort((a, b) => b[1] - a[1])
.map(b => b[0]);
const capitalsTree = d3.quadtree();
const neutral = pack.states[0].name; // neutrals name
const count = Math.min(statesCount, burgs.length) + 1; // +1 for neutral
let spacing = (graphWidth + graphHeight) / 2 / count; // min distance between capitals
pack.states = d3.range(count).map(i => {
if (!i) return {i, name: neutral};
let capital = null,
x = 0,
y = 0;
for (const i of sorted) {
capital = burgs[i];
(x = capital.x), (y = capital.y);
if (capitalsTree.find(x, y, spacing) === undefined) break;
let capital = null;
for (const burg of sortedBurgs) {
const {x, y} = burg;
if (capitalsTree.find(x, y, spacing) === undefined) {
burg.capital = 1;
capital = burg;
capitalsTree.add([x, y]);
moveBurgToGroup(burg.i, "cities");
break;
}
spacing = Math.max(spacing - 1, 1);
}
capitalsTree.add([x, y]);
capital.capital = 1;
moveBurgToGroup(capital.i, "cities");
const culture = capital.culture;
const basename = capital.name.length < 9 && capital.cell % 5 === 0 ? capital.name : Names.getCulture(culture, 3, 6, "", 0);
const name = Names.getState(basename, culture);

View file

@ -1,13 +1,17 @@
function editWorld() {
if (customization) return;
$("#worldConfigurator").dialog({title: "Configure World", resizable: false, width: "42em",
$("#worldConfigurator").dialog({
title: "Configure World",
resizable: false,
width: "42em",
buttons: {
"Whole World": () => applyWorldPreset(100, 50),
"Northern": () => applyWorldPreset(33, 25),
"Tropical": () => applyWorldPreset(33, 50),
"Southern": () => applyWorldPreset(33, 75),
Northern: () => applyWorldPreset(33, 25),
Tropical: () => applyWorldPreset(33, 50),
Southern: () => applyWorldPreset(33, 75),
"Restore Winds": restoreDefaultWinds
}, open: function() {
},
open: function () {
const buttons = $(this).dialog("widget").find(".ui-dialog-buttonset > button");
buttons[0].addEventListener("mousemove", () => tip("Click to set map size to cover the whole World"));
buttons[1].addEventListener("mousemove", () => tip("Click to set map size to cover the Northern latitudes"));
@ -19,7 +23,8 @@ function editWorld() {
const globe = d3.select("#globe");
const clr = d3.scaleSequential(d3.interpolateSpectral);
const tMax = 30, tMin = -25; // temperature extremes
const tMax = 30,
tMin = -25; // temperature extremes
const projection = d3.geoOrthographic().translate([100, 100]).scale(100);
const path = d3.geoPath(projection);
@ -29,15 +34,15 @@ function editWorld() {
if (modules.editWorld) return;
modules.editWorld = true;
document.getElementById("worldControls").addEventListener("input", (e) => updateWorld(e.target));
document.getElementById("worldControls").addEventListener("input", e => updateWorld(e.target));
globe.select("#globeWindArrows").on("click", changeWind);
globe.select("#globeGraticule").attr("d", round(path(d3.geoGraticule()()))); // globe graticule
updateWindDirections();
function updateWorld(el) {
if (el) {
document.getElementById(el.dataset.stored+"Input").value = el.value;
document.getElementById(el.dataset.stored+"Output").value = el.value;
document.getElementById(el.dataset.stored + "Input").value = el.value;
document.getElementById(el.dataset.stored + "Output").value = el.value;
if (el.dataset.stored) lock(el.dataset.stored);
}
@ -56,16 +61,18 @@ function editWorld() {
if (layerIsOn("togglePrec")) drawPrec();
if (layerIsOn("toggleBiomes")) drawBiomes();
if (layerIsOn("toggleCoordinates")) drawCoordinates();
if (layerIsOn("toggleRivers")) drawRivers();
if (document.getElementById("canvas3d")) setTimeout(ThreeD.update(), 500);
}
function updateGlobePosition() {
const size = +document.getElementById("mapSizeOutput").value;
const eqD = graphHeight / 2 * 100 / size;
const eqD = ((graphHeight / 2) * 100) / size;
calculateMapCoordinates();
const mc = mapCoordinates; // shortcut
const scale = +distanceScaleInput.value, unit = distanceUnitInput.value;
const scale = +distanceScaleInput.value,
unit = distanceUnitInput.value;
const meridian = toKilometer(eqD * 2 * scale);
document.getElementById("mapSize").innerHTML = `${graphWidth}x${graphHeight}`;
document.getElementById("mapSizeFriendly").innerHTML = `${rn(graphWidth * scale)}x${rn(graphHeight * scale)} ${unit}`;
@ -82,27 +89,35 @@ function editWorld() {
return 0; // 0 if distanceUnitInput is a custom unit
}
function lat(lat) {return lat > 0 ? Math.abs(rn(lat)) + "°N" : Math.abs(rn(lat)) + "°S";} // parse latitude value
const area = d3.geoGraticule().extent([[mc.lonW, mc.latN], [mc.lonE, mc.latS]]);
function lat(lat) {
return lat > 0 ? Math.abs(rn(lat)) + "°N" : Math.abs(rn(lat)) + "°S";
} // parse latitude value
const area = d3.geoGraticule().extent([
[mc.lonW, mc.latN],
[mc.lonE, mc.latS]
]);
globe.select("#globeArea").attr("d", round(path(area.outline()))); // map area
}
function updateGlobeTemperature() {
const tEq = +document.getElementById("temperatureEquatorOutput").value;
document.getElementById("temperatureEquatorF").innerHTML = rn(tEq * 9/5 + 32);
document.getElementById("temperatureEquatorF").innerHTML = rn((tEq * 9) / 5 + 32);
const tPole = +document.getElementById("temperaturePoleOutput").value;
document.getElementById("temperaturePoleF").innerHTML = rn(tPole * 9/5 + 32);
document.getElementById("temperaturePoleF").innerHTML = rn((tPole * 9) / 5 + 32);
globe.selectAll(".tempGradient90").attr("stop-color", clr(1 - (tPole - tMin) / (tMax - tMin)));
globe.selectAll(".tempGradient60").attr("stop-color", clr(1 - (tEq - (tEq - tPole) * 2/3 - tMin) / (tMax - tMin)));
globe.selectAll(".tempGradient30").attr("stop-color", clr(1 - (tEq - (tEq - tPole) * 1/3 - tMin) / (tMax - tMin)));
globe.selectAll(".tempGradient60").attr("stop-color", clr(1 - (tEq - ((tEq - tPole) * 2) / 3 - tMin) / (tMax - tMin)));
globe.selectAll(".tempGradient30").attr("stop-color", clr(1 - (tEq - ((tEq - tPole) * 1) / 3 - tMin) / (tMax - tMin)));
globe.select(".tempGradient0").attr("stop-color", clr(1 - (tEq - tMin) / (tMax - tMin)));
}
function updateWindDirections() {
globe.select("#globeWindArrows").selectAll("path").each(function(d, i) {
const tr = parseTransform(this.getAttribute("transform"));
this.setAttribute("transform", `rotate(${options.winds[i]} ${tr[1]} ${tr[2]})`);
});
globe
.select("#globeWindArrows")
.selectAll("path")
.each(function (d, i) {
const tr = parseTransform(this.getAttribute("transform"));
this.setAttribute("transform", `rotate(${options.winds[i]} ${tr[1]} ${tr[2]})`);
});
}
function changeWind() {
@ -112,13 +127,13 @@ function editWorld() {
const tr = parseTransform(arrow.getAttribute("transform"));
arrow.setAttribute("transform", `rotate(${options.winds[tier]} ${tr[1]} ${tr[2]})`);
localStorage.setItem("winds", options.winds);
const mapTiers = d3.range(mapCoordinates.latN, mapCoordinates.latS, -30).map(c => (90-c) / 30 | 0);
const mapTiers = d3.range(mapCoordinates.latN, mapCoordinates.latS, -30).map(c => ((90 - c) / 30) | 0);
if (mapTiers.includes(tier)) updateWorld();
}
function restoreDefaultWinds() {
const defaultWinds = [225, 45, 225, 315, 135, 315];
const mapTiers = d3.range(mapCoordinates.latN, mapCoordinates.latS, -30).map(c => (90-c) / 30 | 0);
const mapTiers = d3.range(mapCoordinates.latN, mapCoordinates.latS, -30).map(c => ((90 - c) / 30) | 0);
const update = mapTiers.some(t => options.winds[t] != defaultWinds[t]);
options.winds = defaultWinds;
updateWindDirections();