This commit is contained in:
Azgaar 2019-09-28 15:10:53 +03:00
parent 729d91c053
commit 35b3e9dd9c
18 changed files with 621 additions and 171 deletions

162
main.js
View file

@ -7,7 +7,7 @@
// See also https://github.com/Azgaar/Fantasy-Map-Generator/issues/153
"use strict";
const version = "1.0"; // generator version
const version = "1.1"; // generator version
document.title += " v" + version;
// if map version is not stored, clear localStorage and show a message
@ -64,8 +64,14 @@ let fogging = viewbox.append("g").attr("id", "fogging-cont").attr("mask", "url(#
let ruler = viewbox.append("g").attr("id", "ruler").attr("display", "none");
let debug = viewbox.append("g").attr("id", "debug");
let freshwater = lakes.append("g").attr("id", "freshwater");
let salt = lakes.append("g").attr("id", "salt");
// lake and coast groups
lakes.append("g").attr("id", "freshwater");
lakes.append("g").attr("id", "salt");
lakes.append("g").attr("id", "sinkhole");
lakes.append("g").attr("id", "frozen");
lakes.append("g").attr("id", "lava");
coastline.append("g").attr("id", "sea_island");
coastline.append("g").attr("id", "lake_island");
labels.append("g").attr("id", "states");
labels.append("g").attr("id", "addedLabels");
@ -321,8 +327,6 @@ function applyDefaultStyle() {
compass.attr("opacity", .8).attr("transform", null).attr("filter", null).attr("mask", "url(#water)").attr("shape-rendering", "optimizespeed");
if (!d3.select("#initial").size()) d3.select("#rose").attr("transform", "translate(80 80) scale(.25)");
coastline.attr("opacity", .5).attr("stroke", "#1f3846").attr("stroke-width", .7).attr("filter", "url(#dropShadow)");
styleCoastlineAuto.checked = true;
relig.attr("opacity", .7).attr("stroke", "#404040").attr("stroke-width", .7).attr("filter", null).attr("fill-rule", "evenodd");
cults.attr("opacity", .6).attr("stroke", "#777777").attr("stroke-width", .5).attr("filter", null).attr("fill-rule", "evenodd");
icons.selectAll("g").attr("opacity", null).attr("fill", "#ffffff").attr("stroke", "#3e3e4b").attr("filter", null).attr("mask", null);
@ -334,8 +338,15 @@ function applyDefaultStyle() {
population.select("#rural").attr("stroke", "#0000ff");
population.select("#urban").attr("stroke", "#ff0000");
freshwater.attr("opacity", .5).attr("fill", "#a6c1fd").attr("stroke", "#5f799d").attr("stroke-width", .7).attr("filter", null);
salt.attr("opacity", .5).attr("fill", "#409b8a").attr("stroke", "#388985").attr("stroke-width", .7).attr("filter", null);
lakes.select("#freshwater").attr("opacity", .5).attr("fill", "#a6c1fd").attr("stroke", "#5f799d").attr("stroke-width", .7).attr("filter", null);
lakes.select("#salt").attr("opacity", .5).attr("fill", "#409b8a").attr("stroke", "#388985").attr("stroke-width", .7).attr("filter", null);
lakes.select("#sinkhole").attr("opacity", 1).attr("fill", "#5bc9fd").attr("stroke", "#53a3b0").attr("stroke-width", .7).attr("filter", null);
lakes.select("#frozen").attr("opacity", .95).attr("fill", "#cdd4e7").attr("stroke", "#cfe0eb").attr("stroke-width", 0).attr("filter", null);
lakes.select("#lava").attr("opacity", .7).attr("fill", "#90270d").attr("stroke", "#f93e0c").attr("stroke-width", 2).attr("filter", "url(#crumpled)");
coastline.select("#sea_island").attr("opacity", .5).attr("stroke", "#1f3846").attr("stroke-width", .7).attr("filter", "url(#dropShadow)");
coastline.select("#lake_island").attr("opacity", 1).attr("stroke", "#7c8eaf").attr("stroke-width", .35).attr("filter", null);
styleCoastlineAuto.checked = true;
terrain.attr("opacity", null).attr("filter", null).attr("mask", null);
rivers.attr("opacity", null).attr("fill", "#5d97bb").attr("filter", null);
@ -404,30 +415,21 @@ function applyDefaultStyle() {
}
function showWelcomeMessage() {
const link = 'https://www.reddit.com/r/FantasyMapGenerator/comments/cxu1c5/update_new_version_is_published_v_10'; // announcement on Reddit
const link = 'https://www.reddit.com/r/FantasyMapGenerator/comments/daf6g2/update_new_version_is_published_v_11'; // announcement on Reddit
alertMessage.innerHTML = `The Fantasy Map Generator is updated up to version <b>${version}</b>.
This version is compatible with versions 0.8b and 0.9b, but not with older .map files.
Please use an <a href='https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog' target='_blank'>archived version</a> to open old files.
This version is compatible with <a href='https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog' target='_blank'>previous version</a>,
loaded <i>.map</i> files will be auto-updated.
<ul><a href=${link} target='_blank'>Main changes:</a>
<li>Provinces and Provinces Editor</li>
<li>Religions Layer and Religions Editor</li>
<li>Full state names (state types)</li>
<li>Multi-lined labels</li>
<li>State relations (diplomacy)</li>
<li>Custom layers (zones)</li>
<li>Places of interest (auto-added markers)</li>
<li>New color picker and hatching fill</li>
<li>Legend boxes</li>
<li>World Configurator presets</li>
<li>Improved state labels placement</li>
<li>Relief icons sets</li>
<li>Fogging</li>
<li>Custom layer presets</li>
<li>Custom biomes</li>
<li>State, province and burg COAs</li>
<li>Desktop version (see <a href='https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Q&A#is-there-a-desktop-version' target='_blank'>here)</a></li>
<li>Lake Editor</li>
<li>Coastline Editor</li>
<li>New lake groups (types)</li>
<li>Culture presets</li>
<li>Provinces, states and burgs charts</li>
<li>Editable religions tree</li>
<li>Data export in geojson format</li>
<li>Map quick save and quick load</li>
<li>Map loading from URL</li>
</ul>
<p>Join our <a href='https://www.reddit.com/r/FantasyMapGenerator' target='_blank'>Reddit community</a> and
@ -437,7 +439,7 @@ function showWelcomeMessage() {
<p>Thanks for all supporters on <a href='https://www.patreon.com/azgaar' target='_blank'>Patreon</a>!</i></p>`;
$("#alert").dialog(
{resizable: false, title: "Fantasy Map Generator update", width: "31em",
{resizable: false, title: "Fantasy Map Generator update", width: "28em",
buttons: {OK: function() {$(this).dialog("close")}},
position: {my: "center", at: "center", of: "svg"},
close: () => localStorage.setItem("version", version)}
@ -487,7 +489,7 @@ function invokeActiveZooming() {
// toggle shade/blur filter for coatline on zoom
let filter = scale > 2.6 ? "url(#blurFilter)" : "url(#dropShadow)";
if (scale > 1.5 && scale <= 2.6) filter = null;
coastline.attr("filter", filter);
coastline.select("#sea_island").attr("filter", filter);
}
// rescale lables on zoom
@ -982,23 +984,29 @@ function drawCoastline() {
const type = features[f].type === "lake" ? 1 : -1; // type value to search for
const start = findStart(i, type);
if (start === -1) continue; // cannot start here
const connectedVertices = connectVertices(start, type);
let vchain = connectVertices(start, type);
if (features[f].type === "lake") relax(vchain, 1.2);
used[f] = 1;
let points = connectedVertices.map(v => vertices.p[v]);
let points = vchain.map(v => vertices.p[v]);
const area = d3.polygonArea(points); // area with lakes/islands
if (area > 0 && features[f].type === "lake") points = points.reverse();
if (area > 0 && features[f].type === "lake") {
points = points.reverse();
vchain = vchain.reverse();
}
features[f].area = Math.abs(area);
features[f].vertices = vchain;
const path = round(lineGen(points));
const id = features[f].group + features[f].i;
if (features[f].type === "lake") {
landMask.append("path").attr("d", path).attr("fill", "black");
// waterMask.append("path").attr("d", path).attr("fill", "white"); // uncomment to show over lakes
lakes.select("#"+features[f].group).append("path").attr("d", path).attr("id", id); // draw the lake
landMask.append("path").attr("d", path).attr("fill", "black").attr("id", "land_"+f);
// waterMask.append("path").attr("d", path).attr("fill", "white").attr("id", "water_"+id); // uncomment to show over lakes
lakes.select("#"+features[f].group).append("path").attr("d", path).attr("id", "lake_"+f).attr("data-f", f); // draw the lake
} else {
landMask.append("path").attr("d", path).attr("fill", "white");
waterMask.append("path").attr("d", path).attr("fill", "black");
coastline.append("path").attr("d", path).attr("id", id); // draw the coastline
landMask.append("path").attr("d", path).attr("fill", "white").attr("id", "land_"+f);
waterMask.append("path").attr("d", path).attr("fill", "black").attr("id", "water_"+f);
const g = features[f].group === "lake_island" ? "lake_island" : "sea_island";
coastline.select("#"+g).append("path").attr("d", path).attr("id", "island_"+f).attr("data-f", f); // draw the coastline
}
// draw ruler to cover the biggest land piece
@ -1034,10 +1042,28 @@ function drawCoastline() {
if (v[2] !== prev && c0 !== c2) current = v[2];
if (current === chain[chain.length-1]) {console.error("Next vertex is not found"); break;}
}
chain.push(chain[0]); // push first vertex as the last one
//chain.push(chain[0]); // push first vertex as the last one
return chain;
}
// move vertices that are too close to already added ones
function relax(vchain, r) {
const p = vertices.p, tree = d3.quadtree();
for (let i=0; i < vchain.length; i++) {
const v = vchain[i];
let [x, y] = [p[v][0], p[v][1]];
if (i && vchain[i+1] && tree.find(x, y, r) !== undefined) {
const v1 = vchain[i-1], v2 = vchain[i+1];
const [x1, y1] = [p[v1][0], p[v1][1]];
const [x2, y2] = [p[v2][0], p[v2][1]];
[x, y] = [(x1 + x2) / 2, (y1 + y2) / 2];
p[v] = [x, y];
}
tree.add([x, y]);
}
}
console.timeEnd('drawCoastline');
}
@ -1045,18 +1071,17 @@ function drawCoastline() {
function reMarkFeatures() {
console.time("reMarkFeatures");
const cells = pack.cells, features = pack.features = [0];
const continentCells = grid.cells.i.length / 10, islandCell = continentCells / 50;
cells.f = new Uint16Array(cells.i.length); // cell feature number
cells.t = new Int8Array(cells.i.length); // cell type: 1 = land along coast; -1 = water along coast;
cells.haven = new Uint16Array(cells.i.length); // cell haven (opposite water cell);
cells.harbor = new Uint16Array(cells.i.length); // cell harbor (number of adjacent water cells);
for (let i=1, queue=[0]; queue[0] !== -1; i++) {
cells.f[queue[0]] = i; // feature number
const land = cells.h[queue[0]] >= 20;
const start = queue[0]; // first cell
cells.f[start] = i; // assign feature number
const land = cells.h[start] >= 20;
let border = false; // true if feature touches map border
let cellNumber = 1; // to count cells number in a feature
const temp = grid.cells.temp[cells.g[queue[0]]]; // first cell temparature
while (queue.length) {
const q = queue.pop();
@ -1082,13 +1107,30 @@ function reMarkFeatures() {
const type = land ? "island" : border ? "ocean" : "lake";
let group;
if (type === "lake") group = temp < 25 ? "freshwater" : "salt"; else
if (type === "ocean") group = "ocean"; else
if (type === "island") group = cellNumber > continentCells ? "continent" : cellNumber > islandCell ? "island" : "isle";
features.push({i, land, border, type, cells: cellNumber, group});
if (type === "lake") group = defineLakeGroup(start, cellNumber);
else if (type === "ocean") group = "ocean";
else if (type === "island") group = defineIslandGroup(start, cellNumber);
features.push({i, land, border, type, cells: cellNumber, firstCell: start, group});
queue[0] = cells.f.findIndex(f => !f); // find unmarked cell
}
function defineLakeGroup(cell, number) {
const temp = grid.cells.temp[cells.g[cell]];
if (temp > 24) return "salt";
if (temp < -3) return "frozen";
const height = d3.max(cells.c[cell].map(c => cells.h[c]));
if (height > 69 && number < 3 && cell%5 === 0) return "sinkhole";
if (height > 69 && number < 10 && cell%5 === 0) return "lava";
return "freshwater";
}
function defineIslandGroup(cell, number) {
if (cell && features[cells.f[cell-1]].type === "lake") return "lake_island";
if (number > grid.cells.i.length / 10) return "continent";
if (number > grid.cells.i.length / 1000) return "island";
return "isle";
}
console.timeEnd("reMarkFeatures");
}
@ -1128,17 +1170,17 @@ function defineBiomes() {
cells.biome[i] = getBiomeId(moist, temp, cells.h[i]);
}
function getBiomeId(moisture, temperature, height) {
if (temperature < -5) return 11; // permafrost biome
if (moisture > 40 && height < 25 || moisture > 24 && height > 24) return 12; // wetland biome
const m = Math.min(moisture / 5 | 0, 4); // moisture band from 0 to 4
const t = Math.min(Math.max(20 - temperature, 0), 25); // temparature band from 0 to 25
return biomesData.biomesMartix[m][t];
}
console.timeEnd("defineBiomes");
}
function getBiomeId(moisture, temperature, height) {
if (temperature < -5) return 11; // permafrost biome
if (moisture > 40 && height < 25 || moisture > 24 && height > 24) return 12; // wetland biome
const m = Math.min(moisture / 5 | 0, 4); // moisture band from 0 to 4
const t = Math.min(Math.max(20 - temperature, 0), 25); // temparature band from 0 to 25
return biomesData.biomesMartix[m][t];
}
// assess cells suitability to calculate population and rand cells for culture center and burgs placement
function rankCells() {
console.time('rankCells');
@ -1160,7 +1202,9 @@ function rankCells() {
const type = f[cells.f[cells.haven[i]]].type;
const group = f[cells.f[cells.haven[i]]].group;
if (type === "lake") {
if (group === "salt") s += 10; else s += 30; // lake coast is valued
// lake coast is valued
if (group === "freshwater") s += 30;
else if (group !== "lava") s += 10;
} else {
s += 5; // ocean coast is valued
if (cells.harbor[i] === 1) s += 20; // safe sea harbor is valued
@ -1399,7 +1443,7 @@ function addZones(number = 1) {
for (let i=0; i < rn(Math.random() * 1.2 * number); i++) addTsunami() // tsunami starting near coast
function addInvasion() {
const atWar = states.filter(s => s.diplomacy.some(d => d === "Enemy"));
const atWar = states.filter(s => s.diplomacy && s.diplomacy.some(d => d === "Enemy"));
if (!atWar.length) return;
const invader = ra(atWar);