diff --git a/main.js b/main.js
index a7a7ff57..f014a821 100644
--- a/main.js
+++ b/main.js
@@ -109,7 +109,7 @@ scaleBar.on("mousemove", () => tip("Click to open Units Editor")).on("click", ()
legend.on("mousemove", () => tip("Drag to change the position. Click to hide the legend")).on("click", () => clearLegend());
// main data variables
-let grid = {}; // initial grapg based on jittered square grid and data
+let grid = {}; // initial graph based on jittered square grid and data
let pack = {}; // packed graph and data
let seed;
let mapId;
@@ -161,7 +161,7 @@ let urbanDensity = +document.getElementById("urbanDensityInput").value;
applyStoredOptions();
-// voronoi graph extention, cannot be changed arter generation
+// voronoi graph extension, cannot be changed after generation
let graphWidth = +mapWidthInput.value;
let graphHeight = +mapHeightInput.value;
@@ -493,7 +493,7 @@ function invokeActiveZooming() {
coastline.select("#sea_island").attr("filter", filter);
}
- // rescale lables on zoom
+ // rescale labels on zoom
if (labels.style("display") !== "none") {
labels.selectAll("g").each(function () {
if (this.id === "burgLabels") return;
@@ -668,7 +668,7 @@ function generate() {
const parsedError = parseError(error);
clearMainTip();
- alertMessage.innerHTML = `An error is occured on map generation. Please retry.
+ alertMessage.innerHTML = `An error has occurred on map generation. Please retry.
If error is critical, clear the stored data and try again.
${parsedError}
`;
$("#alert").dialog({
@@ -909,7 +909,7 @@ function defineMapSize() {
const template = document.getElementById("templateInput").value; // heightmap template
const part = grid.features.some(f => f.land && f.border); // if land goes over map borders
const max = part ? 80 : 100; // max size
- const lat = () => gauss(P(0.5) ? 40 : 60, 15, 25, 75); // latiture shift
+ const lat = () => gauss(P(0.5) ? 40 : 60, 15, 25, 75); // latitude shift
if (!part) {
if (template === "Pangea") return [100, 50];
@@ -981,7 +981,10 @@ function generatePrecipitation() {
prec.selectAll("*").remove();
const {cells, cellsX, cellsY} = grid;
cells.prec = new Uint8Array(cells.i.length); // precipitation array
- const modifier = precInput.value / 100; // user's input
+
+ const cellsNumberModifier = (pointsInput.dataset.cells / 10000) ** 0.25;
+ const precInputModifier = precInput.value / 100;
+ const modifier = cellsNumberModifier * precInputModifier;
const westerly = [];
const easterly = [];
@@ -997,14 +1000,14 @@ function generatePrecipitation() {
// x2 = 60-70 latitude: wet summer (rising zone), dry winter (sinking zone)
// x1 = 70-85 latitude: dry all year (sinking zone)
// x0.5 = 85-90 latitude: dry all year (sinking zone)
- const lalitudeModifier = [4, 2, 2, 2, 1, 1, 2, 2, 2, 2, 3, 3, 2, 2, 1, 1, 1, 0.5];
+ const latitudeModifier = [4, 2, 2, 2, 1, 1, 2, 2, 2, 2, 3, 3, 2, 2, 1, 1, 1, 0.5];
const MAX_PASSABLE_ELEVATION = 85;
// define wind directions based on cells latitude and prevailing winds there
d3.range(0, cells.i.length, cellsX).forEach(function (c, i) {
const lat = mapCoordinates.latN - (i / cellsY) * mapCoordinates.latT;
const latBand = ((Math.abs(lat) - 1) / 5) | 0;
- const latMod = lalitudeModifier[latBand];
+ const latMod = latitudeModifier[latBand];
const windTier = (Math.abs(lat - 89) / 30) | 0; // 30d tiers from 0 to 5 from N to S
const {isWest, isEast, isNorth, isSouth} = getWindDirections(windTier);
@@ -1021,14 +1024,14 @@ function generatePrecipitation() {
const vertT = southerly + northerly;
if (northerly) {
const bandN = ((Math.abs(mapCoordinates.latN) - 1) / 5) | 0;
- const latModN = mapCoordinates.latT > 60 ? d3.mean(lalitudeModifier) : lalitudeModifier[bandN];
+ const latModN = mapCoordinates.latT > 60 ? d3.mean(latitudeModifier) : latitudeModifier[bandN];
const maxPrecN = (northerly / vertT) * 60 * modifier * latModN;
passWind(d3.range(0, cellsX, 1), maxPrecN, cellsX, cellsY);
}
if (southerly) {
const bandS = ((Math.abs(mapCoordinates.latS) - 1) / 5) | 0;
- const latModS = mapCoordinates.latT > 60 ? d3.mean(lalitudeModifier) : lalitudeModifier[bandS];
+ const latModS = mapCoordinates.latT > 60 ? d3.mean(latitudeModifier) : latitudeModifier[bandS];
const maxPrecS = (southerly / vertT) * 60 * modifier * latModS;
passWind(d3.range(cells.i.length - cellsX, cells.i.length, 1), maxPrecS, -cellsX, cellsY);
}
@@ -1054,7 +1057,7 @@ function generatePrecipitation() {
}
let humidity = maxPrec - cells.h[first]; // initial water amount
- if (humidity <= 0) continue; // if first cell in row is too elevated cosdired wind dry
+ if (humidity <= 0) continue; // if first cell in row is too elevated consider wind dry
for (let s = 0, current = first; s < steps; s++, current += next) {
if (cells.temp[current] < -5) continue; // no flux in permafrost
@@ -1182,7 +1185,7 @@ function reGraph() {
TIME && console.timeEnd("reGraph");
}
-// Detect and draw the coasline
+// Detect and draw the coastline
function drawCoastline() {
TIME && console.time("drawCoastline");
reMarkFeatures();
@@ -1191,7 +1194,7 @@ function drawCoastline() {
vertices = pack.vertices,
n = cells.i.length,
features = pack.features;
- const used = new Uint8Array(features.length); // store conneted features
+ const used = new Uint8Array(features.length); // store connected features
const largestLand = d3.scan(
features.map(f => (f.land ? f.cells : 0)),
(a, b) => b - a
@@ -1395,6 +1398,12 @@ function reMarkFeatures() {
TIME && console.timeEnd("reMarkFeatures");
}
+function isWetLand(moisture, temperature, height) {
+ if (moisture > 40 && temperature > -2 && height < 25) return true; //near coast
+ if (moisture > 24 && temperature > -2 && height > 24 && height < 60) return true; //off coast
+ return false;
+}
+
// assign biome id for each cell
function defineBiomes() {
TIME && console.time("defineBiomes");
@@ -1427,7 +1436,7 @@ function defineBiomes() {
function getBiomeId(moisture, temperature, height) {
if (height < 20) return 0; // marine biome: all water cells
if (temperature < -5) return 11; // permafrost biome
- if (moisture > 40 && temperature > -2 && (height < 25 || (moisture > 24 && height > 24 && height < 60))) return 12; // wetland biome
+ if (isWetLand(moisture, temperature, height)) return 12; // wetland biome
const moistureBand = Math.min((moisture / 5) | 0, 4); // [0-4]
const temperatureBand = Math.min(Math.max(20 - temperature, 0), 25); // [0-25]
diff --git a/modules/burgs-and-states.js b/modules/burgs-and-states.js
index 09d70f2d..865159aa 100644
--- a/modules/burgs-and-states.js
+++ b/modules/burgs-and-states.js
@@ -31,7 +31,7 @@ window.BurgsAndStates = (function () {
function placeCapitals() {
TIME && console.time("placeCapitals");
- let count = +regionsInput.value;
+ let count = +regionsOutput.value;
let burgs = [0];
const rand = () => 0.5 + Math.random() * 0.5;
@@ -240,7 +240,7 @@ window.BurgsAndStates = (function () {
b.citadel = b.capital || (pop > 50 && P(0.75)) || P(0.5) ? 1 : 0;
b.plaza = pop > 50 || (pop > 30 && P(0.75)) || (pop > 10 && P(0.5)) || P(0.25) ? 1 : 0;
b.walls = b.capital || pop > 30 || (pop > 20 && P(0.75)) || (pop > 10 && P(0.5)) || P(0.2) ? 1 : 0;
- b.shanty = pop > 30 || (pop > 20 && P(0.75)) || (b.walls && P(0.75)) ? 1 : 0;
+ b.shanty = pop > 60 || (pop > 40 && P(0.75)) || (pop > 20 && b.walls && P(0.4)) ? 1 : 0;
const religion = cells.religion[b.cell];
const theocracy = pack.states[b.state].form === "Theocracy";
b.temple = (religion && theocracy) || pop > 50 || (pop > 35 && P(0.75)) || (pop > 20 && P(0.5)) ? 1 : 0;
@@ -726,7 +726,7 @@ window.BurgsAndStates = (function () {
TIME && console.time("assignColors");
const colors = ["#66c2a5", "#fc8d62", "#8da0cb", "#e78ac3", "#a6d854", "#ffd92f"]; // d3.schemeSet2;
- // assin basic color using greedy coloring algorithm
+ // assign basic color using greedy coloring algorithm
pack.states.forEach(s => {
if (!s.i || s.removed) return;
const neibs = s.neighbors;
@@ -962,12 +962,12 @@ window.BurgsAndStates = (function () {
const republic = {
Republic: 75,
Federation: 4,
- Oligarchy: 2,
+ "Trade Company": 4,
"Most Serene Republic": 2,
+ Oligarchy: 2,
Tetrarchy: 1,
Triumvirate: 1,
Diarchy: 1,
- "Trade Company": 4,
Junta: 1
}; // weighted random
const union = {Union: 3, League: 4, Confederation: 1, "United Kingdom": 1, "United Republic": 1, "United Provinces": 2, Commonwealth: 1, Heptarchy: 1}; // weighted random
@@ -997,7 +997,7 @@ window.BurgsAndStates = (function () {
const form = monarchy[tier];
// Default name depends on exponent tier, some culture bases have special names for tiers
if (s.diplomacy) {
- if (form === "Duchy" && s.neighbors.length > 1 && rand(6) < s.neighbors.length && s.diplomacy.includes("Vassal")) return "Marches"; // some vassal dutchies on borderland
+ if (form === "Duchy" && s.neighbors.length > 1 && rand(6) < s.neighbors.length && s.diplomacy.includes("Vassal")) return "Marches"; // some vassal duchies on borderland
if (base === 1 && P(0.3) && s.diplomacy.includes("Vassal")) return "Dominion"; // English vassals
if (P(0.3) && s.diplomacy.includes("Vassal")) return "Protectorate"; // some vassals
}
@@ -1037,7 +1037,12 @@ window.BurgsAndStates = (function () {
if (tier < 2 && P(0.5)) return "Diocese";
if (tier < 2 && P(0.5)) return "Bishopric";
}
- if (tier < 2 && P(0.9) && [7, 5].includes(base)) return "Eparchy"; // Greek, Ruthenian
+ if (P(0.9) && [7, 5].includes(base)) {
+ // Greek, Ruthenian
+ if (tier < 2) return "Eparchy";
+ if (tier === 2) return "Exarchate";
+ if (tier > 2) return "Patriarchate";
+ }
if (P(0.9) && [21, 16].includes(base)) return "Imamah"; // Nigerian, Turkish
if (tier > 2 && P(0.8) && [18, 17, 28].includes(base)) return "Caliphate"; // Arabic, Berber, Swahili
return rw(theocracy);
@@ -1093,7 +1098,7 @@ window.BurgsAndStates = (function () {
const max = percentage == 100 ? 1000 : gauss(20, 5, 5, 100) * percentage ** 0.5; // max growth
const forms = {
- Monarchy: {County: 11, Earldom: 3, Shire: 1, Landgrave: 1, Margrave: 1, Barony: 1},
+ Monarchy: {County: 22, Earldom: 6, Shire: 2, Landgrave: 2, Margrave: 2, Barony: 2, Captaincy: 1, Seneschalty: 1},
Republic: {Province: 6, Department: 2, Governorate: 2, District: 1, Canton: 1, Prefecture: 1},
Theocracy: {Parish: 3, Deanery: 1},
Union: {Province: 1, State: 1, Canton: 1, Republic: 1, County: 1, Council: 1},
diff --git a/modules/export.js b/modules/export.js
index 4a9abd9f..7b88a7e6 100644
--- a/modules/export.js
+++ b/modules/export.js
@@ -405,8 +405,8 @@ function saveGeoJSON_Cells() {
json.features.push(feature);
});
- const name = getFileName("Cells") + ".geojson";
- downloadFile(JSON.stringify(json), name, "application/json");
+ const fileName = getFileName("Cells") + ".geojson";
+ downloadFile(JSON.stringify(json), fileName, "application/json");
}
function saveGeoJSON_Routes() {
@@ -421,30 +421,25 @@ function saveGeoJSON_Routes() {
json.features.push(feature);
});
- const name = getFileName("Routes") + ".geojson";
- downloadFile(JSON.stringify(json), name, "application/json");
+ const fileName = getFileName("Routes") + ".geojson";
+ downloadFile(JSON.stringify(json), fileName, "application/json");
}
function saveGeoJSON_Rivers() {
const json = {type: "FeatureCollection", features: []};
rivers.selectAll("path").each(function () {
- const coordinates = getRiverPoints(this);
- const id = this.id;
- const width = +this.dataset.increment;
- const increment = +this.dataset.increment;
- const river = pack.rivers.find(r => r.i === +id.slice(5));
- const name = river ? river.name : "";
- const type = river ? river.type : "";
- const i = river ? river.i : "";
- const basin = river ? river.basin : "";
+ const river = pack.rivers.find(r => r.i === +this.id.slice(5));
+ if (!river) return;
- const feature = {type: "Feature", geometry: {type: "LineString", coordinates}, properties: {id, i, basin, name, type, width, increment}};
+ const coordinates = getRiverPoints(this);
+ const properties = {...river, id: this.id};
+ const feature = {type: "Feature", geometry: {type: "LineString", coordinates}, properties};
json.features.push(feature);
});
- const name = getFileName("Rivers") + ".geojson";
- downloadFile(JSON.stringify(json), name, "application/json");
+ const fileName = getFileName("Rivers") + ".geojson";
+ downloadFile(JSON.stringify(json), fileName, "application/json");
}
function saveGeoJSON_Markers() {
diff --git a/modules/load.js b/modules/load.js
index ad3211f3..c6102d37 100644
--- a/modules/load.js
+++ b/modules/load.js
@@ -828,6 +828,7 @@ function parseLoadedData(data) {
// v 1.65 changed rivers data
d3.select("#rivers").attr("style", null); // remove style to unhide layer
const {cells, rivers} = pack;
+ const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2);
for (const river of rivers) {
const node = document.getElementById("river" + river.i);
@@ -856,7 +857,7 @@ function parseLoadedData(data) {
river.points = riverPoints;
}
- river.widthFactor = 1;
+ river.widthFactor = defaultWidthFactor;
cells.i.forEach(i => {
const riverInWater = cells.r[i] && cells.h[i] < 20;
@@ -1013,6 +1014,31 @@ function parseLoadedData(data) {
ERROR && console.error("Data Integrity Check. Province", p.i, "is linked to removed state", p.state);
p.removed = true; // remove incorrect province
});
+
+ {
+ const markerIds = [];
+ let nextId = last(pack.markers)?.i + 1 || 0;
+
+ pack.markers.forEach(marker => {
+ if (markerIds[marker.i]) {
+ ERROR && console.error("Data Integrity Check. Marker", marker.i, "has non-unique id. Changing to", nextId);
+
+ const domElements = document.querySelectorAll("#marker" + marker.i);
+ if (domElements[1]) domElements[1].id = "marker" + nextId; // rename 2nd dom element
+
+ const noteElements = notes.filter(note => note.id === "marker" + marker.i);
+ if (noteElements[1]) noteElements[1].id = "marker" + nextId; // rename 2nd note
+
+ marker.i = nextId;
+ nextId += 1;
+ } else {
+ markerIds[marker.i] = true;
+ }
+ });
+
+ // sort markers by index
+ pack.markers.sort((a, b) => a.i - b.i);
+ }
})();
changeMapSize();
diff --git a/modules/markers-generator.js b/modules/markers-generator.js
index b60c3599..1aefca5e 100644
--- a/modules/markers-generator.js
+++ b/modules/markers-generator.js
@@ -97,7 +97,7 @@ window.Markers = (function () {
}
function addMarker({cell, type, icon, dx, dy, px}) {
- const i = pack.markers.length;
+ const i = last(pack.markers)?.i + 1 || 0;
const [x, y] = getMarkerCoordinates(cell);
const marker = {i, icon, type, x, y, cell};
if (dx) marker.dx = dx;
diff --git a/modules/religions-generator.js b/modules/religions-generator.js
index b0b0dae2..abe168ef 100644
--- a/modules/religions-generator.js
+++ b/modules/religions-generator.js
@@ -30,63 +30,59 @@ window.Religions = (function () {
const base = {
number: ["One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve"],
being: [
- "God",
- "Goddess",
- "Lord",
- "Lady",
- "Deity",
- "Creator",
- "Maker",
- "Overlord",
- "Ruler",
- "Chief",
- "Master",
- "Spirit",
"Ancestor",
+ "Ancient",
+ "Brother",
+ "Chief",
+ "Council",
+ "Creator",
+ "Deity",
+ "Elder",
"Father",
"Forebear",
"Forefather",
- "Mother",
- "Brother",
- "Sister",
- "Elder",
- "Numen",
- "Ancient",
- "Virgin",
"Giver",
- "Council",
+ "God",
+ "Goddess",
"Guardian",
- "Reaper"
+ "Lady",
+ "Lord",
+ "Maker",
+ "Master",
+ "Mother",
+ "Numen",
+ "Overlord",
+ "Reaper",
+ "Ruler",
+ "Sister",
+ "Spirit",
+ "Virgin"
],
animal: [
- "Dragon",
- "Wyvern",
- "Phoenix",
- "Unicorn",
- "Sphinx",
- "Centaur",
- "Pegasus",
- "Kraken",
- "Basilisk",
- "Chimera",
- "Cyclope",
"Antelope",
"Ape",
"Badger",
+ "Basilisk",
"Bear",
"Beaver",
"Bison",
"Boar",
"Buffalo",
+ "Camel",
"Cat",
+ "Centaur",
+ "Chimera",
"Cobra",
"Crane",
"Crocodile",
"Crow",
+ "Cyclope",
"Deer",
"Dog",
+ "Dragon",
"Eagle",
"Elk",
+ "Falcon",
"Fox",
"Goat",
"Goose",
@@ -94,10 +90,12 @@ window.Religions = (function () {
"Hawk",
"Heron",
"Horse",
+ "Hound",
"Hyena",
"Ibis",
"Jackal",
"Jaguar",
+ "Kraken",
"Lark",
"Leopard",
"Lion",
@@ -107,177 +105,179 @@ window.Religions = (function () {
"Mule",
"Narwhal",
"Owl",
+ "Ox",
"Panther",
+ "Pegasus",
+ "Phoenix",
"Rat",
"Raven",
"Rook",
"Scorpion",
+ "Serpent",
"Shark",
"Sheep",
"Snake",
+ "Sphinx",
"Spider",
"Swan",
"Tiger",
"Turtle",
+ "Unicorn",
"Viper",
"Vulture",
"Walrus",
"Wolf",
"Wolverine",
"Worm",
- "Camel",
- "Falcon",
- "Hound",
- "Ox",
- "Serpent"
+ "Wyvern"
],
adjective: [
- "New",
- "Good",
- "High",
- "Old",
- "Great",
- "Big",
- "Young",
- "Major",
- "Strong",
- "Happy",
- "Last",
- "Main",
- "Huge",
- "Far",
- "Beautiful",
- "Wild",
- "Fair",
- "Prime",
- "Crazy",
- "Ancient",
- "Proud",
- "Secret",
- "Lucky",
- "Sad",
- "Silent",
- "Latter",
- "Severe",
- "Fat",
- "Holy",
- "Pure",
"Aggressive",
- "Honest",
- "Giant",
- "Mad",
- "Pregnant",
- "Distant",
- "Lost",
- "Broken",
+ "Almighty",
+ "Ancient",
+ "Beautiful",
+ "Benevolent",
+ "Big",
"Blind",
- "Friendly",
- "Unknown",
- "Sleeping",
- "Slumbering",
- "Loud",
- "Hungry",
- "Wise",
- "Worried",
- "Sacred",
- "Magical",
- "Superior",
- "Patient",
+ "Blond",
+ "Bloody",
+ "Brave",
+ "Broken",
+ "Brutal",
+ "Burning",
+ "Calm",
+ "Cheerful",
+ "Crazy",
+ "Cruel",
"Dead",
"Deadly",
- "Peaceful",
- "Grateful",
- "Frozen",
- "Evil",
- "Scary",
- "Burning",
- "Divine",
- "Bloody",
- "Dying",
- "Waking",
- "Brutal",
- "Unhappy",
- "Calm",
- "Cruel",
- "Favorable",
- "Blond",
- "Explicit",
- "Disturbing",
"Devastating",
- "Brave",
- "Sunny",
- "Troubled",
- "Flying",
- "Sustainable",
- "Marine",
- "Fatal",
- "Inherent",
- "Selected",
- "Naval",
- "Cheerful",
- "Almighty",
- "Benevolent",
+ "Distant",
+ "Disturbing",
+ "Divine",
+ "Dying",
"Eternal",
+ "Evil",
+ "Explicit",
+ "Fair",
+ "Far",
+ "Fat",
+ "Fatal",
+ "Favorable",
+ "Flying",
+ "Friendly",
+ "Frozen",
+ "Giant",
+ "Good",
+ "Grateful",
+ "Great",
+ "Happy",
+ "High",
+ "Holy",
+ "Honest",
+ "Huge",
+ "Hungry",
"Immutable",
- "Infallible"
+ "Infallible",
+ "Inherent",
+ "Last",
+ "Latter",
+ "Lost",
+ "Loud",
+ "Lucky",
+ "Mad",
+ "Magical",
+ "Main",
+ "Major",
+ "Marine",
+ "Naval",
+ "New",
+ "Old",
+ "Patient",
+ "Peaceful",
+ "Pregnant",
+ "Prime",
+ "Proud",
+ "Pure",
+ "Sacred",
+ "Sad",
+ "Scary",
+ "Secret",
+ "Selected",
+ "Severe",
+ "Silent",
+ "Sleeping",
+ "Slumbering",
+ "Strong",
+ "Sunny",
+ "Superior",
+ "Sustainable",
+ "Troubled",
+ "Unhappy",
+ "Unknown",
+ "Waking",
+ "Wild",
+ "Wise",
+ "Worried",
+ "Young"
],
genitive: [
- "Day",
- "Life",
- "Death",
- "Night",
- "Home",
- "Fog",
- "Snow",
- "Winter",
- "Summer",
"Cold",
- "Springs",
- "Gates",
- "Nature",
- "Thunder",
- "Lightning",
- "War",
- "Ice",
- "Frost",
- "Fire",
+ "Day",
+ "Death",
"Doom",
"Fate",
- "Pain",
+ "Fire",
+ "Fog",
+ "Frost",
+ "Gates",
"Heaven",
+ "Home",
+ "Ice",
"Justice",
+ "Life",
"Light",
+ "Lightning",
"Love",
+ "Nature",
+ "Night",
+ "Pain",
+ "Snow",
+ "Springs",
+ "Summer",
+ "Thunder",
"Time",
- "Victory"
+ "Victory",
+ "War",
+ "Winter"
],
theGenitive: [
- "World",
- "Word",
- "South",
- "West",
- "North",
- "East",
- "Sun",
- "Moon",
- "Peak",
- "Fall",
- "Dawn",
- "Eclipse",
"Abyss",
"Blood",
- "Tree",
+ "Dawn",
"Earth",
+ "East",
+ "Eclipse",
+ "Fall",
"Harvest",
+ "Moon",
+ "North",
+ "Peak",
"Rainbow",
"Sea",
"Sky",
+ "South",
"Stars",
"Storm",
+ "Sun",
+ "Tree",
"Underworld",
- "Wild"
+ "West",
+ "Wild",
+ "Word",
+ "World"
],
- color: ["Dark", "Light", "Bright", "Golden", "White", "Black", "Red", "Pink", "Purple", "Blue", "Green", "Yellow", "Amber", "Orange", "Brown", "Grey"]
+ color: ["Amber", "Black", "Blue", "Bright", "Brown", "Dark", "Golden", "Green", "Grey", "Light", "Orange", "Pink", "Purple", "Red", "White", "Yellow"]
};
const forms = {
@@ -308,10 +308,10 @@ window.Religions = (function () {
Monotheism: {Religion: 1, Church: 1},
"Non-theism": {Beliefs: 3, Spirits: 1},
- Cult: {Cult: 4, Sect: 4, Worship: 1, Orden: 1, Coterie: 1, Arcanum: 1},
- "Dark Cult": {Cult: 2, Sect: 2, Occultism: 1, Idols: 1, Coven: 1, Circle: 1, Blasphemy: 1},
+ Cult: {Cult: 4, Sect: 4, Arcanum: 1, Coterie: 1, Order: 1, Worship: 1},
+ "Dark Cult": {Cult: 2, Sect: 2, Blasphemy: 1, Circle: 1, Coven: 1, Idols: 1, Occultism: 1},
- Heresy: {Heresy: 3, Sect: 2, Schism: 1, Dissenters: 1, Circle: 1, Brotherhood: 1, Society: 1, Iconoclasm: 1, Dissent: 1, Apostates: 1}
+ Heresy: {Heresy: 3, Sect: 2, Apostates: 1, Brotherhood: 1, Circle: 1, Dissent: 1, Dissenters: 1, Iconoclasm: 1, Schism: 1, Society: 1}
};
const generate = function () {
diff --git a/modules/river-generator.js b/modules/river-generator.js
index 752c84a2..59a27468 100644
--- a/modules/river-generator.js
+++ b/modules/river-generator.js
@@ -23,10 +23,14 @@ window.Rivers = (function () {
resolveDepressions(h);
drainWater();
defineRivers();
+
calculateConfluenceFlux();
Lakes.cleanupLakeData();
- if (allowErosion) cells.h = Uint8Array.from(h); // apply changed heights as basic one
+ if (allowErosion) {
+ cells.h = Uint8Array.from(h); // apply gradient
+ downcutRivers(); // downcut river beds
+ }
TIME && console.timeEnd("generateRivers");
@@ -34,6 +38,8 @@ window.Rivers = (function () {
const pixel2 = distanceScale * distanceScale
//const MIN_FLUX_TO_FORM_RIVER = 10 * distanceScale;
const MIN_FLUX_TO_FORM_RIVER = 30;
+ const cellsNumberModifier = (pointsInput.dataset.cells / 10000) ** 0.25;
+
const prec = grid.cells.prec;
// const area = c => pack.cells.area[c] * pixel2;
const area = pack.cells.area;
@@ -41,7 +47,7 @@ window.Rivers = (function () {
const lakeOutCells = Lakes.setClimateData(h);
land.forEach(function (i) {
- cells.fl[i] += (prec[cells.g[i]] * area[i]) / 100; // add flux from precipitation
+ cells.fl[i] += prec[cells.g[i]] / cellsNumberModifier; // add flux from precipitation
// create lake outlet if lake is not in deep depression and flux > evaporation
const lakes = lakeOutCells[i] ? features.filter(feature => i === feature.outCell && feature.flux > feature.evaporation) : [];
@@ -93,6 +99,15 @@ window.Rivers = (function () {
// cells is depressed
if (h[i] <= h[min]) return;
+ // debug
+ // .append("line")
+ // .attr("x1", pack.cells.p[i][0])
+ // .attr("y1", pack.cells.p[i][1])
+ // .attr("x2", pack.cells.p[min][0])
+ // .attr("y2", pack.cells.p[min][1])
+ // .attr("stroke", "#333")
+ // .attr("stroke-width", 0.2);
+
if (cells.fl[i] < MIN_FLUX_TO_FORM_RIVER) {
// flux is too small to operate as a river
if (h[min] >= 20) cells.fl[min] += cells.fl[i];
@@ -152,6 +167,9 @@ window.Rivers = (function () {
cells.conf = new Uint16Array(cells.i.length);
pack.rivers = [];
+ const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2);
+ const mainStemWidthFactor = defaultWidthFactor * 1.2;
+
for (const key in riversData) {
const riverCells = riversData[key];
if (riverCells.length < 3) continue; // exclude tiny rivers
@@ -169,7 +187,7 @@ window.Rivers = (function () {
const mouth = riverCells[riverCells.length - 2];
const parent = riverParents[key] || 0;
- const widthFactor = (!parent || parent === riverId ? 1.2 : 1);
+ const widthFactor = !parent || parent === riverId ? mainStemWidthFactor : defaultWidthFactor;
const meanderedPoints = addMeandering(riverCells);
const discharge = cells.fl[mouth]; // m3 in second
const length = getApproximateLength(meanderedPoints);
@@ -179,6 +197,22 @@ window.Rivers = (function () {
}
}
+ function downcutRivers() {
+ const MAX_DOWNCUT = 5;
+
+ for (const i of pack.cells.i) {
+ if (cells.h[i] < 35) continue; // don't donwcut lowlands
+ if (!cells.fl[i]) continue;
+
+ const higherCells = cells.c[i].filter(c => cells.h[c] > cells.h[i]);
+ const higherFlux = higherCells.reduce((acc, c) => acc + cells.fl[c], 0) / higherCells.length;
+ if (!higherFlux) continue;
+
+ const downcut = Math.floor(cells.fl[i] / higherFlux);
+ if (downcut) cells.h[i] -= Math.min(downcut, MAX_DOWNCUT);
+ }
+ }
+
function calculateConfluenceFlux() {
for (const i of cells.i) {
if (!cells.conf[i]) continue;
@@ -347,14 +381,14 @@ window.Rivers = (function () {
const LENGTH_PROGRESSION = [1, 1, 2, 3, 5, 8, 13, 21, 34].map(n => n / LENGTH_FACTOR);
const MAX_PROGRESSION = last(LENGTH_PROGRESSION);
- const getOffset = (flux, pointNumber, widthFactor = 1, startingWidth = 0) => {
+ const getOffset = (flux, pointNumber, widthFactor, startingWidth = 0) => {
const fluxWidth = Math.min(flux ** 0.9 / FLUX_FACTOR, MAX_FLUX_WIDTH);
const lengthWidth = pointNumber * STEP_WIDTH + (LENGTH_PROGRESSION[pointNumber] || MAX_PROGRESSION);
return widthFactor * (lengthWidth + fluxWidth) + startingWidth;
};
// build polygon from a list of points and calculated offset (width)
- const getRiverPath = function (points, widthFactor = 1, startingWidth = 0) {
+ const getRiverPath = function (points, widthFactor, startingWidth = 0) {
const riverPointsLeft = [];
const riverPointsRight = [];
@@ -447,5 +481,20 @@ window.Rivers = (function () {
return getBasin(parent);
};
- return {generate, alterHeights, resolveDepressions, addMeandering, getRiverPath, specify, getName, getType, getBasin, getWidth, getOffset, getApproximateLength, getRiverPoints, remove};
+ return {
+ generate,
+ alterHeights,
+ resolveDepressions,
+ addMeandering,
+ getRiverPath,
+ specify,
+ getName,
+ getType,
+ getBasin,
+ getWidth,
+ getOffset,
+ getApproximateLength,
+ getRiverPoints,
+ remove
+ };
})();
diff --git a/modules/ui/burg-editor.js b/modules/ui/burg-editor.js
index 7af55c29..1a2bef17 100644
--- a/modules/ui/burg-editor.js
+++ b/modules/ui/burg-editor.js
@@ -37,6 +37,7 @@ function editBurg(id) {
burgBody.querySelectorAll(".burgFeature").forEach(el => el.addEventListener("click", toggleFeature));
document.getElementById("mfcgBurgSeed").addEventListener("change", changeSeed);
document.getElementById("regenerateMFCGBurgSeed").addEventListener("click", randomizeSeed);
+ document.getElementById("addCustomMFCGBurgLink").addEventListener("click", addCustomMfcgLink);
document.getElementById("burgStyleShow").addEventListener("click", showStyleSection);
document.getElementById("burgStyleHide").addEventListener("click", hideStyleSection);
@@ -112,7 +113,13 @@ function editBurg(id) {
if (options.showMFCGMap) {
document.getElementById("mfcgPreviewSection").style.display = "block";
updateMFCGFrame(b);
- document.getElementById("mfcgBurgSeed").value = getBurgSeed(b);
+
+ if (b.link) {
+ document.getElementById("mfcgBurgSeedSection").style.display = "none";
+ } else {
+ document.getElementById("mfcgBurgSeedSection").style.display = "inline-block";
+ document.getElementById("mfcgBurgSeed").value = getBurgSeed(b);
+ }
} else {
document.getElementById("mfcgPreviewSection").style.display = "none";
}
@@ -347,22 +354,25 @@ function editBurg(id) {
function toggleFeature() {
const id = +elSelected.attr("data-id");
- const b = pack.burgs[id];
+ const burg = pack.burgs[id];
const feature = this.dataset.feature;
const turnOn = this.classList.contains("inactive");
if (feature === "port") togglePort(id);
else if (feature === "capital") toggleCapital(id);
- else b[feature] = +turnOn;
- if (b[feature]) this.classList.remove("inactive");
- else if (!b[feature]) this.classList.add("inactive");
+ else burg[feature] = +turnOn;
+ if (burg[feature]) this.classList.remove("inactive");
+ else if (!burg[feature]) this.classList.add("inactive");
- if (b.port) document.getElementById("burgEditAnchorStyle").style.display = "inline-block";
+ if (burg.port) document.getElementById("burgEditAnchorStyle").style.display = "inline-block";
else document.getElementById("burgEditAnchorStyle").style.display = "none";
+ updateMFCGFrame(burg);
}
function toggleBurgLockButton() {
const id = +elSelected.attr("data-id");
- toggleBurgLock(id);
+ const burg = pack.burgs[id];
+ burg.lock = !burg.lock;
+
updateBurgLockIcon();
}
@@ -405,7 +415,7 @@ function editBurg(id) {
function updateMFCGFrame(burg) {
const mfcgURL = getMFCGlink(burg);
- document.getElementById("mfcgPreview").setAttribute("src", mfcgURL);
+ document.getElementById("mfcgPreview").setAttribute("src", mfcgURL + "&preview=1");
document.getElementById("mfcgLink").setAttribute("href", mfcgURL);
}
@@ -426,6 +436,17 @@ function editBurg(id) {
document.getElementById("mfcgBurgSeed").value = burgSeed;
}
+ function addCustomMfcgLink() {
+ const id = +elSelected.attr("data-id");
+ const burg = pack.burgs[id];
+ const message = "Enter custom link to the burg map. It can be a link to Medieval Fantasy City Generator or other tool. Keep empty to use MFCG seed";
+ prompt(message, {default: burg.link || "", required: false}, link => {
+ if (link) burg.link = link;
+ else delete burg.link;
+ updateMFCGFrame(burg);
+ });
+ }
+
function openEmblemEdit() {
const id = +elSelected.attr("data-id"),
burg = pack.burgs[id];
diff --git a/modules/ui/burgs-overview.js b/modules/ui/burgs-overview.js
index 97093035..454671cb 100644
--- a/modules/ui/burgs-overview.js
+++ b/modules/ui/burgs-overview.js
@@ -7,6 +7,7 @@ function overviewBurgs() {
const body = document.getElementById("burgsBody");
updateFilter();
+ updateLockAllIcon();
burgsOverviewAddLines();
$("#burgsOverview").dialog();
@@ -33,6 +34,7 @@ function overviewBurgs() {
document.getElementById("burgsListToLoad").addEventListener("change", function () {
uploadFile(this, importBurgNames);
});
+ document.getElementById("burgsLockAll").addEventListener("click", toggleLockAll);
document.getElementById("burgsRemoveAll").addEventListener("click", triggerAllBurgsRemove);
document.getElementById("burgsInvertLock").addEventListener("click", invertLock);
@@ -87,7 +89,7 @@ function overviewBurgs() {
-
${getCultureOptions(
+ ${getCultureOptions(
b.culture
)}
@@ -195,8 +197,11 @@ function overviewBurgs() {
}
function toggleBurgLockStatus() {
- const burg = +this.parentNode.dataset.id;
- toggleBurgLock(burg);
+ const burgId = +this.parentNode.dataset.id;
+
+ const burg = pack.burgs[burgId];
+ burg.lock = !burg.lock;
+
if (this.classList.contains("icon-lock")) {
this.classList.remove("icon-lock");
this.classList.add("icon-lock-open");
@@ -478,9 +483,9 @@ function overviewBurgs() {
}
function renameBurgsInBulk() {
- const message = `Download burgs list as a text file, make changes and re-upload the file.
+ alertMessage.innerHTML = `Download burgs list as a text file, make changes and re-upload the file.
+ Make sure the file is a plain text document with each name on its own line (the dilimiter is CRLF).
If you do not want to change the name, just leave it as is`;
- alertMessage.innerHTML = message;
$("#alert").dialog({
title: "Burgs bulk renaming",
@@ -562,4 +567,21 @@ function overviewBurgs() {
pack.burgs = pack.burgs.map(burg => ({...burg, lock: !burg.lock}));
burgsOverviewAddLines();
}
+
+ function toggleLockAll() {
+ const activeBurgs = pack.burgs.filter(b => b.i && !b.removed);
+ const allLocked = activeBurgs.every(burg => burg.lock);
+
+ pack.burgs.forEach(burg => {
+ burg.lock = !allLocked;
+ });
+
+ burgsOverviewAddLines();
+ document.getElementById("burgsLockAll").className = allLocked ? "icon-lock" : "icon-lock-open";
+ }
+
+ function updateLockAllIcon() {
+ const allLocked = pack.burgs.every(({lock, i, removed}) => lock || !i || removed);
+ document.getElementById("burgsLockAll").className = allLocked ? "icon-lock-open" : "icon-lock";
+ }
}
diff --git a/modules/ui/diplomacy-editor.js b/modules/ui/diplomacy-editor.js
index c5f5e986..657f6b73 100644
--- a/modules/ui/diplomacy-editor.js
+++ b/modules/ui/diplomacy-editor.js
@@ -1,10 +1,9 @@
"use strict";
function editDiplomacy() {
if (customization) return;
- if (pack.states.filter(s => s.i && !s.removed).length < 2) {
- tip("There should be at least 2 states to edit the diplomacy", false, "error");
- return;
- }
+ if (pack.states.filter(s => s.i && !s.removed).length < 2) return tip("There should be at least 2 states to edit the diplomacy", false, "error");
+
+ const body = document.getElementById("diplomacyBodySection");
closeDialogs("#diplomacyEditor, .stable");
if (!layerIsOn("toggleStates")) toggleStates();
@@ -14,21 +13,29 @@ function editDiplomacy() {
if (layerIsOn("toggleBiomes")) toggleBiomes();
if (layerIsOn("toggleReligions")) toggleReligions();
- const body = document.getElementById("diplomacyBodySection");
- const statuses = ["Ally", "Friendly", "Neutral", "Suspicion", "Enemy", "Unknown", "Rival", "Vassal", "Suzerain"];
- const description = [" is an ally of ", " is friendly to ", " is neutral to ", " is suspicious of ",
- " is at war with ", " does not know about ", " is a rival of ", " is a vassal of ", " is suzerain to "];
- const colors = ["#00b300", "#d4f8aa", "#edeee8", "#eeafaa", "#e64b40", "#a9a9a9", "#ad5a1f", "#87CEFA", "#00008B"];
- refreshDiplomacyEditor();
+ const relations = {
+ Ally: {inText: "is an ally of", color: "#00b300", tip: "Allies formed a defensive pact and protect each other in case of third party aggression"},
+ Friendly: {inText: "is friendly to", color: "#d4f8aa", tip: "State is friendly to anouther state when they share some common interests"},
+ Neutral: {inText: "is neutral to", color: "#edeee8", tip: "Neutral means states relations are neither positive nor negative"},
+ Suspicion: {inText: "is suspicious of", color: "#eeafaa", tip: "Suspicion means state has a cautious distrust of another state"},
+ Enemy: {inText: "is at war with", color: "#e64b40", tip: "Enemies are states at war with each other"},
+ Unknown: {inText: "does not know about", color: "#a9a9a9", tip: "Relations are unknown if states do not have enough information about each other"},
+ Rival: {inText: "is a rival of", color: "#ad5a1f", tip: "Rivalry is a state of competing for dominance in the region"},
+ Vassal: {inText: "is a vassal of", color: "#87CEFA", tip: "Vassal is a state having obligation to its suzerain"},
+ Suzerain: {inText: "is suzerain to", color: "#00008B", tip: "Suzerain is a state having some control over its vassals"}
+ };
- tip("Click on a state to see its diplomatic relations", false, "warning");
+ refreshDiplomacyEditor();
viewbox.style("cursor", "crosshair").on("click", selectStateOnMapClick);
if (modules.editDiplomacy) return;
modules.editDiplomacy = true;
$("#diplomacyEditor").dialog({
- title: "Diplomacy Editor", resizable: false, width: fitContent(), close: closeDiplomacyEditor,
+ title: "Diplomacy Editor",
+ resizable: false,
+ width: fitContent(),
+ close: closeDiplomacyEditor,
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
});
@@ -36,10 +43,30 @@ function editDiplomacy() {
document.getElementById("diplomacyEditorRefresh").addEventListener("click", refreshDiplomacyEditor);
document.getElementById("diplomacyEditStyle").addEventListener("click", () => editStyle("regions"));
document.getElementById("diplomacyRegenerate").addEventListener("click", regenerateRelations);
- document.getElementById("diplomacyMatrix").addEventListener("click", showRelationsMatrix);
+ document.getElementById("diplomacyReset").addEventListener("click", resetRelations);
+ document.getElementById("diplomacyShowMatrix").addEventListener("click", showRelationsMatrix);
document.getElementById("diplomacyHistory").addEventListener("click", showRelationsHistory);
document.getElementById("diplomacyExport").addEventListener("click", downloadDiplomacyData);
- document.getElementById("diplomacySelect").addEventListener("mouseup", diplomacyChangeRelations);
+
+ body.addEventListener("click", function (ev) {
+ const el = ev.target;
+ if (el.parentElement.classList.contains("Self")) return;
+
+ if (el.classList.contains("changeRelations")) {
+ const line = el.parentElement;
+ const subjectId = +line.dataset.id;
+ const objectId = +body.querySelector("div.Self").dataset.id;
+ const currentRelation = line.dataset.relations;
+
+ selectRelation(subjectId, objectId, currentRelation);
+ return;
+ }
+
+ // select state of clicked line
+ body.querySelector("div.Self").classList.remove("Self");
+ el.parentElement.classList.add("Self");
+ refreshDiplomacyEditor();
+ });
function refreshDiplomacyEditor() {
diplomacyEditorAddLines();
@@ -50,33 +77,36 @@ function editDiplomacy() {
function diplomacyEditorAddLines() {
const states = pack.states;
const selectedLine = body.querySelector("div.Self");
- const sel = selectedLine ? +selectedLine.dataset.id : states.find(s => s.i && !s.removed).i;
- const selName = states[sel].fullName;
- diplomacySelect.style.display = "none";
+ const selectedId = selectedLine ? +selectedLine.dataset.id : states.find(s => s.i && !s.removed).i;
+ const selectedName = states[selectedId].name;
- COArenderer.trigger("stateCOA"+sel, states[sel].coa);
- let lines = `
-
${selName}
-
+ COArenderer.trigger("stateCOA" + selectedId, states[selectedId].coa);
+ let lines = `
+
${states[selectedId].fullName}
+
`;
- for (const s of states) {
- if (!s.i || s.removed || s.i === sel) continue;
- const relation = s.diplomacy[sel];
- const index = statuses.indexOf(relation);
- const color = colors[index];
- const tip = s.fullName + description[index] + selName;
- const tipSelect = `${tip}. Click to see relations to ${s.name}`;
- const tipChange = `${tip}. Click to change relations to ${selName}`;
- COArenderer.trigger("stateCOA"+s.i, s.coa);
+ for (const state of states) {
+ if (!state.i || state.removed || state.i === selectedId) continue;
+ const relation = state.diplomacy[selectedId];
+ const {color, inText} = relations[relation];
- lines += `
-
-
${s.fullName}
-
-
-
-
+ const tip = `${state.name} ${inText} ${selectedName}`;
+ const tipSelect = `${tip}. Click to see relations to ${state.name}`;
+ const tipChange = `Click to change relations. ${tip}`;
+
+ const name = state.fullName.length < 23 ? state.fullName : state.name;
+ COArenderer.trigger("stateCOA" + state.i, state.coa);
+
+ lines += `
+
+
${name}
+
+
+
+
+ ${relation}
+
`;
}
body.innerHTML = lines;
@@ -84,8 +114,6 @@ function editDiplomacy() {
// add listeners
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => stateHighlightOn(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => stateHighlightOff(ev)));
- body.querySelectorAll("div.states").forEach(el => el.addEventListener("click", selectStateOnLineClick));
- body.querySelectorAll(".changeRelations").forEach(el => el.addEventListener("click", toggleDiplomacySelect));
applySorting(diplomacyHeader);
$("#diplomacyEditor").dialog();
@@ -95,19 +123,31 @@ function editDiplomacy() {
if (!layerIsOn("toggleStates")) return;
const state = +event.target.dataset.id;
if (customization || !state) return;
- const d = regions.select("#state"+state).attr("d");
+ const d = regions.select("#state" + state).attr("d");
- const path = debug.append("path").attr("class", "highlight").attr("d", d)
- .attr("fill", "none").attr("stroke", "red").attr("stroke-width", 1).attr("opacity", 1)
+ const path = debug
+ .append("path")
+ .attr("class", "highlight")
+ .attr("d", d)
+ .attr("fill", "none")
+ .attr("stroke", "red")
+ .attr("stroke-width", 1)
+ .attr("opacity", 1)
.attr("filter", "url(#blur1)");
- const l = path.node().getTotalLength(), dur = (l + 5000) / 2;
+ const l = path.node().getTotalLength(),
+ dur = (l + 5000) / 2;
const i = d3.interpolateString("0," + l, l + "," + l);
- path.transition().duration(dur).attrTween("stroke-dasharray", function() {return t => i(t)});
+ path
+ .transition()
+ .duration(dur)
+ .attrTween("stroke-dasharray", function () {
+ return t => i(t);
+ });
}
function stateHighlightOff(event) {
- debug.selectAll(".highlight").each(function() {
+ debug.selectAll(".highlight").each(function () {
d3.select(this).transition().duration(1000).attr("opacity", 0).remove();
});
}
@@ -118,22 +158,17 @@ function editDiplomacy() {
if (!sel) return;
if (!layerIsOn("toggleStates")) toggleStates();
- statesBody.selectAll("path").each(function() {
+ statesBody.selectAll("path").each(function () {
if (this.id.slice(0, 9) === "state-gap") return; // exclude state gap element
const id = +this.id.slice(5); // state id
- const index = statuses.indexOf(pack.states[id].diplomacy[sel]); // status index
- const clr = index !== -1 ? colors[index] : "#4682b4"; // Self (bluish)
- this.setAttribute("fill", clr);
- statesBody.select("#state-gap"+id).attr("stroke", clr);
- statesHalo.select("#state-border"+id).attr("stroke", d3.color(clr).darker().hex());
- });
- }
- function selectStateOnLineClick() {
- if (this.classList.contains("Self")) return;
- body.querySelector("div.Self").classList.remove("Self");
- this.classList.add("Self");
- refreshDiplomacyEditor();
+ const relation = pack.states[id].diplomacy[sel];
+ const color = relations[relation]?.color || "#4682b4";
+
+ this.setAttribute("fill", color);
+ statesBody.select("#state-gap" + id).attr("stroke", color);
+ statesHalo.select("#state-border" + id).attr("stroke", d3.color(color).darker().hex());
+ });
}
function selectStateOnMapClick() {
@@ -145,42 +180,63 @@ function editDiplomacy() {
if (+selectedLine.dataset.id === state) return;
selectedLine.classList.remove("Self");
- body.querySelector("div[data-id='"+state+"']").classList.add("Self");
+ body.querySelector("div[data-id='" + state + "']").classList.add("Self");
refreshDiplomacyEditor();
}
- function toggleDiplomacySelect(event) {
- event.stopPropagation();
- const select = document.getElementById("diplomacySelect");
- const show = select.style.display === "none";
- if (!show) {select.style.display = "none"; return;}
- select.style.display = "block";
- const input = event.target.closest("div").querySelector("input");
- select.style.left = input.getBoundingClientRect().left + "px";
- select.style.top = input.getBoundingClientRect().bottom + "px";
- body.dataset.state = event.target.closest("div.states").dataset.id;
+ function selectRelation(subjectId, objectId, currentRelation) {
+ const states = pack.states;
+
+ const subject = states[subjectId];
+ const header = `
${subject.fullName}
`;
+
+ const options = Object.entries(relations)
+ .map(
+ ([relation, {color, inText, tip}]) =>
+ `
+
+
+
+
+ ${inText}
+
`
+ )
+ .join("");
+
+ const object = states[objectId];
+ const footer = `
${object.fullName}
`;
+
+ alertMessage.innerHTML = `
${header} ${options} ${footer}
`;
+
+ $("#alert").dialog({
+ width: fitContent(),
+ title: `Change relations`,
+ buttons: {
+ Apply: function () {
+ const newRelation = document.querySelector('input[name="relationSelect"]:checked')?.value;
+ changeRelation(subjectId, objectId, currentRelation, newRelation);
+ $(this).dialog("close");
+ },
+ Cancel: function () {
+ $(this).dialog("close");
+ }
+ }
+ });
}
- function diplomacyChangeRelations(event) {
- event.stopPropagation();
- diplomacySelect.style.display = "none";
- const subject = +body.dataset.state;
- const rel = event.target.innerHTML;
+ function changeRelation(subjectId, objectId, oldRelation, newRelation) {
+ if (newRelation === oldRelation) return;
+ const states = pack.states;
+ const chronicle = states[0].diplomacy;
- const states = pack.states, chronicle = states[0].diplomacy;
- const selectedLine = body.querySelector("div.Self");
- const object = selectedLine ? +selectedLine.dataset.id : states.find(s => s.i && !s.removed).i;
- if (!object) return;
- const objectName = states[object].name; // object of relations change
- const subjectName = states[subject].name; // subject of relations change - actor
+ const subjectName = states[subjectId].name;
+ const objectName = states[objectId].name;
- const oldRel = states[subject].diplomacy[object];
- if (rel === oldRel) return;
- states[subject].diplomacy[object] = rel;
- states[object].diplomacy[subject] = rel === "Vassal" ? "Suzerain" : rel === "Suzerain" ? "Vassal" : rel;
+ states[subjectId].diplomacy[objectId] = newRelation;
+ states[objectId].diplomacy[subjectId] = newRelation === "Vassal" ? "Suzerain" : newRelation === "Suzerain" ? "Vassal" : newRelation;
// update relation history
- const change = () => [`Relations change`, `${subjectName}-${getAdjective(objectName)} relations changed to ${rel.toLowerCase()}`];
+ const change = () => [`Relations change`, `${subjectName}-${getAdjective(objectName)} relations changed to ${newRelation.toLowerCase()}`];
const ally = () => [`Defence pact`, `${subjectName} entered into defensive pact with ${objectName}`];
const vassal = () => [`Vassalization`, `${subjectName} became a vassal of ${objectName}`];
const suzerain = () => [`Vassalization`, `${subjectName} vassalized ${objectName}`];
@@ -189,24 +245,33 @@ function editDiplomacy() {
const war = () => [`War declaration`, `${subjectName} declared a war on its enemy ${objectName}`];
const peace = () => {
const treaty = `${subjectName} and ${objectName} agreed to cease fire and signed a peace treaty`;
- const changed = rel === "Ally" ? ally()
- : rel === "Vassal" ? vassal()
- : rel === "Suzerain" ? suzerain()
- : rel === "Unknown" ? unknown()
- : change();
+ const changed =
+ newRelation === "Ally"
+ ? ally()
+ : newRelation === "Vassal"
+ ? vassal()
+ : newRelation === "Suzerain"
+ ? suzerain()
+ : newRelation === "Unknown"
+ ? unknown()
+ : change();
return [`War termination`, treaty, changed[1]];
- }
+ };
- if (oldRel === "Enemy") chronicle.push(peace()); else
- if (rel === "Enemy") chronicle.push(war()); else
- if (rel === "Vassal") chronicle.push(vassal()); else
- if (rel === "Suzerain") chronicle.push(suzerain()); else
- if (rel === "Ally") chronicle.push(ally()); else
- if (rel === "Unknown") chronicle.push(unknown()); else
- if (rel === "Rival") chronicle.push(rival()); else
- chronicle.push(change());
+ if (oldRelation === "Enemy") chronicle.push(peace());
+ else if (newRelation === "Enemy") chronicle.push(war());
+ else if (newRelation === "Vassal") chronicle.push(vassal());
+ else if (newRelation === "Suzerain") chronicle.push(suzerain());
+ else if (newRelation === "Ally") chronicle.push(ally());
+ else if (newRelation === "Unknown") chronicle.push(unknown());
+ else if (newRelation === "Rival") chronicle.push(rival());
+ else chronicle.push(change());
refreshDiplomacyEditor();
+ if (diplomacyMatrix.offsetParent) {
+ document.getElementById("diplomacyMatrixBody").innerHTML = "";
+ showRelationsMatrix();
+ }
}
function regenerateRelations() {
@@ -214,28 +279,52 @@ function editDiplomacy() {
refreshDiplomacyEditor();
}
+ function resetRelations() {
+ const selectedId = +body.querySelector("div.Self")?.dataset?.id;
+ if (!selectedId) return;
+ const states = pack.states;
+
+ states[selectedId].diplomacy.forEach((relations, index) => {
+ if (relations !== "x") {
+ states[selectedId].diplomacy[index] = "Neutral";
+ states[index].diplomacy[selectedId] = "Neutral";
+ }
+ });
+
+ refreshDiplomacyEditor();
+ }
+
function showRelationsHistory() {
const chronicle = pack.states[0].diplomacy;
- if (!chronicle.length) {tip("Relations history is blank", false, "error"); return;}
+ if (!chronicle.length) return tip("Relations history is blank", false, "error");
let message = `
`;
- chronicle.forEach((e, d) => {
+ chronicle.forEach((entry, d) => {
message += `
`;
- e.forEach((l, i) => message += `
${l}
`);
+ entry.forEach((l, i) => {
+ message += `
${l}
`;
+ });
message += `
`;
});
- alertMessage.innerHTML = message + `
Type to edit. Press Enter to add a new line, empty the element to remove it `;
+ alertMessage.innerHTML = message + `
Type to edit. Press Enter to add a new line, empty the element to remove it
`;
alertMessage.querySelectorAll("div[contenteditable='true']").forEach(el => el.addEventListener("input", changeReliationsHistory));
- $("#alert").dialog({title: "Relations history", position: {my: "center", at: "center", of: "svg"},
+ $("#alert").dialog({
+ title: "Relations history",
+ position: {my: "center", at: "center", of: "svg"},
buttons: {
- Save: function() {
+ Save: function () {
const data = this.querySelector("div").innerText.split("\n").join("\r\n");
const name = getFileName("Relations history") + ".txt";
downloadFile(data, name);
},
- Clear: function() {pack.states[0].diplomacy = []; $(this).dialog("close");},
- Close: function() {$(this).dialog("close");}
+ Clear: function () {
+ pack.states[0].diplomacy = [];
+ $(this).dialog("close");
+ },
+ Close: function () {
+ $(this).dialog("close");
+ }
}
});
}
@@ -251,22 +340,48 @@ function editDiplomacy() {
function showRelationsMatrix() {
const states = pack.states.filter(s => s.i && !s.removed);
- const valid = states.map(s => s.i);
+ const valid = states.map(state => state.i);
+ const diplomacyMatrixBody = document.getElementById("diplomacyMatrixBody");
- let message = `
`;
- message += states.map(s => `${s.name} `).join("") + ` `; // headers
- states.forEach(s => {
- message += `${s.name} ` + s.diplomacy
- .filter((v, i) => valid.includes(i)).map((r, i) => {
- const desc = description[statuses.indexOf(r)];
- const tip = desc ? s.fullName + desc + pack.states[valid[i]].fullName : '';
- return `${r} `
- }).join("") + " ";
+ let table = ` `;
+ table += states.map(state => `${state.name} `).join("") + ` `;
+ table += ``;
+
+ states.forEach(state => {
+ table +=
+ `${state.name} ` +
+ state.diplomacy
+ .filter((v, i) => valid.includes(i))
+ .map((relation, index) => {
+ const relationObj = relations[relation];
+ if (!relationObj) return `${relation} `;
+
+ const objectState = pack.states[valid[index]];
+ const tip = `${state.fullName} ${relationObj.inText} ${objectState.fullName}`;
+ return `${relation} `;
+ })
+ .join("") +
+ " ";
});
- message += `
`;
- alertMessage.innerHTML = message;
-
- $("#alert").dialog({title: "Relations matrix", width: fitContent(), position: {my: "center", at: "center", of: "svg"}, buttons: {}});
+
+ table += `
`;
+ diplomacyMatrixBody.innerHTML = table;
+
+ const tableEl = diplomacyMatrixBody.querySelector("table");
+ tableEl.addEventListener("click", function (event) {
+ const el = event.target;
+ if (el.tagName !== "TD") return;
+
+ const currentRelation = el.innerText;
+ if (!relations[currentRelation]) return;
+
+ const subjectId = +el.closest("tr")?.dataset?.id;
+ const objectId = +el?.dataset?.id;
+
+ selectRelation(subjectId, objectId, currentRelation);
+ });
+
+ $("#diplomacyMatrix").dialog({title: "Relations matrix", position: {my: "center", at: "center", of: "svg"}, buttons: {}});
}
function downloadDiplomacyData() {
@@ -288,7 +403,8 @@ function editDiplomacy() {
clearMainTip();
const selected = body.querySelector("div.Self");
if (selected) selected.classList.remove("Self");
- if (layerIsOn("toggleStates")) drawStates(); else toggleStates();
+ if (layerIsOn("toggleStates")) drawStates();
+ else toggleStates();
debug.selectAll(".highlight").remove();
}
}
diff --git a/modules/ui/editors.js b/modules/ui/editors.js
index 5f648fa5..3c14c149 100644
--- a/modules/ui/editors.js
+++ b/modules/ui/editors.js
@@ -265,41 +265,48 @@ function getBurgSeed(burg) {
}
function getMFCGlink(burg) {
+ if (burg.link) return burg.link;
+
const {cells} = pack;
- const {name, population, cell} = burg;
- const burgSeed = getBurgSeed(burg);
- const sizeRaw = 2.13 * Math.pow((population * populationRate) / urbanDensity, 0.385);
+ const {i, name, population: burgPopulation, cell} = burg;
+ const seed = getBurgSeed(burg);
+
+ const sizeRaw = 2.13 * Math.pow((burgPopulation * populationRate) / urbanDensity, 0.385);
const size = minmax(Math.ceil(sizeRaw), 6, 100);
- const people = rn(population * populationRate * urbanization);
+ const population = rn(burgPopulation * populationRate * urbanization);
+
+ const river = cells.r[cell] ? 1 : 0;
+ const coast = Number(burg.port > 0);
+ const sea = coast && cells.haven[cell] ? getSeaDirections(cell) : null;
+
+ const biome = cells.biome[cell];
+ const arableBiomes = river ? [1, 2, 3, 4, 5, 6, 7, 8] : [5, 6, 7, 8];
+ const farms = +arableBiomes.includes(biome);
+
+ const citadel = +burg.citadel;
+ const urban_castle = +(citadel && each(2)(i));
const hub = +cells.road[cell] > 50;
- const river = cells.r[cell] ? 1 : 0;
- const coast = +burg.port;
- const citadel = +burg.citadel;
const walls = +burg.walls;
const plaza = +burg.plaza;
const temple = +burg.temple;
- const shanty = +burg.shanty;
+ const shantytown = +burg.shanty;
- const sea = coast && cells.haven[cell] ? getSeaDirections(cell) : "";
function getSeaDirections(i) {
const p1 = cells.p[i];
const p2 = cells.p[cells.haven[i]];
let deg = (Math.atan2(p2[1] - p1[1], p2[0] - p1[0]) * 180) / Math.PI - 90;
if (deg < 0) deg += 360;
- const norm = rn(normalize(deg, 0, 360) * 2, 2); // 0 = south, 0.5 = west, 1 = north, 1.5 = east
- return "&sea=" + norm;
+ return rn(normalize(deg, 0, 360) * 2, 2); // 0 = south, 0.5 = west, 1 = north, 1.5 = east
}
- const baseURL = "https://watabou.github.io/city-generator/?random=0&continuous=0";
- const url = `${baseURL}&name=${name}&population=${people}&size=${size}&seed=${burgSeed}&hub=${hub}&river=${river}&coast=${coast}&citadel=${citadel}&plaza=${plaza}&temple=${temple}&walls=${walls}&shantytown=${shanty}${sea}`;
- return url;
-}
+ const parameters = {name, population, size, seed, river, coast, farms, citadel, urban_castle, hub, plaza, temple, walls, shantytown, gates: -1};
+ const url = new URL("https://watabou.github.io/city-generator");
+ url.search = new URLSearchParams(parameters);
+ if (sea) url.searchParams.append("sea", sea);
-function toggleBurgLock(burg) {
- const b = pack.burgs[burg];
- b.lock = b.lock ? 0 : 1;
+ return url.toString();
}
// draw legend box
diff --git a/modules/ui/general.js b/modules/ui/general.js
index 356b3668..5823afec 100644
--- a/modules/ui/general.js
+++ b/modules/ui/general.js
@@ -460,32 +460,31 @@ function showInfo() {
const Discord = link("https://discordapp.com/invite/X7E84HU", "Discord");
const Reddit = link("https://www.reddit.com/r/FantasyMapGenerator", "Reddit");
const Patreon = link("https://www.patreon.com/azgaar", "Patreon");
- const Trello = link("https://trello.com/b/7x832DG4/fantasy-map-generator", "Trello");
const Armoria = link("https://azgaar.github.io/Armoria", "Armoria");
const QuickStart = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Quick-Start-Tutorial", "Quick start tutorial");
const QAA = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Q&A", "Q&A page");
+ const VideoTutorial = link("https://youtube.com/playlist?list=PLtgiuDC8iVR2gIG8zMTRn7T_L0arl9h1C", "Video tutorial");
alertMessage.innerHTML = `
-
Fantasy Map Generator (FMG) is an open-source application, it means the code is published an anyone can use it.
- In case of FMG is also means that you own all created maps and can use them as you wish, you can even sell them.
+
Fantasy Map Generator (FMG) is a free open-source application.
+ It means that you own all created maps and can use them as you wish.
-
The development is supported by community, you can donate on ${Patreon}.
+
The development is community-backed, you can donate on ${Patreon}.
You can also help creating overviews, tutorials and spreding the word about the Generator.
-
The best way to get help is to contact the community on ${Discord} and ${Reddit}.
- Before asking questions, please check out the ${QuickStart} and the ${QAA}.
+
The best way to get help is to contact the community on ${Discord} and ${Reddit}.
+ Before asking questions, please check out the ${QuickStart}, the ${QAA}, and ${VideoTutorial}.
-
Track the development process on ${Trello}.
+
Check out our another project: ${Armoria} — heraldry generator and editor.
-
Check out our new project: ${Armoria}, heraldry generator and editor.
-
-
Links:
${link("https://github.com/Azgaar/Fantasy-Map-Generator", "GitHub repository")}
${link("https://github.com/Azgaar/Fantasy-Map-Generator/blob/master/LICENSE", "License")}
${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "Changelog")}
${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys", "Hotkeys")}
+ ${link("https://trello.com/b/7x832DG4/fantasy-map-generator", "Devboard")}
+ Contact Azgaar
`;
$("#alert").dialog({
diff --git a/modules/ui/layers.js b/modules/ui/layers.js
index baae4c88..653cc9e4 100644
--- a/modules/ui/layers.js
+++ b/modules/ui/layers.js
@@ -470,23 +470,26 @@ function togglePrec(event) {
function drawPrec() {
prec.selectAll("circle").remove();
- const cells = grid.cells,
- p = grid.points;
+ const {cells, points} = grid;
+
prec.style("display", "block");
const show = d3.transition().duration(800).ease(d3.easeSinIn);
prec.selectAll("text").attr("opacity", 0).transition(show).attr("opacity", 1);
+ const cellsNumberModifier = (pointsInput.dataset.cells / 10000) ** 0.25;
const data = cells.i.filter(i => cells.h[i] >= 20 && cells.prec[i]);
+ const getRadius = prec => rn(Math.sqrt(prec / 4) / cellsNumberModifier, 2);
+
prec
.selectAll("circle")
.data(data)
.enter()
.append("circle")
- .attr("cx", d => p[d][0])
- .attr("cy", d => p[d][1])
+ .attr("cx", d => points[d][0])
+ .attr("cy", d => points[d][1])
.attr("r", 0)
.transition(show)
- .attr("r", d => rn(Math.max(Math.sqrt(cells.prec[d] * 0.5), 0.8), 2));
+ .attr("r", d => getRadius(cells.prec[d]));
}
function togglePopulation(event) {
diff --git a/modules/ui/namesbase-editor.js b/modules/ui/namesbase-editor.js
index 03149a13..d347c498 100644
--- a/modules/ui/namesbase-editor.js
+++ b/modules/ui/namesbase-editor.js
@@ -17,18 +17,24 @@ function editNamesbase() {
document.getElementById("namesbaseMax").addEventListener("input", updateBaseMax);
document.getElementById("namesbaseDouble").addEventListener("input", updateBaseDublication);
document.getElementById("namesbaseAdd").addEventListener("click", namesbaseAdd);
- document.getElementById("namesbaseAnalize").addEventListener("click", analizeNamesbase);
+ document.getElementById("namesbaseAnalyze").addEventListener("click", analyzeNamesbase);
document.getElementById("namesbaseDefault").addEventListener("click", namesbaseRestoreDefault);
document.getElementById("namesbaseDownload").addEventListener("click", namesbaseDownload);
- document.getElementById("namesbaseUpload").addEventListener("click", () => namesbaseToLoad.click());
- document.getElementById("namesbaseToLoad").addEventListener("change", function() {uploadFile(this, namesbaseUpload)});
+ document.getElementById("namesbaseUpload").addEventListener("click", () => document.getElementById("namesbaseToLoad").click());
+ document.getElementById("namesbaseToLoad").addEventListener("change", function () {
+ uploadFile(this, namesbaseUpload);
+ });
+ document.getElementById("namesbaseCA").addEventListener("click", () => {
+ openURL("https://cartographyassets.com/asset-category/specific-assets/azgaars-generator/namebases/");
+ });
document.getElementById("namesbaseSpeak").addEventListener("click", () => speak(namesbaseExamples.textContent));
createBasesList();
updateInputs();
$("#namesbaseEditor").dialog({
- title: "Namesbase Editor", width: "42.5em",
+ title: "Namesbase Editor",
+ width: "auto",
position: {my: "center", at: "center", of: "svg"}
});
@@ -40,7 +46,10 @@ function editNamesbase() {
function updateInputs() {
const base = +document.getElementById("namesbaseSelect").value;
- if (!nameBases[base]) {tip(`Namesbase ${base} is not defined`, false, "error"); return;}
+ if (!nameBases[base]) {
+ tip(`Namesbase ${base} is not defined`, false, "error");
+ return;
+ }
document.getElementById("namesbaseTextarea").value = nameBases[base].b;
document.getElementById("namesbaseName").value = nameBases[base].name;
document.getElementById("namesbaseMin").value = nameBases[base].min;
@@ -52,7 +61,7 @@ function editNamesbase() {
function updateExamples() {
const base = +document.getElementById("namesbaseSelect").value;
let examples = "";
- for (let i=0; i < 10; i++) {
+ for (let i = 0; i < 10; i++) {
const example = Names.getBase(base);
if (example === undefined) {
examples = "Cannot generate examples. Please verify the data";
@@ -84,13 +93,19 @@ function editNamesbase() {
function updateBaseMin() {
const base = +document.getElementById("namesbaseSelect").value;
- if (+this.value > nameBases[base].max) {tip("Minimal length cannot be greater than maximal", false, "error"); return;}
+ if (+this.value > nameBases[base].max) {
+ tip("Minimal length cannot be greater than maximal", false, "error");
+ return;
+ }
nameBases[base].min = +this.value;
}
function updateBaseMax() {
const base = +document.getElementById("namesbaseSelect").value;
- if (+this.value < nameBases[base].min) {tip("Maximal length should be greater than minimal", false, "error"); return;}
+ if (+this.value < nameBases[base].min) {
+ tip("Maximal length should be greater than minimal", false, "error");
+ return;
+ }
nameBases[base].max = +this.value;
}
@@ -99,59 +114,70 @@ function editNamesbase() {
nameBases[base].d = this.value;
}
- function analizeNamesbase() {
- const string = document.getElementById("namesbaseTextarea").value;
- if (!string) {tip("Names data field should not be empty", false, "error"); return;}
- const base = string.toLowerCase();
- const array = base.split(",");
- const l = array.length;
- if (!l) {tip("Names data should not be empty", false, "error"); return;}
+ function analyzeNamesbase() {
+ const namesSourceString = document.getElementById("namesbaseTextarea").value;
+ const namesArray = namesSourceString.toLowerCase().split(",");
+ const length = namesArray.length;
+ if (!namesSourceString || !length) return tip("Names data should not be empty", false, "error");
- const wordsLength = array.map(n => n.length);
- const multi = rn(d3.mean(array.map(n => (n.match(/ /i)||[]).length)) * 100, 2);
- const geminate = array.map(name => name.match(/[^\w\s]|(.)(?=\1)/g)||[]).flat();
- const doubled = ([...new Set(geminate)].filter(l => geminate.filter(d => d === l).length > 3)||["none"]).join("");
- const chain = Names.calculateChain(string);
- const depth = rn(d3.mean(Object.keys(chain).map(key => chain[key].filter(c => c !== " ").length)));
- const nonLatin = (string.match(/[^\u0000-\u007f]/g)||["none"]).join("");
+ const chain = Names.calculateChain(namesSourceString);
+ const variety = rn(d3.mean(Object.values(chain).map(keyValue => keyValue.length)));
- const lengthStat =
- l < 30 ? "
[not enough] " :
- l < 150 ? "
[low] " :
- l < 150 ? "
[low] " :
- l < 400 ? "
[good] " :
- l < 600 ? "
[overmuch] " :
- "
[overmuch] ";
+ const wordsLength = namesArray.map(n => n.length);
- const rangeStat =
- l < 10 ? "
[low] " :
- l < 15 ? "
[low] " :
- l < 20 ? "
[low] " :
- "
[good] ";
+ const nonLatin = namesSourceString.match(/[^\u0000-\u007f]/g);
+ const nonBasicLatinChars = nonLatin
+ ? unique(
+ namesSourceString
+ .match(/[^\u0000-\u007f]/g)
+ .join("")
+ .toLowerCase()
+ ).join("")
+ : "none";
- const depthStat =
- l < 15 ? "
[low] " :
- l < 20 ? "
[low] " :
- l < 25 ? "
[low] " :
- "
[good] ";
+ const geminate = namesArray.map(name => name.match(/[^\w\s]|(.)(?=\1)/g) || []).flat();
+ const doubled = unique(geminate).filter(char => geminate.filter(doudledChar => doudledChar === char).length > 3) || ["none"];
+
+ const duplicates = unique(namesArray.filter((e, i, a) => a.indexOf(e) !== i)).join(", ") || "none";
+ const multiwordRate = d3.mean(namesArray.map(n => +n.includes(" ")));
+
+ const getLengthQuality = () => {
+ if (length < 30) return "
[not enough] ";
+ if (length < 100) return "
[low] ";
+ if (length <= 400) return "
[good] ";
+ return "
[overmuch] ";
+ };
+
+ const getVarietyLevel = () => {
+ if (variety < 15) return "
[low] ";
+ if (variety < 30) return "
[mean] ";
+ return "
[good] ";
+ };
alertMessage.innerHTML = `
-
Namesbase length: ${l} ${lengthStat}
-
Namesbase range: ${Object.keys(chain).length-1} ${rangeStat}
-
Namesbase depth: ${depth} ${depthStat}
-
Non-basic chars: ${nonLatin}
+
Namesbase length: ${length} ${getLengthQuality()}
+
Namesbase variety: ${variety} ${getVarietyLevel()}
-
Min name length: ${d3.min(wordsLength)}
-
Max name length: ${d3.max(wordsLength)}
-
Mean name length: ${rn(d3.mean(wordsLength), 1)}
-
Median name length: ${d3.median(wordsLength)}
-
Doubled chars: ${doubled}
-
Multi-word names: ${multi}%
+
Min name length: ${d3.min(wordsLength)}
+
Max name length: ${d3.max(wordsLength)}
+
Mean name length: ${rn(d3.mean(wordsLength), 1)}
+
Median name length: ${d3.median(wordsLength)}
+
+
Non-basic chars: ${nonBasicLatinChars}
+
Doubled chars: ${doubled.join("")}
+
Duplicates: ${duplicates}
+
Multi-word names: ${rn(multiwordRate * 100, 2)}%
`;
+
$("#alert").dialog({
- resizable: false, title: "Data Analysis",
+ resizable: false,
+ title: "Data Analysis",
position: {my: "left top-30", at: "right+10 top", of: "#namesbaseEditor"},
- buttons: {OK: function() {$(this).dialog("close");}}
+ buttons: {
+ OK: function () {
+ $(this).dialog("close");
+ }
+ }
});
}
@@ -171,35 +197,42 @@ function editNamesbase() {
function namesbaseRestoreDefault() {
alertMessage.innerHTML = `Are you sure you want to restore default namesbase?`;
- $("#alert").dialog({resizable: false, title: "Restore default data",
+ $("#alert").dialog({
+ resizable: false,
+ title: "Restore default data",
buttons: {
- Restore: function() {
+ Restore: function () {
$(this).dialog("close");
Names.clearChains();
nameBases = Names.getNameBases();
createBasesList();
updateInputs();
},
- Cancel: function() {$(this).dialog("close");}
+ Cancel: function () {
+ $(this).dialog("close");
+ }
}
});
}
function namesbaseDownload() {
- const data = nameBases.map((b,i) => `${b.name}|${b.min}|${b.max}|${b.d}|${b.m}|${b.b}`).join("\r\n");
+ const data = nameBases.map((b, i) => `${b.name}|${b.min}|${b.max}|${b.d}|${b.m}|${b.b}`).join("\r\n");
const name = getFileName("Namesbase") + ".txt";
downloadFile(data, name);
}
function namesbaseUpload(dataLoaded) {
const data = dataLoaded.split("\r\n");
- if (!data || !data[0]) {tip("Cannot load a namesbase. Please check the data format", false, "error"); return;}
+ if (!data || !data[0]) {
+ tip("Cannot load a namesbase. Please check the data format", false, "error");
+ return;
+ }
Names.clearChains();
nameBases = [];
data.forEach(d => {
const e = d.split("|");
- nameBases.push({name:e[0], min:e[1], max:e[2], d:e[3], m:e[4], b:e[5]});
+ nameBases.push({name: e[0], min: e[1], max: e[2], d: e[3], m: e[4], b: e[5]});
});
createBasesList();
diff --git a/modules/ui/options.js b/modules/ui/options.js
index 82b3b8a4..35645d14 100644
--- a/modules/ui/options.js
+++ b/modules/ui/options.js
@@ -102,7 +102,8 @@ function showSupporters() {
Dominick Ormsby,Linn Browning,Václav Švec,Alan Buehne,George J.Lekkas,Alexandre Boivin,Tommy Mayfield,Skylar Mangum-Turner,Karen Blythe,Stefan Gugerel,
Mike Conley,Xavier privé,Hope You're Well,Mark Sprietsma,Robert Landry,Nick Mowry,steve hall,Markell,Josh Wren,Neutrix,BLRageQuit,Rocky,
Dario Spadavecchia,Bas Kroot,John Patrick Callahan Jr,Alexandra Vesey,D,Exp1nt,james,Braxton Istace,w,Rurikid,AntiBlock,Redsauz,BigE0021,
- Jonathan Williams,ojacid .,Brian Wilson,A Patreon of the Ahts,Shubham Jakhotiya`;
+ Jonathan Williams,ojacid .,Brian Wilson,A Patreon of the Ahts,Shubham Jakhotiya,www15o,Jan Bundesmann,Angelique Badger,Joshua Xiong,Moist mongol,
+ Frank Fewkes,jason baldrick,Game Master Pro,Andrew Kircher,Preston Mitchell,Chris Kohut`;
const array = supporters
.replace(/(?:\r\n|\r|\n)/g, "")
@@ -287,18 +288,16 @@ function generateMapWithSeed(source) {
}
function showSeedHistoryDialog() {
- const alert = mapHistory
- .map(function (h, i) {
- const created = new Date(h.created).toLocaleTimeString();
- const button = `
`;
- return `
${i + 1}. Seed: ${h.seed} ${button}. Size: ${h.width}x${h.height}. Template: ${h.template}. Created: ${created}
`;
- })
- .join("");
- alertMessage.innerHTML = alert;
+ const lines = mapHistory.map((h, i) => {
+ const created = new Date(h.created).toLocaleTimeString();
+ const button = `
`;
+ return `
Seed: ${h.seed} ${button}. Size: ${h.width}x${h.height}. Template: ${h.template}. Created: ${created} `;
+ });
+ alertMessage.innerHTML = `
${lines.join("")} `;
+
$("#alert").dialog({
resizable: false,
title: "Seed history",
- width: fitContent(),
position: {my: "center", at: "center", of: "svg"}
});
}
diff --git a/modules/ui/provinces-editor.js b/modules/ui/provinces-editor.js
index 84eca9b0..7983d430 100644
--- a/modules/ui/provinces-editor.js
+++ b/modules/ui/provinces-editor.js
@@ -137,7 +137,7 @@ function editProvinces() {
p.color
}" class="fillRect pointer">
-
+
-
+
diff --git a/modules/ui/style.js b/modules/ui/style.js
index bbaa6a93..8fc85844 100644
--- a/modules/ui/style.js
+++ b/modules/ui/style.js
@@ -831,7 +831,7 @@ function applyDefaultStyle() {
landmass.attr("opacity", 1).attr("fill", "#eef6fb").attr("filter", null);
markers.attr("opacity", null).attr("rescale", 1).attr("filter", "url(#dropShadow01)");
- prec.attr("opacity", null).attr("stroke", "#000000").attr("stroke-width", 0.1).attr("fill", "#003dff").attr("filter", null);
+ prec.attr("opacity", null).attr("stroke", "#000000").attr("stroke-width", 0).attr("fill", "#003dff").attr("filter", null);
population.attr("opacity", null).attr("stroke-width", 1.6).attr("stroke-dasharray", null).attr("stroke-linecap", "butt").attr("filter", null);
population.select("#rural").attr("stroke", "#0000ff");
population.select("#urban").attr("stroke", "#ff0000");
@@ -938,7 +938,7 @@ function applyDefaultStyle() {
.attr("stroke-linecap", "round");
legend.select("#legendBox").attr("fill", "#ffffff").attr("fill-opacity", 0.8);
- const citiesSize = Math.max(rn(8 - regionsInput.value / 20), 3);
+ const citiesSize = Math.max(rn(8 - regionsOutput.value / 20), 3);
burgLabels
.select("#cities")
.attr("fill", "#3e3e4b")
@@ -979,7 +979,7 @@ function applyDefaultStyle() {
.attr("stroke-linecap", "butt");
anchors.select("#towns").attr("opacity", 1).attr("fill", "#ffffff").attr("stroke", "#3e3e4b").attr("stroke-width", 1.2).attr("size", 1);
- const stateLabelSize = Math.max(rn(24 - regionsInput.value / 6), 6);
+ const stateLabelSize = Math.max(rn(24 - regionsOutput.value / 6), 6);
labels
.select("#states")
.attr("fill", "#3e3e4b")
diff --git a/modules/ui/tools.js b/modules/ui/tools.js
index f928c25c..aeed31ff 100644
--- a/modules/ui/tools.js
+++ b/modules/ui/tools.js
@@ -143,7 +143,7 @@ function regenerateStates() {
const localSeed = Math.floor(Math.random() * 1e9); // new random seed
Math.random = aleaPRNG(localSeed);
- const statesCount = +regionsInput.value;
+ const statesCount = +regionsOutput.value;
const burgs = pack.burgs.filter(b => b.i && !b.removed);
if (!burgs.length) return tip("There are no any burgs to generate states. Please create burgs first", false, "error");
if (burgs.length < statesCount) tip(`Not enough burgs to generate ${statesCount} states. Will generate only ${burgs.length} states`, false, "warn");
@@ -624,7 +624,9 @@ function addRiverOnClick() {
const source = riverCells[0];
const mouth = riverCells[riverCells.length - 2];
- const widthFactor = river?.widthFactor || (!parent || parent === riverId ? 1.2 : 1);
+
+ const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2);
+ const widthFactor = river?.widthFactor || (!parent || parent === riverId ? defaultWidthFactor * 1.2 : defaultWidthFactor);
const meanderedPoints = addMeandering(riverCells);
const discharge = cells.fl[mouth]; // m3 in second
@@ -739,7 +741,7 @@ function configMarkersGeneration() {
const inputId = `markerIconInput${index}`;
return `
-
+
diff --git a/utils/commonUtils.js b/utils/commonUtils.js
index 5f6c5742..49ab7754 100644
--- a/utils/commonUtils.js
+++ b/utils/commonUtils.js
@@ -154,18 +154,23 @@ void (function () {
const prompt = document.getElementById("prompt");
const form = prompt.querySelector("#promptForm");
- window.prompt = function (promptText = "Please provide an input", options = {default: 1, step: 0.01, min: 0, max: 100}, callback) {
- if (options.default === undefined) {
- ERROR && console.error("Prompt: options object does not have default value defined");
- return;
- }
+ const defaultText = "Please provide an input";
+ const defaultOptions = {default: 1, step: 0.01, min: 0, max: 100, required: true};
+
+ window.prompt = function (promptText = defaultText, options = defaultOptions, callback) {
+ if (options.default === undefined) return ERROR && console.error("Prompt: options object does not have default value defined");
+
const input = prompt.querySelector("#promptInput");
prompt.querySelector("#promptText").innerHTML = promptText;
+
const type = typeof options.default === "number" ? "number" : "text";
input.type = type;
+
if (options.step !== undefined) input.step = options.step;
if (options.min !== undefined) input.min = options.min;
if (options.max !== undefined) input.max = options.max;
+
+ input.required = options.required === false ? false : true;
input.placeholder = "type a " + type;
input.value = options.default;
prompt.style.display = "block";
@@ -173,9 +178,9 @@ void (function () {
form.addEventListener(
"submit",
event => {
+ event.preventDefault();
prompt.style.display = "none";
const v = type === "number" ? +input.value : input.value;
- event.preventDefault();
if (callback) callback(v);
},
{once: true}
@@ -183,7 +188,9 @@ void (function () {
};
const cancel = prompt.querySelector("#promptCancel");
- cancel.addEventListener("click", () => (prompt.style.display = "none"));
+ cancel.addEventListener("click", () => {
+ prompt.style.display = "none";
+ });
})();
// indexedDB; ldb object