diff --git a/README.md b/README.md
index 17cfd7a9..20331678 100644
--- a/README.md
+++ b/README.md
@@ -2,9 +2,9 @@
# Fantasy Map Generator
-Azgaar's _Fantasy Map Generator_ is a free web application generating interactive and highly customizable svg maps based on voronoi diagram.
+Azgaar's _Fantasy Map Generator_ is a free web application that helps fantasy writers, game masters, and cartographers create and edit fantasy maps.
-Project is under development, the current version is available on [Github Pages](https://azgaar.github.io/Fantasy-Map-Generator).
+Link: [azgaar.github.io/Fantasy-Map-Generator](https://azgaar.github.io/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).
diff --git a/Readme.txt b/Readme.txt
deleted file mode 100644
index 9d270a7c..00000000
--- a/Readme.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-Azgaar's Fantasy Map Generator
-
-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
\ No newline at end of file
diff --git a/heightmaps/import-rules.txt b/heightmaps/import-rules.txt
index 9478b35c..69499114 100644
--- a/heightmaps/import-rules.txt
+++ b/heightmaps/import-rules.txt
@@ -1,8 +1,8 @@
To get heightmap with correct height scale:
-1. Open tangrams.github.io
+1. Open https://tangrams.github.io/heightmapper
2. Toggle off auto-exposure
3. Set max elevation to 2000
4. Set min elevation to -500
5. Find region you like
6. Render image
-7. Optionally rescale image to a smaller size (e.g. 500x300px) as high resolution is not used
\ No newline at end of file
+7. Optionally rescale image to a smaller size (e.g. 500x300px) as high resolution is not used
diff --git a/index.css b/index.css
index bfd1ed67..9ec277b6 100644
--- a/index.css
+++ b/index.css
@@ -1082,12 +1082,16 @@ tr.battleSurvivors {
font-size: 0.9em;
}
-#battleBody div.battlePhases,
#battleBottom div.battleTypes {
position: fixed;
background-color: #ffffff30;
}
+#battleBody div.battlePhases {
+ position: absolute;
+ background-color: #ffffff30;
+}
+
#battleBody div.battlePhases > button,
#battleBottom div.battleTypes > button {
width: 3.2em;
@@ -2045,6 +2049,7 @@ div.textual span,
}
#notesLegend {
+ width: auto;
height: 87%;
outline: 0;
overflow-y: auto;
@@ -2239,7 +2244,6 @@ svg.button {
user-select: none;
}
-
.dontAsk {
margin: 0.9em 0 0 0.6em;
display: inline-flex;
@@ -2338,7 +2342,7 @@ svg.button {
}
@media (prefers-color-scheme: dark) {
- body {
- background: #25252a;
- }
+ body {
+ background: #25252a;
+ }
}
diff --git a/index.html b/index.html
index bc9d9fc5..d0714dab 100644
--- a/index.html
+++ b/index.html
@@ -3,14 +3,33 @@
Halo width
@@ -1446,7 +1461,7 @@
Canvas size
@@ -1458,7 +1473,7 @@
@@ -1883,8 +1898,8 @@
Rendering
- Best quality
- Best performace
+ Best quality
+ Best performace
@@ -2749,7 +2764,7 @@
-
+
-
+
Select from the list or paste a Unicode character here:
@@ -7830,7 +7845,7 @@
-
+
@@ -7841,14 +7856,14 @@
-
-
+
+
-
-
+
+
-
+
@@ -7859,34 +7874,34 @@
-
+
-
-
+
+
-
-
+
+
-
+
-
-
+
+
-
+
-
+
diff --git a/main.js b/main.js
index 69323e92..a7756124 100644
--- a/main.js
+++ b/main.js
@@ -191,7 +191,6 @@ let populationRate = +document.getElementById("populationRateInput").value;
let distanceScale = +document.getElementById("distanceScaleInput").value;
let urbanization = +document.getElementById("urbanizationInput").value;
let urbanDensity = +document.getElementById("urbanDensityInput").value;
-let statesNeutral = 1; // statesEditor growth parameter
applyStoredOptions();
@@ -695,6 +694,7 @@ async function generate(options) {
if (shouldRegenerateGrid(grid, precreatedSeed)) grid = precreatedGraph || generateGrid();
else delete grid.cells.h;
grid.cells.h = await HeightmapGenerator.generate(grid);
+ pack = {}; // reset pack
markFeatures();
markupGridOcean();
diff --git a/modules/burgs-and-states.js b/modules/burgs-and-states.js
index c3d2704d..e360b45a 100644
--- a/modules/burgs-and-states.js
+++ b/modules/burgs-and-states.js
@@ -359,7 +359,7 @@ window.BurgsAndStates = (function () {
TIME && console.timeEnd("drawBurgs");
};
- // growth algorithm to assign cells to states like we did for cultures
+ // expand cultures across the map (Dijkstra-like algorithm)
const expandStates = function () {
TIME && console.time("expandStates");
const {cells, states, cultures, burgs} = pack;
@@ -367,18 +367,28 @@ window.BurgsAndStates = (function () {
cells.state = cells.state || new Uint16Array(cells.i.length);
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const cost = [];
- const neutral = (cells.i.length / 5000) * 2500 * neutralInput.value * statesNeutral; // limit cost for state growth
- states
- .filter(s => s.i && !s.removed)
- .forEach(s => {
- const capitalCell = burgs[s.capital].cell;
- cells.state[capitalCell] = s.i;
- const cultureCenter = cultures[s.culture].center;
- const b = cells.biome[cultureCenter]; // state native biome
- queue.queue({e: s.center, p: 0, s: s.i, b});
- cost[s.center] = 1;
- });
+ const globalNeutralRate = byId("neutralInput")?.valueAsNumber || 1;
+ const statesNeutralRate = byId("statesNeutral")?.valueAsNumber || 1;
+ const neutral = (cells.i.length / 2) * globalNeutralRate * statesNeutralRate; // limit cost for state growth
+
+ // remove state from all cells except of locked
+ for (const cellId of cells.i) {
+ const state = states[cells.state[cellId]];
+ if (state.lock) continue;
+ cells.state[cellId] = 0;
+ }
+
+ for (const state of states) {
+ if (!state.i || state.removed) continue;
+
+ const capitalCell = burgs[state.capital].cell;
+ cells.state[capitalCell] = state.i;
+ const cultureCenter = cultures[state.culture].center;
+ const b = cells.biome[cultureCenter]; // state native biome
+ queue.queue({e: state.center, p: 0, s: state.i, b});
+ cost[state.center] = 1;
+ }
while (queue.length) {
const next = queue.dequeue();
@@ -608,7 +618,7 @@ window.BurgsAndStates = (function () {
if (list && !list.includes(state.i)) continue;
byId(`stateLabel${state.i}`)?.remove();
- byId(`textPath_stateLabel6${state.i}`)?.remove();
+ byId(`textPath_stateLabel${state.i}`)?.remove();
}
const example = g.append("text").attr("x", 0).attr("x", 0).text("Average");
diff --git a/modules/cultures-generator.js b/modules/cultures-generator.js
index 90f93d69..1c0e07a7 100644
--- a/modules/cultures-generator.js
+++ b/modules/cultures-generator.js
@@ -116,23 +116,25 @@ window.Cultures = (function () {
cultures.forEach(c => (c.base = c.base % nameBases.length));
- function selectCultures(c) {
- let def = getDefault(c);
- if (c === def.length) return def;
- if (def.every(d => d.odd === 1)) return def.splice(0, c);
-
- const count = Math.min(c, def.length);
+ function selectCultures(culturesNumber) {
+ let def = getDefault(culturesNumber);
const cultures = [];
-
+
pack.cultures?.forEach(function (culture) {
if (culture.lock) cultures.push(culture);
});
+
+ if (!cultures.length) {
+ if (culturesNumber === def.length) return def;
+ if (def.every(d => d.odd === 1)) return def.splice(0, culturesNumber);
+ }
- for (let culture, rnd, i = 0; cultures.length < count && i < 200; i++) {
+ for (let culture, rnd, i = 0; cultures.length < culturesNumber && def.length > 0;) {
do {
rnd = rand(def.length - 1);
culture = def[rnd];
- } while (!P(culture.odd));
+ i++;
+ } while (i < 200 && !P(culture.odd));
cultures.push(culture);
def.splice(rnd, 1);
}
@@ -507,28 +509,37 @@ window.Cultures = (function () {
// expand cultures across the map (Dijkstra-like algorithm)
const expand = function () {
TIME && console.time("expandCultures");
- cells = pack.cells;
+ const {cells, cultures} = pack;
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
- pack.cultures.forEach(function (c) {
- if (!c.i || c.removed || c.lock) return;
- queue.queue({e: c.center, p: 0, c: c.i});
- });
-
- const neutral = (cells.i.length / 5000) * 3000 * neutralInput.value; // limit cost for culture growth
const cost = [];
+
+ const neutralRate = byId("neutralRate")?.valueAsNumber || 1;
+ const neutral = cells.i.length * 0.6 * neutralRate; // limit cost for culture growth
+
+ // remove culture from all cells except of locked
+ for (const cellId of cells.i) {
+ const culture = cultures[cells.culture[cellId]];
+ if (culture.lock) continue;
+ cells.culture[cellId] = 0;
+ }
+
+ for (const culture of cultures) {
+ if (!culture.i || culture.removed) continue;
+ queue.queue({e: culture.center, p: 0, c: culture.i});
+ }
+
while (queue.length) {
- const next = queue.dequeue(),
- n = next.e,
- p = next.p,
- c = next.c;
- const type = pack.cultures[c].type;
- cells.c[n].forEach(e => {
- if (pack.cultures[cells.culture[e]]?.lock) return;
+ const {e, p, c} = queue.dequeue();
+ const {type} = pack.cultures[c];
+
+ cells.c[e].forEach(e => {
+ const culture = cells.culture[e];
+ if (culture?.lock) return; // do not overwrite cell of locked culture
const biome = cells.biome[e];
const biomeCost = getBiomeCost(c, biome, type);
- const biomeChangeCost = biome === cells.biome[n] ? 0 : 20; // penalty on biome change
+ const biomeChangeCost = biome === cells.biome[e] ? 0 : 20; // penalty on biome change
const heightCost = getHeightCost(e, cells.h[e], type);
const riverCost = getRiverCost(cells.r[e], e, type);
const typeCost = getTypeCost(cells.t[e], type);
diff --git a/modules/dynamic/editors/cultures-editor.js b/modules/dynamic/editors/cultures-editor.js
index 44565449..5e66941d 100644
--- a/modules/dynamic/editors/cultures-editor.js
+++ b/modules/dynamic/editors/cultures-editor.js
@@ -24,7 +24,7 @@ export function open() {
function insertEditorHtml() {
const editorHtml = /* html */ `
- `;
continue;
@@ -207,6 +207,7 @@ function culturesEditorAddLines() {
${getTypeOptions(c.type)}
+
${getBaseOptions(c.base)}
@@ -225,10 +226,9 @@ function culturesEditorAddLines() {
${si(area)} ${unit}
${si(population)}
-
+ style="width: 4em">${si(population)}
${getShapeOptions(selectShape, c.shield)}
-
+
`;
}
@@ -251,7 +251,7 @@ function culturesEditorAddLines() {
$body.querySelectorAll("fill-box").forEach($el => $el.on("click", cultureChangeColor));
$body.querySelectorAll("div > input.cultureName").forEach($el => $el.on("input", cultureChangeName));
$body.querySelectorAll("div > span.icon-cw").forEach($el => $el.on("click", cultureRegenerateName));
- $body.querySelectorAll("div > input.cultureExpan").forEach($el => $el.on("input", cultureChangeExpansionism));
+ $body.querySelectorAll("div > input.cultureExpan").forEach($el => $el.on("change", cultureChangeExpansionism));
$body.querySelectorAll("div > select.cultureType").forEach($el => $el.on("change", cultureChangeType));
$body.querySelectorAll("div > select.cultureBase").forEach($el => $el.on("change", cultureChangeBase));
$body.querySelectorAll("div > select.cultureEmblems").forEach($el => $el.on("change", cultureChangeEmblemsShape));
@@ -590,16 +590,23 @@ function drawCultureCenters() {
}
function cultureCenterDrag() {
- const $el = d3.select(this);
const cultureId = +this.id.slice(13);
- d3.event.on("drag", () => {
+ const tr = parseTransform(this.getAttribute("transform"));
+ const x0 = +tr[0] - d3.event.x;
+ const y0 = +tr[1] - d3.event.y;
+
+ function handleDrag() {
const {x, y} = d3.event;
- $el.attr("cx", x).attr("cy", y);
+ this.setAttribute("transform", `translate(${x0 + x},${y0 + y})`);
const cell = findCell(x, y);
if (pack.cells.h[cell] < 20) return; // ignore dragging on water
+
pack.cultures[cultureId].center = cell;
recalculateCultures();
- });
+ }
+
+ const dragDebounced = debounce(handleDrag, 50);
+ d3.event.on("drag", dragDebounced);
}
function toggleLegend() {
@@ -666,17 +673,10 @@ async function showHierarchy() {
function recalculateCultures(must) {
if (!must && !culturesAutoChange.checked) return;
- pack.cells.culture = new Uint16Array(pack.cells.i.length);
- pack.cultures.forEach(function (c) {
- if (!c.i || c.removed) return;
- pack.cells.culture[c.center] = c.i;
- });
-
Cultures.expand();
drawCultures();
pack.burgs.forEach(b => (b.culture = pack.cells.culture[b.cell]));
refreshCulturesEditor();
- document.querySelector("input.cultureExpan").focus(); // to not trigger hotkeys
}
function enterCultureManualAssignent() {
diff --git a/modules/dynamic/editors/religions-editor.js b/modules/dynamic/editors/religions-editor.js
index 4cf1903f..b6b2b5a0 100644
--- a/modules/dynamic/editors/religions-editor.js
+++ b/modules/dynamic/editors/religions-editor.js
@@ -3,10 +3,10 @@ addListeners();
export function open() {
closeDialogs("#religionsEditor, .stable");
- if (!layerIsOn("toggleReligions")) toggleCultures();
+ if (!layerIsOn("toggleReligions")) toggleReligions();
if (layerIsOn("toggleStates")) toggleStates();
if (layerIsOn("toggleBiomes")) toggleBiomes();
- if (layerIsOn("toggleCultures")) toggleReligions();
+ if (layerIsOn("toggleCultures")) toggleCultures();
if (layerIsOn("toggleProvinces")) toggleProvinces();
refreshReligionsEditor();
@@ -23,13 +23,15 @@ export function open() {
function insertEditorHtml() {
const editorHtml = /* html */ `
-
+
+
+
+ auto-apply changes
+
`;
@@ -109,6 +116,7 @@ function addListeners() {
byId("religionsManuallyCancel").on("click", () => exitReligionsManualAssignment());
byId("religionsAdd").on("click", enterAddReligionMode);
byId("religionsExport").on("click", downloadReligionsCsv);
+ byId("religionsRecalculate").on("click", () => recalculateReligions(true));
}
function refreshReligionsEditor() {
@@ -166,9 +174,10 @@ function religionsEditorAddLines() {
data-type=""
data-form=""
data-deity=""
+ data-expansion=""
data-expansionism=""
>
-
+
@@ -178,9 +187,11 @@ function religionsEditorAddLines() {
- ${si(area) + unit}
+ ${si(area) + unit}
- ${si(population)}
+ ${si(
+ population
+ )}
`;
continue;
}
@@ -195,6 +206,7 @@ function religionsEditorAddLines() {
data-type="${r.type}"
data-form="${r.form}"
data-deity="${r.deity || ""}"
+ data-expansion="${r.expansion}"
data-expansionism="${r.expansionism}"
>
@@ -209,13 +221,13 @@ function religionsEditorAddLines() {
- ${si(area) + unit}
+ ${si(area) + unit}
- ${si(population)}
-
+ ${si(
+ population
+ )}
+ ${getExpansionColumns(r)}
+
`;
}
@@ -245,6 +257,8 @@ function religionsEditorAddLines() {
$body.querySelectorAll("div > input.religionDeity").forEach(el => el.on("input", religionChangeDeity));
$body.querySelectorAll("div > span.icon-arrows-cw").forEach(el => el.on("click", regenerateDeity));
$body.querySelectorAll("div > div.religionPopulation").forEach(el => el.on("click", changePopulation));
+ $body.querySelectorAll("div > select.religionExtent").forEach(el => el.on("change", religionChangeExtent));
+ $body.querySelectorAll("div > input.religionExpantion").forEach(el => el.on("change", religionChangeExpansionism));
$body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.on("click", religionRemovePrompt));
$body.querySelectorAll("div > span.icon-lock").forEach($el => $el.on("click", updateLockStatus));
$body.querySelectorAll("div > span.icon-lock-open").forEach($el => $el.on("click", updateLockStatus));
@@ -253,6 +267,7 @@ function religionsEditorAddLines() {
$body.dataset.type = "absolute";
togglePercentageMode();
}
+
applySorting(religionsHeader);
$("#religionsEditor").dialog({width: fitContent()});
}
@@ -264,6 +279,41 @@ function getTypeOptions(type) {
return options;
}
+function getExpansionColumns(r) {
+ if (r.type === "Folk") {
+ const tip =
+ "Folk religions are not competitive and do not expand. Initially they cover all cells of their parent culture, but get ousted by organized religions when they expand";
+ return /* html */ `
+
+ culture
+
+ `;
+ }
+
+ return /* html */ `
+
+
+ ${getExtentOptions(r.expansion)}
+
+
+ `;
+}
+
+function getExtentOptions(type) {
+ let options = "";
+ const types = ["global", "state", "culture"];
+ types.forEach(t => (options += `${t} `));
+ return options;
+}
+
const religionHighlightOn = debounce(event => {
const religionId = Number(event.id || event.target.dataset.id);
const $el = $body.querySelector(`div[data-id='${religionId}']`);
@@ -272,20 +322,19 @@ const religionHighlightOn = debounce(event => {
if (!layerIsOn("toggleReligions")) return;
if (customization) return;
- const animate = d3.transition().duration(1500).ease(d3.easeSinIn);
+ const animate = d3.transition().duration(2000).ease(d3.easeSinIn);
relig
.select("#religion" + religionId)
.raise()
.transition(animate)
.attr("stroke-width", 2.5)
- .attr("stroke", "#c13119");
+ .attr("stroke", "#d0240f");
debug
.select("#religionsCenter" + religionId)
.raise()
.transition(animate)
- .attr("r", 8)
- .attr("stroke-width", 2)
- .attr("stroke", "#c13119");
+ .attr("r", 3)
+ .attr("stroke", "#d0240f");
}, 200);
function religionHighlightOff(event) {
@@ -301,8 +350,7 @@ function religionHighlightOff(event) {
debug
.select("#religionsCenter" + religionId)
.transition()
- .attr("r", 4)
- .attr("stroke-width", 1.2)
+ .attr("r", 2)
.attr("stroke", null);
}
@@ -434,6 +482,20 @@ function changePopulation() {
}
}
+function religionChangeExtent() {
+ const religion = +this.parentNode.dataset.id;
+ this.parentNode.dataset.expansion = this.value;
+ pack.religions[religion].expansion = this.value;
+ recalculateReligions();
+}
+
+function religionChangeExpansionism() {
+ const religion = +this.parentNode.dataset.id;
+ this.parentNode.dataset.expansionism = this.value;
+ pack.religions[religion].expansionism = +this.value;
+ recalculateReligions();
+}
+
function religionRemovePrompt() {
if (customization) return;
@@ -471,11 +533,14 @@ function drawReligionCenters() {
const religionCenters = debug
.append("g")
.attr("id", "religionCenters")
- .attr("stroke-width", 1.2)
+ .attr("stroke-width", 0.8)
.attr("stroke", "#444444")
.style("cursor", "move");
- const data = pack.religions.filter(r => r.i && r.center && r.cells && !r.removed);
+ let data = pack.religions.filter(r => r.i && r.center && !r.removed);
+ const showExtinct = $body.dataset.extinct === "show";
+ if (!showExtinct) data = data.filter(r => r.cells > 0);
+
religionCenters
.selectAll("circle")
.data(data)
@@ -483,7 +548,7 @@ function drawReligionCenters() {
.append("circle")
.attr("id", d => "religionsCenter" + d.i)
.attr("data-id", d => d.i)
- .attr("r", 4)
+ .attr("r", 2)
.attr("fill", d => d.color)
.attr("cx", d => pack.cells.p[d.center][0])
.attr("cy", d => pack.cells.p[d.center][1])
@@ -499,15 +564,23 @@ function drawReligionCenters() {
}
function religionCenterDrag() {
- const $el = d3.select(this);
const religionId = +this.dataset.id;
- d3.event.on("drag", () => {
+ const tr = parseTransform(this.getAttribute("transform"));
+ const x0 = +tr[0] - d3.event.x;
+ const y0 = +tr[1] - d3.event.y;
+
+ function handleDrag() {
const {x, y} = d3.event;
- $el.attr("cx", x).attr("cy", y);
+ this.setAttribute("transform", `translate(${x0 + x},${y0 + y})`);
const cell = findCell(x, y);
if (pack.cells.h[cell] < 20) return; // ignore dragging on water
+
pack.religions[religionId].center = cell;
- });
+ recalculateReligions();
+ }
+
+ const dragDebounced = debounce(handleDrag, 50);
+ d3.event.on("drag", dragDebounced);
}
function toggleLegend() {
@@ -578,13 +651,14 @@ async function showHierarchy() {
function toggleExtinct() {
$body.dataset.extinct = $body.dataset.extinct !== "show" ? "show" : "hide";
religionsEditorAddLines();
+ drawReligionCenters();
}
function enterReligionsManualAssignent() {
if (!layerIsOn("toggleReligions")) toggleReligions();
customization = 7;
relig.append("g").attr("id", "temp");
- document.querySelectorAll("#religionsBottom > button").forEach(el => (el.style.display = "none"));
+ document.querySelectorAll("#religionsBottom > *").forEach(el => (el.style.display = "none"));
byId("religionsManuallyButtons").style.display = "inline-block";
debug.select("#religionCenters").style("display", "none");
@@ -686,7 +760,7 @@ function exitReligionsManualAssignment(close) {
customization = 0;
relig.select("#temp").remove();
removeCircle();
- document.querySelectorAll("#religionsBottom > button").forEach(el => (el.style.display = "inline-block"));
+ document.querySelectorAll("#religionsBottom > *").forEach(el => (el.style.display = "inline-block"));
byId("religionsManuallyButtons").style.display = "none";
byId("religionsEditor")
@@ -740,15 +814,15 @@ function addReligion() {
function downloadReligionsCsv() {
const unit = getAreaUnit("2");
- const headers = `Id,Name,Color,Type,Form,Supreme Deity,Area ${unit},Believers,Origins`;
+ const headers = `Id,Name,Color,Type,Form,Supreme Deity,Area ${unit},Believers,Origins,Potential,Expansionism`;
const lines = Array.from($body.querySelectorAll(":scope > div"));
const data = lines.map($line => {
- const {id, name, color, type, form, deity, area, population} = $line.dataset;
+ const {id, name, color, type, form, deity, area, population, expansion, expansionism} = $line.dataset;
const deityText = '"' + deity + '"';
const {origins} = pack.religions[+id];
const originList = (origins || []).filter(origin => origin).map(origin => pack.religions[origin].name);
const originText = '"' + originList.join(", ") + '"';
- return [id, name, color, type, form, deityText, area, population, originText].join(",");
+ return [id, name, color, type, form, deityText, area, population, originText, expansion, expansionism].join(",");
});
const csvData = [headers].concat(data).join("\n");
@@ -773,3 +847,13 @@ function updateLockStatus() {
classList.toggle("icon-lock-open");
classList.toggle("icon-lock");
}
+
+function recalculateReligions(must) {
+ if (!must && !religionsAutoChange.checked) return;
+
+ Religions.recalculate();
+
+ drawReligions();
+ refreshReligionsEditor();
+ drawReligionCenters();
+}
diff --git a/modules/dynamic/editors/states-editor.js b/modules/dynamic/editors/states-editor.js
index 773d421d..41241340 100644
--- a/modules/dynamic/editors/states-editor.js
+++ b/modules/dynamic/editors/states-editor.js
@@ -163,8 +163,6 @@ function addListeners() {
const line = $element.parentNode;
const state = +line.dataset.id;
if (classList.contains("stateCapital")) stateChangeCapitalName(state, line, $element.value);
- else if (classList.contains("cultureType")) stateChangeType(state, line, $element.value);
- else if (classList.contains("statePower")) stateChangeExpansionism(state, line, $element.value);
});
$body.on("change", function (ev) {
@@ -173,6 +171,8 @@ function addListeners() {
const line = $element.parentNode;
const state = +line.dataset.id;
if (classList.contains("stateCulture")) stateChangeCulture(state, line, $element.value);
+ else if (classList.contains("cultureType")) stateChangeType(state, line, $element.value);
+ else if (classList.contains("statePower")) stateChangeExpansionism(state, line, $element.value);
});
}
@@ -883,7 +883,6 @@ function changeStatesGrowthRate() {
const growthRate = +this.value;
byId("statesNeutral").value = growthRate;
byId("statesNeutralNumber").value = growthRate;
- statesNeutral = growthRate;
tip("Growth rate: " + growthRate);
recalculateStates(false);
}
diff --git a/modules/dynamic/supporters.js b/modules/dynamic/supporters.js
index 038da4d8..c83e10ec 100644
--- a/modules/dynamic/supporters.js
+++ b/modules/dynamic/supporters.js
@@ -1,44 +1,481 @@
-const capitalize = text => text.charAt(0).toUpperCase() + text.slice(1);
-
-const format = rawList =>
- rawList
- .replace(/(?:\r\n|\r|\n)/g, "")
- .split(",")
- .map(name => capitalize(name.trim()))
- .sort();
-
-export const supporters = format(`
- Aaron Meyer,Ahmad Amerih,AstralJacks,aymeric,Billy Dean Goehring,Branndon Edwards,Chase Mayers,Curt Flood,cyninge,Dino Princip,
- E.M. White,es,Fondue,Fritjof Olsson,Gatsu,Johan Fröberg,Jonathan Moore,Joseph Miranda,Kate,KC138,Luke Nelson,Markus Finster,Massimo Vella,Mikey,
- Nathan Mitchell,Paavi1,Pat,Ryan Westcott,Sasquatch,Shawn Spencer,Sizz_TV,Timothée CALLET,UTG community,Vlad Tomash,Wil Sisney,William Merriott,
- Xariun,Gun Metal Games,Scott Marner,Spencer Sherman,Valerii Matskevych,Alloyed Clavicle,Stewart Walsh,Ruthlyn Mollett (Javan),Benjamin Mair-Pratt,
- Diagonath,Alexander Thomas,Ashley Wilson-Savoury,William Henry,Preston Brooks,JOSHUA QUALTIERI,Hilton Williams,Katharina Haase,Hisham Bedri,
- Ian arless,Karnat,Bird,Kevin,Jessica Thomas,Steve Hyatt,Logicspren,Alfred García,Jonathan Killstring,John Ackley,Invad3r233,Norbert Žigmund,Jennifer,
- PoliticsBuff,_gfx_,Maggie,Connor McMartin,Jared McDaris,BlastWind,Franc Casanova Ferrer,Dead & Devil,Michael Carmody,Valerie Elise,naikibens220,
- Jordon Phillips,William Pucs,The Dungeon Masters,Brady R Rathbun,J,Shadow,Matthew Tiffany,Huw Williams,Joseph Hamilton,FlippantFeline,Tamashi Toh,
- kms,Stephen Herron,MidnightMoon,Whakomatic x,Barished,Aaron bateson,Brice Moss,Diklyquill,PatronUser,Michael Greiner,Steven Bennett,Jacob Harrington,
- Miguel C.,Reya C.,Giant Monster Games,Noirbard,Brian Drennen,Ben Craigie,Alex Smolin,Endwords,Joshua E Goodwin,SirTobit ,Allen S. Rout,Allen Bull Bear,
- Pippa Mitchell,R K,G0atfather,Ryan Lege,Caner Oleas Pekgönenç,Bradley Edwards,Tertiary ,Austin Miller,Jesse Holmes,Jan Dvořák,Marten F,Erin D. Smale,
- Maxwell Hill,Drunken_Legends,rob bee,Jesse Holmes,YYako,Detocroix,Anoplexian,Hannah,Paul,Sandra Krohn,Lucid,Richard Keating,Allen Varney,Rick Falkvinge,
- Seth Fusion,Adam Butler,Gus,StroboWolf,Sadie Blackthorne,Zewen Senpai,Dell McKnight,Oneiris,Darinius Dragonclaw Studios,Christopher Whitney,Rhodes HvZ,
- Jeppe Skov Jensen,María Martín López,Martin Seeger,Annie Rishor,Aram Sabatés,MadNomadMedia,Eric Foley,Vito Martono,James H. Anthony,Kevin Cossutta,
- Thirty-OneR,ThatGuyGW,Dee Chiu,MontyBoosh,Achillain,Jaden,SashaTK,Steve Johnson,Pierrick Bertrand,Jared Kennedy,Dylan Devenny,Kyle Robertson,
- Andrew Rostaing,Daniel Gill,Char,Jack,Barna Csíkos,Ian Rousseau,Nicholas Grabstas,Tom Van Orden jr,Bryan Brake,Akylos,Riley Seaman,MaxOliver,Evan-DiLeo,
- Alex Debus,Joshua Vaught,Kyle S,Eric Moore,Dean Dunakin,Uniquenameosaurus,WarWizardGames,Chance Mena,Jan Ka,Miguel Alejandro,Dalton Clark,Simon Drapeau,
- Radovan Zapletal,Jmmat6,Justa Badge,Blargh Blarghmoomoo,Vanessa Anjos,Grant A. Murray,Akirsop,Rikard Wolff,Jake Fish,teco 47,Antiroo,Jakob Siegel,
- Guilherme Aguiar,Jarno Hallikainen,Justin Mcclain,Kristin Chernoff,Rowland Kingman,Esther Busch,Grayson McClead,Austin,Hakon the Viking,Chad Riley,
- Cooper Counts,Patrick Jones,Clonetone,PlayByMail.Net,Brad Wardell,Lance Saba,Egoensis,Brea Richards,Tiber,Chris Bloom,Maxim Lowe,Aquelion,
- Page One Project,Spencer Morris,Paul Ingram,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,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,www15o,Jan Bundesmann,Angelique Badger,Joshua Xiong,Moist mongol,
- Frank Fewkes,jason baldrick,Game Master Pro,Andrew Kircher,Preston Mitchell,Chris Kohut,Emarandzeb,Trentin Bergeron,Damon Gallaty,Pleaseworkforonce,
- Jordan,William Markus,Sidr Dim,Alexander Whittaker,The Next Level,Patrick Valverde,Markus Peham,Daniel Cooper,the Beagles of Neorbus,Marley Moule,
- Maximilian Schielke,Johnathan Xavier Hutchinson,Ele,Rita,Randy Ross,John Wick,RedSpaz,cameron cannon,Ian Grau-Fay,Kyle Barrett,Charlotte Wiland,
- David Kaul,E. Jason Davis,Cyberate,Atenfox,Sea Wolf,Holly Loveless,Roekai,Alden Z,angel carrillo,Sam Spoerle,S A Rudy,Bird Law Expert,Mira Cyr,
- Aaron Blair,Neyimadd,RLKZ1022,DerWolf,Kenji Yamada,Zion,Robert Rinne,Actual_Dio,Kyarou
-`);
+export const supporters = `ken burgan
+Sera's Nafitlaan
+Richard Rogers
+Hylobate
+Colin deSousa
+Aurelia De La Silla
+Maciej Kontny
+Ricky L Cain
+Iggyflare
+Garrett Renner
+Michael Harris
+Joshua Maly
+Nigel Guest
+Theo Hodges
+BERTHEAS Frédéric
+lilMoni
+Δημήτρης Μάρκογιαννακης
+Lee S.
+Chris Dibbs
+jarrad tait
+Jacen Solo
+Hannes Rotestam
+Preston Hicks
+Лонгин
+Will Fink
+ControlFreq
+IllAngel
+John Giardina
+Thiago Prado
+Zhang Dijon
+NoBurny
+thibault tersinet
+scarletsky
+Nich Smith
+Omegus
+Karl Abrahamsson
+Sara Fernandes
+peetey897
+Cooper Janse
+G F
+Glen Aultman-Bettridge
+Nathan Rogers
+Benjamin Mock
+CadmiumMan
+Kirk Edwards
+Leigh G
+Thom Colyer
+Frederik
+C pstj
+Zachary Pecora
+Trevor D'Arcey
+Ryan Gauvin
+Shawn Moore
+Jim Channon
+Kyarou
+Actual_Dio
+Jim B Johnson
+Robert Rinne
+Zion
+Kenji Yamada
+DerWolf
+RLKZ1022
+Neyimadd
+Aaron Blair
+Mira Cyr
+Bird Law Expert
+S A Rudy
+Sam Spoerle
+angel carrillo
+Alden Z
+Holly Loveless
+Sea Wolf
+Atenfox
+Cyberate
+E. Jason Davis
+Caro Lyns
+David Kaul
+Charlotte Wiland
+Kyle Barrett
+Ian Grau-Fay
+cameron cannon
+RedSpaz
+John Wick
+Randy Ross
+Rita
+Ele
+Johnathan Xavier Hutchinson
+Andrew Stein
+Ghettov Milan
+Malke
+TameMoon
+Daniel Cooper
+Markus Peham
+The Next Level
+Alexander Whittaker
+Sidr Dim
+William Markus
+Jordan
+Pleaseworkforonce
+Damon Gallaty
+Trentin Bergeron
+Emarandzeb
+Laulajatar
+Dale McBane
+Chris Kohut
+Preston Mitchell
+Andrew Kircher
+Frank Fewkes
+Moist mongol
+Joshua Xiong
+Jan Bundesmann
+www15o
+Game Master Pro
+jason baldrick
+Exp1nt
+w
+Shubham Jakhotiya
+Braxton Istace
+LesterThePossum
+Rurikid
+ojacid .
+james
+A Patreon of the Ahts
+BigE0021
+Angelique Badger
+Jonathan Williams
+AntiBlock
+Redsauz
+Florian Kelber
+John Patrick Callahan Jr
+Alexandra Vesey
+Bas Kroot
+Dzmitry Malyshau
+PedanticSteve
+Josh Wren
+BLRageQuit
+Dario Spadavecchia
+Neutrix
+Markell
+Rocky
+Robert Landry
+Skylar Mangum-Turner
+Nick Mowry
+Anjen Pai
+Hope You're Well
+Alexandre Boivin
+Racussa
+Mike Conley
+Karen Blythe
+Mark Sprietsma
+Xavier privé
+Tommy Mayfield
+Václav Švec
+Binks
+Mackenzie
+Linn Browning
+Writer's Consultant Page by George J.Lekkas
+Andrew Hines
+Wexxler
+Jason Matthew Wuerfel
+Milo Cohen
+Alan Buehne
+Dominick Ormsby
+Espen Sæverud
+Rasmus Legêne
+rbbalderama
+Nobody679
+Prince of Morgoth
+Jaryd Armstrong
+Gary Smith
+ThyHolyDevil
+良义 金
+Andrew Pirkola
+Dig
+Chris Gray
+Tyshaun Wise
+Phoenix
+Ethan Cook
+Jordan Bellah
+Petro Lombaard
+Kass Frisson
+Lazer Elf
+Gavin Madrigal
+Rox
+PinkEvil
+Martin Lorber
+Emanuel Pietri
+Alex Beard
+Jeffrey Henning
+Eric Alexander Cartaya
+Dust Bunny
+GameNight
+Beingus
+Crys Cain
+Lon Varnadore
+Thomas Mortensen Hansen
+Drinarius
+Ed Wright
+Adrian Wright
+Zklaus
+Chris Bloom
+PlayByMail.Net
+Maxim Lowe
+Aquelion
+Tiber
+Daydream1013
+Page One Project
+Clonetone
+Egoensis
+Brad Wardell
+Heaven N Lee
+BarnabyJones
+Paul Ingram
+Lance Saba
+Chad Riley
+Austin
+Rowland Kingman
+Decimus Vitalis
+Grayson McClead
+Battleturtle1
+Kristin Chernoff
+Justin Mcclain
+Patrick Jones
+Esther Busch
+Chance Mena
+JimmyTheBob
+Antiroo
+Dalton Clark
+Guilherme Aguiar
+Simon Drapeau
+Akirsop
+Radovan Zapletal
+Vanessa Anjos
+Rikard Wolff
+Justa Badge
+teco 47
+Jake
+Miguel Alejandro
+Blargh Blarghmoomoo
+Jakob Siegel
+Grant A. Murray
+Jarno Hallikainen
+Jan Ka
+Joshua Vaught
+MaxOliver
+WarWizardGames
+Evan-DiLeo
+Eric Moore
+Kyle S
+Alex Debus
+Uniquenameosaurus
+Dean Dunakin
+Jack
+Bryan Brake
+McNeil Atticus Inksmudge
+Char
+Tom Van Orden jr
+Kendall Patterson
+Akylos
+Barna Csíkos
+Nicholas Grabstas
+OldFarkas
+Riley Seaman
+Daniel Gill
+Kyle Robertson
+Natasha Taylor
+Pierrick Bertrand
+Jared.K
+Dylan Devenny
+logic_error
+SashaTK
+Steve Johnson
+MontyBoosh
+Achillain
+Jaden
+Vito Martono
+Thirty-OneR
+Eric Foley
+ThatGuyGW
+Dee Chiu
+James H. Anthony
+Kevin Cossutta
+MadNomadMedia
+Darinius Dragonclaw Studios
+Tsahyla (Triston Lightyear)
+Christopher Whitney
+María Martín López
+Annie Rishor
+Aram Sabatés
+Jeppe Skov Jensen
+Martin Seeger
+Oneiris (Oni)
+EternalDeiwos
+Richard Keating
+StroboWolf
+Rick Falkvinge
+Zewen Senpai
+Adam Butler
+Kassidy
+Sadie Blackthorne
+ErrorForever
+Seth Fusion
+Gus
+Paul
+Lucid
+Allen Varney
+Hannah May
+Sankroh
+Eliot Miller
+Detocroix
+Meg Ziegler
+rob bee
+Anoplexian
+Marten F
+Erin D. Smale
+Johnpaul Morrow
+Roekai
+Drunken_Legends
+Jesse Holmes
+Maxwell Hill
+Jan Dvořák
+SirTobit
+G0atfather
+Allen S. Rout
+Pippa Mitchell
+Austin Miller
+Caner Oleas Pekgönenç
+Alison Bull Bear
+Bradley Edwards
+Tertiary
+Daniel
+Joshua E Goodwin
+Shaun Alexander
+Ryan Lege
+Myrrhlin
+Jesper Cockx
+Noirbard
+Dice
+Brian Drennen
+Giant Monster Games
+Reya C.
+Krk
+Endwords
+Jacob Harrington
+RK
+Michael Greiner
+Steven Bennett
+Brice Moss
+Whakomatic x
+Stephen Herron
+kosmobius
+ZizRenanim
+Barished
+Maur Razimtheth
+Aaron bateson
+Diklyquill
+Shawn Taylor
+Brady R Rathbun
+FlippantFeline
+Shadow
+J
+Tamashi Toh
+Huw Williams
+Graves
+ShadeByTheSea
+The Dungeon Masters
+Valerie Elise
+Empi3
+William Pucs
+Michael Carmody
+Marco Veldman
+naikibens220
+Jordon Phillips
+_gfx_
+F. Casanova
+Jared McDaris
+BlastWind
+Taldonix
+Connor McMartin
+Nexoness
+Guy
+Maggie
+AdvancedAzrielAngel
+Alfred García
+Norbert Žigmund
+Jennifer
+Titanium Tomes
+John Ackley
+Invad3r233
+Jonathan Killstring
+Jessica Thomas
+Nikita Kondratjuks
+Steve Hyatt
+PoliticsBuff
+Ian arless
+Karnat
+Hilton Williams
+Kevin
+Katharina Haase
+Hisham Bedri
+Bird
+JOSHUA QUALTIERI
+Preston Brooks
+Troy Schuler
+DerGeisterbär
+L. V. Werneck
+Marcus Hellyrr
+yami
+Daniel Eric Crosby
+Augusto Chiarle
+Doug Churchman
+David Roza
+Alexander Thomas
+Ashley Wilson-Savoury
+Nathan L Myers
+Theresa Walsh
+JP Roberts III
+William Henry
+OldbeanOldboy
+Javasharp
+Diagonath
+Gun Metal Games
+Scott Marner
+Alloyed Clavicle
+Valerii Matskevych
+Spencer Sherman
+Nolan Moore
+James Schellenger
+Pat
+Dino Princip
+Shawn Spencer
+Timothée CALLET
+KC138
+Nylian
+Kate
+Markus Finster
+CanadianGold
+AstralJacks
+Keith Marshall
+Scott Davis
+Joseph Miranda
+Shaptarshi Joarder
+Branndon
+EP
+Johan Fröberg
+Sasquatch
+Chase Mayers
+Sizz_TV
+Ryan Westcott
+Nathan Mitchell
+Curt Flood
+Mikey
+E.M. White
+Billy
+Vlad Tomash
+Xariun
+Luke Nelson
+W Maxwell Cassity-Guilliom
+Marty H
+Aaron Meyer
+Max Amillios
+chris
+cyninge
+Omegavoid
+Fritjof Olsson
+Crazypedia
+Duncan Thomson
+William Merriott
+Gold Tamarin
+Lhoris
+Jonathan
+Jon
+Massimo Vella
+Feuver
+aymeric
+Eric Schumann
+Rei
+Fondue
+Paavi1
+Wil Sisney
+David Patterson`;
diff --git a/modules/religions-generator.js b/modules/religions-generator.js
index d823b4c6..0ebd226d 100644
--- a/modules/religions-generator.js
+++ b/modules/religions-generator.js
@@ -304,15 +304,34 @@ window.Religions = (function () {
Heresy: {Heresy: 1}
};
- const methods = {
- "Random + type": 3,
- "Random + ism": 1,
- "Supreme + ism": 5,
- "Faith of + Supreme": 5,
- "Place + ism": 1,
- "Culture + ism": 2,
- "Place + ian + type": 6,
- "Culture + type": 4
+ const namingMethods = {
+ Folk: {
+ "Culture + type": 1
+ },
+
+ Organized: {
+ "Random + type": 3,
+ "Random + ism": 1,
+ "Supreme + ism": 5,
+ "Faith of + Supreme": 5,
+ "Place + ism": 1,
+ "Culture + ism": 2,
+ "Place + ian + type": 6,
+ "Culture + type": 4
+ },
+
+ Cult: {
+ "Burg + ian + type": 2,
+ "Random + ian + type": 1,
+ "Type + of the + meaning": 2
+ },
+
+ Heresy: {
+ "Burg + ian + type": 3,
+ "Random + ism": 3,
+ "Random + ian + type": 2,
+ "Type + of the + meaning": 1
+ }
};
const types = {
@@ -342,381 +361,416 @@ window.Religions = (function () {
}
};
- const generate = function () {
+ const expansionismMap = {
+ Folk: () => 0,
+ Organized: () => gauss(5, 3, 0, 10, 1),
+ Cult: () => gauss(0.5, 0.5, 0, 5, 1),
+ Heresy: () => gauss(1, 0.5, 0, 5, 1)
+ };
+
+ function generate() {
TIME && console.time("generateReligions");
- const {cells, states, cultures} = pack;
+ const lockedReligions = pack.religions?.filter(r => r.i && r.lock && !r.removed) || [];
- const religionIds = new Uint16Array(cells.culture); // cell religion; initially based on culture
- const religions = [];
+ const folkReligions = generateFolkReligions();
+ const organizedReligions = generateOrganizedReligions(+religionsInput.value, lockedReligions);
- // add folk religions
- pack.cultures.forEach(c => {
- const newId = c.i;
- if (!newId) return religions.push({i: 0, name: "No religion"});
+ const namedReligions = specifyReligions([...folkReligions, ...organizedReligions]);
+ const indexedReligions = combineReligions(namedReligions, lockedReligions);
+ const religionIds = expandReligions(indexedReligions);
+ const religions = defineOrigins(religionIds, indexedReligions);
- if (c.removed) {
- religions.push({
- i: c.i,
- name: "Extinct religion for " + c.name,
- color: getMixedColor(c.color, 0.1, 0),
- removed: true
- });
- return;
- }
-
- if (pack.religions) {
- const lockedFolkReligion = pack.religions.find(
- r => r.culture === c.i && !r.removed && r.lock && r.type === "Folk"
- );
-
- if (lockedFolkReligion) {
- for (const i of cells.i) {
- if (cells.religion[i] === lockedFolkReligion.i) religionIds[i] = newId;
- }
-
- lockedFolkReligion.i = newId;
- religions.push(lockedFolkReligion);
- return;
- }
- }
-
- const form = rw(forms.Folk);
- const name = c.name + " " + rw(types[form]);
- const deity = form === "Animism" ? null : getDeityName(c.i);
- const color = getMixedColor(c.color, 0.1, 0); // `url(#hatch${rand(8,13)})`;
- religions.push({
- i: newId,
- name,
- color,
- culture: newId,
- type: "Folk",
- form,
- deity,
- center: c.center,
- origins: [0]
- });
- });
-
- if (religionsInput.value == 0 || pack.cultures.length < 2)
- return religions.filter(r => r.i).forEach(r => (r.code = abbreviate(r.name)));
-
- const burgs = pack.burgs.filter(b => b.i && !b.removed);
- const sorted =
- burgs.length > +religionsInput.value
- ? burgs.sort((a, b) => b.population - a.population).map(b => b.cell)
- : cells.i.filter(i => cells.s[i] > 2).sort((a, b) => cells.s[b] - cells.s[a]);
-
- const religionsTree = d3.quadtree();
- const spacing = (graphWidth + graphHeight) / 6 / religionsInput.value; // base min distance between towns
- const cultsCount = Math.floor((rand(10, 40) / 100) * religionsInput.value);
- const count = +religionsInput.value - cultsCount + religions.length;
-
- function getReligionsInRadius({x, y, r, max}) {
- if (max === 0) return [0];
- const cellsInRadius = findAll(x, y, r);
- const religions = unique(cellsInRadius.map(i => religionIds[i]).filter(r => r));
- return religions.length ? religions.slice(0, max) : [0];
- }
-
- // restore locked non-folk religions
- if (pack.religions) {
- const lockedNonFolkReligions = pack.religions.filter(r => r.lock && !r.removed && r.type !== "Folk");
- for (const religion of lockedNonFolkReligions) {
- const newId = religions.length;
- for (const i of cells.i) {
- if (cells.religion[i] === religion.i) religionIds[i] = newId;
- }
-
- religion.i = newId;
- religion.origins = religion.origins.filter(origin => origin < newId);
- religionsTree.add(cells.p[religion.center]);
- religions.push(religion);
- }
- }
-
- // generate organized religions
- for (let i = 0; religions.length < count && i < 1000; i++) {
- let center = sorted[biased(0, sorted.length - 1, 5)]; // religion center
- const form = rw(forms.Organized);
- const state = cells.state[center];
- const culture = cells.culture[center];
-
- const deity = form === "Non-theism" ? null : getDeityName(culture);
- let [name, expansion] = getReligionName(form, deity, center);
- if (expansion === "state" && !state) expansion = "global";
- if (expansion === "culture" && !culture) expansion = "global";
-
- if (expansion === "state" && Math.random() > 0.5) center = states[state].center;
- if (expansion === "culture" && Math.random() > 0.5) center = cultures[culture].center;
-
- if (!cells.burg[center] && cells.c[center].some(c => cells.burg[c]))
- center = cells.c[center].find(c => cells.burg[c]);
- const [x, y] = cells.p[center];
-
- const s = spacing * gauss(1, 0.3, 0.2, 2, 2); // randomize to make the placement not uniform
- if (religionsTree.find(x, y, s) !== undefined) continue; // to close to existing religion
-
- // add "Old" to name of the folk religion on this culture
- const isFolkBased = expansion === "culture" || P(0.5);
- const folk = isFolkBased && religions.find(r => r.culture === culture && r.type === "Folk");
- if (folk && expansion === "culture" && folk.name.slice(0, 3) !== "Old") folk.name = "Old " + folk.name;
-
- const origins = folk ? [folk.i] : getReligionsInRadius({x, y, r: 150 / count, max: 2});
- const expansionism = rand(3, 8);
- const baseColor = religions[culture]?.color || states[state]?.color || getRandomColor();
- const color = getMixedColor(baseColor, 0.3, 0);
-
- religions.push({
- i: religions.length,
- name,
- color,
- culture,
- type: "Organized",
- form,
- deity,
- expansion,
- expansionism,
- center,
- origins
- });
- religionsTree.add([x, y]);
- }
-
- // generate cults
- for (let i = 0; religions.length < count + cultsCount && i < 1000; i++) {
- const form = rw(forms.Cult);
- let center = sorted[biased(0, sorted.length - 1, 1)]; // religion center
- if (!cells.burg[center] && cells.c[center].some(c => cells.burg[c]))
- center = cells.c[center].find(c => cells.burg[c]);
- const [x, y] = cells.p[center];
-
- const s = spacing * gauss(2, 0.3, 1, 3, 2); // randomize to make the placement not uniform
- if (religionsTree.find(x, y, s) !== undefined) continue; // to close to existing religion
-
- const culture = cells.culture[center];
- const origins = getReligionsInRadius({x, y, r: 300 / count, max: rand(0, 4)});
-
- const deity = getDeityName(culture);
- const name = getCultName(form, center);
- const expansionism = gauss(1.1, 0.5, 0, 5);
- const color = getMixedColor(cultures[culture].color, 0.5, 0); // "url(#hatch7)";
- religions.push({
- i: religions.length,
- name,
- color,
- culture,
- type: "Cult",
- form,
- deity,
- expansion: "global",
- expansionism,
- center,
- origins
- });
- religionsTree.add([x, y]);
- }
-
- expandReligions();
-
- // generate heresies
- religions
- .filter(r => r.type === "Organized")
- .forEach(r => {
- if (r.expansionism < 3) return;
- const count = gauss(0, 1, 0, 3);
- for (let i = 0; i < count; i++) {
- let center = ra(cells.i.filter(i => religionIds[i] === r.i && cells.c[i].some(c => religionIds[c] !== r.i)));
- if (!center) continue;
- if (!cells.burg[center] && cells.c[center].some(c => cells.burg[c]))
- center = cells.c[center].find(c => cells.burg[c]);
- const [x, y] = cells.p[center];
- if (religionsTree.find(x, y, spacing / 10) !== undefined) continue; // to close to other
-
- const culture = cells.culture[center];
- const name = getCultName("Heresy", center);
- const expansionism = gauss(1.2, 0.5, 0, 5);
- const color = getMixedColor(r.color, 0.4, 0.2); // "url(#hatch6)";
- religions.push({
- i: religions.length,
- name,
- color,
- culture,
- type: "Heresy",
- form: r.form,
- deity: r.deity,
- expansion: "global",
- expansionism,
- center,
- origins: [r.i]
- });
- religionsTree.add([x, y]);
- }
- });
-
- expandHeresies();
+ pack.religions = religions;
+ pack.cells.religion = religionIds;
checkCenters();
- cells.religion = religionIds;
- pack.religions = religions;
-
TIME && console.timeEnd("generateReligions");
+ }
- // growth algorithm to assign cells to religions
- function expandReligions() {
- const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
- const cost = [];
+ function generateFolkReligions() {
+ return pack.cultures
+ .filter(c => c.i && !c.removed)
+ .map(culture => ({type: "Folk", form: rw(forms.Folk), culture: culture.i, center: culture.center}));
+ }
- religions
- .filter(r => !r.lock && (r.type === "Organized" || r.type === "Cult"))
- .forEach(r => {
- religionIds[r.center] = r.i;
- queue.queue({e: r.center, p: 0, r: r.i, s: cells.state[r.center], c: r.culture});
- cost[r.center] = 1;
- });
+ function generateOrganizedReligions(desiredReligionNumber, lockedReligions) {
+ const cells = pack.cells;
+ const lockedReligionCount = lockedReligions.filter(({type}) => type !== "Folk").length || 0;
+ const requiredReligionsNumber = desiredReligionNumber - lockedReligionCount;
+ if (requiredReligionsNumber < 1) return [];
- const neutral = (cells.i.length / 5000) * 200 * gauss(1, 0.3, 0.2, 2, 2) * neutralInput.value; // limit cost for organized religions growth
- const popCost = d3.max(cells.pop) / 3; // enougth population to spered religion without penalty
+ const candidateCells = getCandidateCells();
+ const religionCores = placeReligions();
- while (queue.length) {
- const {e, p, r, c, s} = queue.dequeue();
- const expansion = religions[r].expansion;
+ const cultsCount = Math.floor((rand(1, 4) / 10) * religionCores.length); // 10-40%
+ const heresiesCount = Math.floor((rand(0, 3) / 10) * religionCores.length); // 0-30%
+ const organizedCount = religionCores.length - cultsCount - heresiesCount;
- cells.c[e].forEach(nextCell => {
- if (expansion === "culture" && c !== cells.culture[nextCell]) return;
- if (expansion === "state" && s !== cells.state[nextCell]) return;
- if (religions[religionIds[nextCell]]?.lock) return;
+ const getType = index => {
+ if (index < organizedCount) return "Organized";
+ if (index < organizedCount + cultsCount) return "Cult";
+ return "Heresy";
+ };
- const cultureCost = c !== cells.culture[nextCell] ? 10 : 0;
- const stateCost = s !== cells.state[nextCell] ? 10 : 0;
- const biomeCost = cells.road[nextCell] ? 1 : biomesData.cost[cells.biome[nextCell]];
- const populationCost = Math.max(rn(popCost - cells.pop[nextCell]), 0);
- const heightCost = Math.max(cells.h[nextCell], 20) - 20;
- const waterCost = cells.h[nextCell] < 20 ? (cells.road[nextCell] ? 50 : 1000) : 0;
- const totalCost =
- p +
- (cultureCost + stateCost + biomeCost + populationCost + heightCost + waterCost) / religions[r].expansionism;
- if (totalCost > neutral) return;
+ return religionCores.map((cellId, index) => {
+ const type = getType(index);
+ const form = rw(forms[type]);
+ const cultureId = cells.culture[cellId];
- if (!cost[nextCell] || totalCost < cost[nextCell]) {
- if (cells.h[nextCell] >= 20 && cells.culture[nextCell]) religionIds[nextCell] = r; // assign religion to cell
- cost[nextCell] = totalCost;
- queue.queue({e: nextCell, p: totalCost, r, c, s});
- }
- });
+ return {type, form, culture: cultureId, center: cellId};
+ });
+
+ function placeReligions() {
+ const religionCells = [];
+ const religionsTree = d3.quadtree();
+
+ // pre-populate with locked centers
+ lockedReligions.forEach(({center}) => religionsTree.add(cells.p[center]));
+
+ // min distance between religion inceptions
+ const spacing = (graphWidth + graphHeight) / 2 / desiredReligionNumber;
+
+ for (const cellId of candidateCells) {
+ const [x, y] = cells.p[cellId];
+
+ if (religionsTree.find(x, y, spacing) === undefined) {
+ religionCells.push(cellId);
+ religionsTree.add([x, y]);
+
+ if (religionCells.length === requiredReligionsNumber) return religionCells;
+ }
+ }
+
+ WARN && console.warn(`Placed only ${religionCells.length} of ${requiredReligionsNumber} religions`);
+ return religionCells;
+ }
+
+ function getCandidateCells() {
+ const validBurgs = pack.burgs.filter(b => b.i && !b.removed);
+
+ if (validBurgs.length >= requiredReligionsNumber)
+ return validBurgs.sort((a, b) => b.population - a.population).map(burg => burg.cell);
+ return cells.i.filter(i => cells.s[i] > 2).sort((a, b) => cells.s[b] - cells.s[a]);
+ }
+ }
+
+ function specifyReligions(newReligions) {
+ const {cells, cultures} = pack;
+
+ const rawReligions = newReligions.map(({type, form, culture: cultureId, center}) => {
+ const supreme = getDeityName(cultureId);
+ const deity = form === "Non-theism" || form === "Animism" ? null : supreme;
+
+ const stateId = cells.state[center];
+
+ let [name, expansion] = generateReligionName(type, form, supreme, center);
+ if (expansion === "state" && !stateId) expansion = "global";
+
+ const expansionism = expansionismMap[type]();
+ const color = getReligionColor(cultures[cultureId], type);
+
+ return {name, type, form, culture: cultureId, center, deity, expansion, expansionism, color};
+ });
+
+ return rawReligions;
+
+ function getReligionColor(culture, type) {
+ if (!culture.i) return getRandomColor();
+
+ if (type === "Folk") return culture.color;
+ if (type === "Heresy") return getMixedColor(culture.color, 0.35, 0.2);
+ if (type === "Cult") return getMixedColor(culture.color, 0.5, 0);
+ return getMixedColor(culture.color, 0.25, 0.4);
+ }
+ }
+
+ // indexes, conditionally renames, and abbreviates religions
+ function combineReligions(namedReligions, lockedReligions) {
+ const indexedReligions = [{name: "No religion", i: 0}];
+
+ const {lockedReligionQueue, highestLockedIndex, codes, numberLockedFolk} = parseLockedReligions();
+ const maxIndex = Math.max(
+ highestLockedIndex,
+ namedReligions.length + lockedReligions.length + 1 - numberLockedFolk
+ );
+
+ for (let index = 1, progress = 0; index < maxIndex; index = indexedReligions.length) {
+ // place locked religion back at its old index
+ if (index === lockedReligionQueue[0]?.i) {
+ const nextReligion = lockedReligionQueue.shift();
+ indexedReligions.push(nextReligion);
+ continue;
+ }
+
+ // slot the new religions
+ if (progress < namedReligions.length) {
+ const nextReligion = namedReligions[progress];
+ progress++;
+
+ if (
+ nextReligion.type === "Folk" &&
+ lockedReligions.some(({type, culture}) => type === "Folk" && culture === nextReligion.culture)
+ )
+ continue; // when there is a locked Folk religion for this culture discard duplicate
+
+ const newName = renameOld(nextReligion);
+ const code = abbreviate(newName, codes);
+ codes.push(code);
+ indexedReligions.push({...nextReligion, i: index, name: newName, code});
+ continue;
+ }
+
+ indexedReligions.push({i: index, type: "Folk", culture: 0, name: "Removed religion", removed: true});
+ }
+ return indexedReligions;
+
+ function parseLockedReligions() {
+ // copy and sort the locked religions list
+ const lockedReligionQueue = lockedReligions
+ .map(religion => {
+ // and filter their origins to locked religions
+ let newOrigin = religion.origins.filter(n => lockedReligions.some(({i: index}) => index === n));
+ if (newOrigin === []) newOrigin = [0];
+ return {...religion, origins: newOrigin};
+ })
+ .sort((a, b) => a.i - b.i);
+
+ const highestLockedIndex = Math.max(...lockedReligions.map(r => r.i));
+ const codes = lockedReligions.length > 0 ? lockedReligions.map(r => r.code) : [];
+ const numberLockedFolk = lockedReligions.filter(({type}) => type === "Folk").length;
+
+ return {lockedReligionQueue, highestLockedIndex, codes, numberLockedFolk};
+ }
+
+ // prepend 'Old' to names of folk religions which have organized competitors
+ function renameOld({name, type, culture: cultureId}) {
+ if (type !== "Folk") return name;
+
+ const haveOrganized =
+ namedReligions.some(
+ ({type, culture, expansion}) => culture === cultureId && type === "Organized" && expansion === "culture"
+ ) ||
+ lockedReligions.some(
+ ({type, culture, expansion}) => culture === cultureId && type === "Organized" && expansion === "culture"
+ );
+ if (haveOrganized && name.slice(0, 3) !== "Old") return `Old ${name}`;
+ return name;
+ }
+ }
+
+ // finally generate and stores origins trees
+ function defineOrigins(religionIds, indexedReligions) {
+ const religionOriginsParamsMap = {
+ Organized: {clusterSize: 100, maxReligions: 2},
+ Cult: {clusterSize: 50, maxReligions: 3},
+ Heresy: {clusterSize: 50, maxReligions: 4}
+ };
+
+ const origins = indexedReligions.map(({i, type, culture: cultureId, expansion, center}) => {
+ if (i === 0) return null; // no religion
+ if (type === "Folk") return [0]; // folk religions originate from its parent culture only
+
+ const folkReligion = indexedReligions.find(({culture, type}) => type === "Folk" && culture === cultureId);
+ const isFolkBased = folkReligion && cultureId && expansion === "culture" && each(2)(center);
+ if (isFolkBased) return [folkReligion.i];
+
+ const {clusterSize, maxReligions} = religionOriginsParamsMap[type];
+ const fallbackOrigin = folkReligion?.i || 0;
+ return getReligionsInRadius(pack.cells.c, center, religionIds, i, clusterSize, maxReligions, fallbackOrigin);
+ });
+
+ return indexedReligions.map((religion, index) => ({...religion, origins: origins[index]}));
+ }
+
+ function getReligionsInRadius(neighbors, center, religionIds, religionId, clusterSize, maxReligions, fallbackOrigin) {
+ const foundReligions = new Set();
+ const queue = [center];
+ const checked = {};
+
+ for (let size = 0; queue.length && size < clusterSize; size++) {
+ const cellId = queue.shift();
+ checked[cellId] = true;
+
+ for (const neibId of neighbors[cellId]) {
+ if (checked[neibId]) continue;
+ checked[neibId] = true;
+
+ const neibReligion = religionIds[neibId];
+ if (neibReligion && neibReligion < religionId) foundReligions.add(neibReligion);
+ if (foundReligions.size >= maxReligions) return [...foundReligions];
+ queue.push(neibId);
}
}
- // growth algorithm to assign cells to heresies
- function expandHeresies() {
- const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
- const cost = [];
+ return foundReligions.size ? [...foundReligions] : [fallbackOrigin];
+ }
- religions
- .filter(r => !r.lock && r.type === "Heresy")
- .forEach(r => {
- const b = religionIds[r.center]; // "base" religion id
- religionIds[r.center] = r.i; // heresy id
- queue.queue({e: r.center, p: 0, r: r.i, b});
- cost[r.center] = 1;
- });
+ // growth algorithm to assign cells to religions
+ function expandReligions(religions) {
+ const cells = pack.cells;
+ const religionIds = spreadFolkReligions(religions);
- const neutral = (cells.i.length / 5000) * 500 * neutralInput.value; // limit cost for heresies growth
+ const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
+ const cost = [];
- while (queue.length) {
- const {e, p, r, b} = queue.dequeue();
+ const maxExpansionCost = (cells.i.length / 20) * neutralInput.value; // limit cost for organized religions growth
- cells.c[e].forEach(nextCell => {
- if (religions[religionIds[nextCell]]?.lock) return;
- const religionCost = religionIds[nextCell] === b ? 0 : 2000;
- const biomeCost = cells.road[nextCell] ? 0 : biomesData.cost[cells.biome[nextCell]];
- const heightCost = Math.max(cells.h[nextCell], 20) - 20;
- const waterCost = cells.h[nextCell] < 20 ? (cells.road[nextCell] ? 50 : 1000) : 0;
- const totalCost =
- p + (religionCost + biomeCost + heightCost + waterCost) / Math.max(religions[r].expansionism, 0.1);
+ const biomePassageCost = cellId => biomesData.cost[cells.biome[cellId]];
- if (totalCost > neutral) return;
+ religions
+ .filter(r => r.i && !r.lock && r.type !== "Folk" && !r.removed)
+ .forEach(r => {
+ religionIds[r.center] = r.i;
+ queue.queue({e: r.center, p: 0, r: r.i, s: cells.state[r.center]});
+ cost[r.center] = 1;
+ });
- if (!cost[nextCell] || totalCost < cost[nextCell]) {
- if (cells.h[nextCell] >= 20 && cells.culture[nextCell]) religionIds[nextCell] = r; // assign religion to cell
- cost[nextCell] = totalCost;
- queue.queue({e: nextCell, p: totalCost, r});
- }
- });
- }
- }
+ const religionsMap = new Map(religions.map(r => [r.i, r]));
- function checkCenters() {
- const codes = religions.map(r => r.code);
- religions.forEach(r => {
- if (!r.i) return;
- r.code = abbreviate(r.name, codes);
+ const isMainRoad = cellId => cells.road[cellId] - cells.crossroad[cellId] > 4;
+ const isTrail = cellId => cells.h[cellId] > 19 && cells.road[cellId] - cells.crossroad[cellId] === 1;
+ const isSeaRoute = cellId => cells.h[cellId] < 20 && cells.road[cellId];
+ const isWater = cellId => cells.h[cellId] < 20;
- // move religion center if it's not within religion area after expansion
- if (religionIds[r.center] === r.i) return; // in area
- const firstCell = cells.i.find(i => religionIds[i] === r.i);
- if (firstCell) r.center = firstCell; // move center, othervise it's an extinct religion
+ while (queue.length) {
+ const {e: cellId, p, r, s: state} = queue.dequeue();
+ const {culture, expansion, expansionism} = religionsMap.get(r);
+
+ cells.c[cellId].forEach(nextCell => {
+ if (expansion === "culture" && culture !== cells.culture[nextCell]) return;
+ if (expansion === "state" && state !== cells.state[nextCell]) return;
+ if (religionsMap.get(religionIds[nextCell])?.lock) return;
+
+ const cultureCost = culture !== cells.culture[nextCell] ? 10 : 0;
+ const stateCost = state !== cells.state[nextCell] ? 10 : 0;
+ const passageCost = getPassageCost(nextCell);
+
+ const cellCost = cultureCost + stateCost + passageCost;
+ const totalCost = p + 10 + cellCost / expansionism;
+ if (totalCost > maxExpansionCost) return;
+
+ if (!cost[nextCell] || totalCost < cost[nextCell]) {
+ if (cells.culture[nextCell]) religionIds[nextCell] = r; // assign religion to cell
+ cost[nextCell] = totalCost;
+
+ queue.queue({e: nextCell, p: totalCost, r, s: state});
+ }
});
}
- };
+
+ return religionIds;
+
+ function getPassageCost(cellId) {
+ if (isWater(cellId)) return isSeaRoute ? 50 : 500;
+ if (isMainRoad(cellId)) return 1;
+ const biomeCost = biomePassageCost(cellId);
+ return isTrail(cellId) ? biomeCost / 1.5 : biomeCost;
+ }
+ }
+
+ // folk religions initially get all cells of their culture, and locked religions are retained
+ function spreadFolkReligions(religions) {
+ const cells = pack.cells;
+ const hasPrior = cells.religion && true;
+ const religionIds = new Uint16Array(cells.i.length);
+
+ const folkReligions = religions.filter(religion => religion.type === "Folk" && !religion.removed);
+ const cultureToReligionMap = new Map(folkReligions.map(({i, culture}) => [culture, i]));
+
+ for (const cellId of cells.i) {
+ const oldId = (hasPrior && cells.religion[cellId]) || 0;
+ if (oldId && religions[oldId]?.lock && !religions[oldId]?.removed) {
+ religionIds[cellId] = oldId;
+ continue;
+ }
+ const cultureId = cells.culture[cellId];
+ religionIds[cellId] = cultureToReligionMap.get(cultureId) || 0;
+ }
+
+ return religionIds;
+ }
+
+ function checkCenters() {
+ const cells = pack.cells;
+ pack.religions.forEach(r => {
+ if (!r.i) return;
+ // move religion center if it's not within religion area after expansion
+ if (cells.religion[r.center] === r.i) return; // in area
+ const firstCell = cells.i.find(i => cells.religion[i] === r.i);
+ const cultureHome = pack.cultures[r.culture]?.center;
+ if (firstCell) r.center = firstCell; // move center, othervise it's an extinct religion
+ else if (r.type === "Folk" && cultureHome) r.center = cultureHome; // reset extinct culture centers
+ });
+ }
+
+ function recalculate() {
+ const newReligionIds = expandReligions(pack.religions);
+ pack.cells.religion = newReligionIds;
+
+ checkCenters();
+ }
const add = function (center) {
- const {cells, religions} = pack;
+ const {cells, cultures, religions} = pack;
const religionId = cells.religion[center];
+ const i = religions.length;
- const culture = cells.culture[center];
- const color = getMixedColor(religions[religionId].color, 0.3, 0);
+ const cultureId = cells.culture[center];
+ const missingFolk =
+ cultureId !== 0 &&
+ !religions.some(({type, culture, removed}) => type === "Folk" && culture === cultureId && !removed);
+ const color = missingFolk ? cultures[cultureId].color : getMixedColor(religions[religionId].color, 0.3, 0);
- const type =
- religions[religionId].type === "Organized" ? rw({Organized: 4, Cult: 1, Heresy: 2}) : rw({Organized: 5, Cult: 2});
+ const type = missingFolk
+ ? "Folk"
+ : religions[religionId].type === "Organized"
+ ? rw({Organized: 4, Cult: 1, Heresy: 2})
+ : rw({Organized: 5, Cult: 2});
const form = rw(forms[type]);
const deity =
- type === "Heresy" ? religions[religionId].deity : form === "Non-theism" ? null : getDeityName(culture);
+ type === "Heresy"
+ ? religions[religionId].deity
+ : form === "Non-theism" || form === "Animism"
+ ? null
+ : getDeityName(cultureId);
- let name, expansion;
- if (type === "Organized") [name, expansion] = getReligionName(form, deity, center);
- else {
- name = getCultName(form, center);
- expansion = "global";
- }
+ const [name, expansion] = generateReligionName(type, form, deity, center);
const formName = type === "Heresy" ? religions[religionId].form : form;
const code = abbreviate(
name,
religions.map(r => r.code)
);
+ const influences = getReligionsInRadius(cells.c, center, cells.religion, i, 25, 3, 0);
+ const origins = type === "Folk" ? [0] : influences;
- const i = religions.length;
religions.push({
i,
name,
color,
- culture,
+ culture: cultureId,
type,
form: formName,
deity,
expansion,
- expansionism: 0,
+ expansionism: expansionismMap[type](),
center,
cells: 0,
area: 0,
rural: 0,
urban: 0,
- origins: [religionId],
+ origins,
code
});
cells.religion[center] = i;
};
function updateCultures() {
- TIME && console.time("updateCulturesForReligions");
pack.religions = pack.religions.map((religion, index) => {
- if (index === 0) {
- return religion;
- }
+ if (index === 0) return religion;
return {...religion, culture: pack.cells.culture[religion.center]};
});
- TIME && console.timeEnd("updateCulturesForReligions");
}
// get supreme deity name
@@ -735,22 +789,24 @@ window.Religions = (function () {
if (a === "Number") return ra(base.number);
if (a === "Being") return ra(base.being);
if (a === "Adjective") return ra(base.adjective);
- if (a === "Color + Animal") return ra(base.color) + " " + ra(base.animal);
- if (a === "Adjective + Animal") return ra(base.adjective) + " " + ra(base.animal);
- if (a === "Adjective + Being") return ra(base.adjective) + " " + ra(base.being);
- if (a === "Adjective + Genitive") return ra(base.adjective) + " " + ra(base.genitive);
- if (a === "Color + Being") return ra(base.color) + " " + ra(base.being);
- if (a === "Color + Genitive") return ra(base.color) + " " + ra(base.genitive);
- if (a === "Being + of + Genitive") return ra(base.being) + " of " + ra(base.genitive);
- if (a === "Being + of the + Genitive") return ra(base.being) + " of the " + ra(base.theGenitive);
- if (a === "Animal + of + Genitive") return ra(base.animal) + " of " + ra(base.genitive);
+ if (a === "Color + Animal") return `${ra(base.color)} ${ra(base.animal)}`;
+ if (a === "Adjective + Animal") return `${ra(base.adjective)} ${ra(base.animal)}`;
+ if (a === "Adjective + Being") return `${ra(base.adjective)} ${ra(base.being)}`;
+ if (a === "Adjective + Genitive") return `${ra(base.adjective)} ${ra(base.genitive)}`;
+ if (a === "Color + Being") return `${ra(base.color)} ${ra(base.being)}`;
+ if (a === "Color + Genitive") return `${ra(base.color)} ${ra(base.genitive)}`;
+ if (a === "Being + of + Genitive") return `${ra(base.being)} of ${ra(base.genitive)}`;
+ if (a === "Being + of the + Genitive") return `${ra(base.being)} of the ${ra(base.theGenitive)}`;
+ if (a === "Animal + of + Genitive") return `${ra(base.animal)} of ${ra(base.genitive)}`;
if (a === "Adjective + Being + of + Genitive")
- return ra(base.adjective) + " " + ra(base.being) + " of " + ra(base.genitive);
+ return `${ra(base.adjective)} ${ra(base.being)} of ${ra(base.genitive)}`;
if (a === "Adjective + Animal + of + Genitive")
- return ra(base.adjective) + " " + ra(base.animal) + " of " + ra(base.genitive);
+ return `${ra(base.adjective)} ${ra(base.animal)} of ${ra(base.genitive)}`;
+
+ ERROR && console.error("Unkown generation approach");
}
- function getReligionName(form, deity, center) {
+ function generateReligionName(variety, form, deity, center) {
const {cells, cultures, burgs, states} = pack;
const random = () => Names.getCulture(cells.culture[center], null, null, "", 0);
@@ -766,7 +822,7 @@ window.Religions = (function () {
return adj ? getAdjective(name) : name;
};
- const m = rw(methods);
+ const m = rw(namingMethods[variety]);
if (m === "Random + type") return [random() + " " + type(), "global"];
if (m === "Random + ism") return [trimVowels(random()) + "ism", "global"];
if (m === "Supreme + ism" && deity) return [trimVowels(supreme()) + "ism", "global"];
@@ -776,24 +832,11 @@ window.Religions = (function () {
if (m === "Culture + ism") return [trimVowels(culture()) + "ism", "culture"];
if (m === "Place + ian + type") return [place("adj") + " " + type(), "state"];
if (m === "Culture + type") return [culture() + " " + type(), "culture"];
+ if (m === "Burg + ian + type") return [`${place("adj")} ${type()}`, "global"];
+ if (m === "Random + ian + type") return [`${getAdjective(random())} ${type()}`, "global"];
+ if (m === "Type + of the + meaning") return [`${type()} of the ${generateMeaning()}`, "global"];
return [trimVowels(random()) + "ism", "global"]; // else
}
- function getCultName(form, center) {
- const cells = pack.cells;
- const type = function () {
- return rw(types[form]);
- };
- const random = function () {
- return trimVowels(Names.getCulture(cells.culture[center], null, null, "", 0).split(/[ ,]+/)[0]);
- };
- const burg = function () {
- return trimVowels(pack.burgs[cells.burg[center]].name.split(/[ ,]+/)[0]);
- };
- if (cells.burg[center]) return burg() + "ian " + type();
- if (Math.random() > 0.5) return random() + "ian " + type();
- return type() + " of the " + generateMeaning();
- }
-
- return {generate, add, getDeityName, updateCultures};
+ return {generate, add, getDeityName, updateCultures, recalculate};
})();
diff --git a/modules/river-generator.js b/modules/river-generator.js
index 957fe6fc..fd8a95b4 100644
--- a/modules/river-generator.js
+++ b/modules/river-generator.js
@@ -48,7 +48,9 @@ window.Rivers = (function () {
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) : [];
+ const lakes = lakeOutCells[i]
+ ? features.filter(feature => i === feature.outCell && feature.flux > feature.evaporation)
+ : [];
for (const lake of lakes) {
const lakeCell = cells.c[i].find(c => h[c] < 20 && cells.f[c] === lake.i);
cells.fl[lakeCell] += Math.max(lake.flux - lake.evaporation, 0); // not evaporated lake water drains to outlet
@@ -191,7 +193,18 @@ window.Rivers = (function () {
const length = getApproximateLength(meanderedPoints);
const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, 0));
- pack.rivers.push({i: riverId, source, mouth, discharge, length, width, widthFactor, sourceWidth: 0, parent, cells: riverCells});
+ pack.rivers.push({
+ i: riverId,
+ source,
+ mouth,
+ discharge,
+ length,
+ width,
+ widthFactor,
+ sourceWidth: 0,
+ parent,
+ cells: riverCells
+ });
}
}
@@ -479,6 +492,10 @@ window.Rivers = (function () {
return getBasin(parent);
};
+ const getNextId = function (rivers) {
+ return rivers.length ? Math.max(...rivers.map(r => r.i)) + 1 : 1;
+ };
+
return {
generate,
alterHeights,
@@ -493,6 +510,7 @@ window.Rivers = (function () {
getOffset,
getApproximateLength,
getRiverPoints,
- remove
+ remove,
+ getNextId
};
})();
diff --git a/modules/ui/editors.js b/modules/ui/editors.js
index 74ed5906..49a525eb 100644
--- a/modules/ui/editors.js
+++ b/modules/ui/editors.js
@@ -1176,18 +1176,18 @@ function refreshAllEditors() {
// dynamically loaded editors
async function editStates() {
if (customization) return;
- const Editor = await import("../dynamic/editors/states-editor.js?v=12062022");
+ const Editor = await import("../dynamic/editors/states-editor.js?v=1.89.05");
Editor.open();
}
async function editCultures() {
if (customization) return;
- const Editor = await import("../dynamic/editors/cultures-editor.js?v=1.88.06");
+ const Editor = await import("../dynamic/editors/cultures-editor.js?v=1.89.09");
Editor.open();
}
async function editReligions() {
if (customization) return;
- const Editor = await import("../dynamic/editors/religions-editor.js?v=1.88.06");
+ const Editor = await import("../dynamic/editors/religions-editor.js?v=1.89.10");
Editor.open();
}
diff --git a/modules/ui/heightmap-editor.js b/modules/ui/heightmap-editor.js
index 07216f90..bd41cefe 100644
--- a/modules/ui/heightmap-editor.js
+++ b/modules/ui/heightmap-editor.js
@@ -204,6 +204,13 @@ function editHeightmap(options) {
INFO && console.group("Edit Heightmap");
TIME && console.time("regenerateErasedData");
+ // remove data
+ pack.cultures = [];
+ pack.burgs = [];
+ pack.states = [];
+ pack.provinces = [];
+ pack.religions = [];
+
const erosionAllowed = allowErosion.checked;
markFeatures();
markupGridOcean();
@@ -231,8 +238,10 @@ function editHeightmap(options) {
Lakes.defineGroup();
defineBiomes();
rankCells();
+
Cultures.generate();
Cultures.expand();
+
BurgsAndStates.generate();
Religions.generate();
BurgsAndStates.defineStateForms();
diff --git a/modules/ui/ice-editor.js b/modules/ui/ice-editor.js
index f07cb6f9..a9e6ff28 100644
--- a/modules/ui/ice-editor.js
+++ b/modules/ui/ice-editor.js
@@ -67,11 +67,11 @@ function editIce() {
function addIcebergOnClick() {
const [x, y] = d3.mouse(this);
const i = findGridCell(x, y, grid);
- const c = grid.points[i];
- const s = +document.getElementById("iceSize").value;
+ const [cx, cy] = grid.points[i];
+ const size = +document.getElementById("iceSize")?.value || 1;
- const points = getGridPolygon(i).map(p => [(p[0] + (c[0] - p[0]) / s) | 0, (p[1] + (c[1] - p[1]) / s) | 0]);
- const iceberg = ice.append("polygon").attr("points", points).attr("cell", i).attr("size", s);
+ const points = getGridPolygon(i).map(([x, y]) => [rn(lerp(cx, x, size), 2), rn(lerp(cy, y, size), 2)]);
+ const iceberg = ice.append("polygon").attr("points", points).attr("cell", i).attr("size", size);
iceberg.call(d3.drag().on("drag", dragElement));
if (d3.event.shiftKey === false) toggleAdd();
}
diff --git a/modules/ui/layers.js b/modules/ui/layers.js
index 28b6ef57..3ea64e72 100644
--- a/modules/ui/layers.js
+++ b/modules/ui/layers.js
@@ -671,11 +671,10 @@ function toggleIce(event) {
}
function drawIce() {
- const cells = grid.cells,
- vertices = grid.vertices,
- n = cells.i.length,
- temp = cells.temp,
- h = cells.h;
+ const {cells, vertices} = grid;
+ const {temp, h} = cells;
+ const n = cells.i.length;
+
const used = new Uint8Array(cells.i.length);
Math.random = aleaPRNG(seed);
@@ -700,23 +699,22 @@ function drawIce() {
continue;
}
+ const tNormalized = normalize(t, -8, 2);
+ const randomFactor = t > -5 ? 0.4 + rand() * 1.2 : 1;
+
// mildly cold: iceberd
- if (P(normalize(t, -7, 2.5))) continue; // t[-5; 2] cold: skip some cells
+ if (P(tNormalized ** 0.5 * randomFactor)) continue; // cold: skip some cells
if (grid.features[cells.f[i]].type === "lake") continue; // lake: no icebers
- let size = (6.5 + t) / 10; // iceberg size: 0 = full size, 1 = zero size
- if (cells.t[i] === -1) size *= 1.3; // coasline: smaller icebers
- size = Math.min(size * (0.4 + rand() * 1.2), 0.95); // randomize iceberg size
- resizePolygon(i, size);
+
+ let size = 1 - tNormalized; // iceberg size: 0 = zero size, 1 = full size
+ if (cells.t[i] === -1) size /= 1.3; // coasline: smaller icebers
+ resizePolygon(i, minmax(rn(size * randomFactor, 2), 0.08, 1));
}
- function resizePolygon(i, s) {
- const c = grid.points[i];
- const points = getGridPolygon(i).map(p => [(p[0] + (c[0] - p[0]) * s) | 0, (p[1] + (c[1] - p[1]) * s) | 0]);
- ice
- .append("polygon")
- .attr("points", points)
- .attr("cell", i)
- .attr("size", rn(1 - s, 2));
+ function resizePolygon(i, size) {
+ const [cx, cy] = grid.points[i];
+ const points = getGridPolygon(i).map(([x, y]) => [rn(lerp(cx, x, size), 2), rn(lerp(cy, y, size), 2)]);
+ ice.append("polygon").attr("points", points).attr("cell", i).attr("size", size);
}
// connect vertices to chain
diff --git a/modules/ui/notes-editor.js b/modules/ui/notes-editor.js
index 95499433..a28885b9 100644
--- a/modules/ui/notes-editor.js
+++ b/modules/ui/notes-editor.js
@@ -42,12 +42,11 @@ function editNotes(id, name) {
$("#notesEditor").dialog({
title: "Notes Editor",
- width: "minmax(80vw, 540px)",
+ width: window.innerWidth * 0.8,
height: window.innerHeight * 0.75,
position: {my: "center", at: "center", of: "svg"},
close: removeEditor
});
- $("[aria-describedby='notesEditor']").css("top", "10vh");
if (modules.editNotes) return;
modules.editNotes = true;
diff --git a/modules/ui/options.js b/modules/ui/options.js
index f8ccfb38..ea7f596e 100644
--- a/modules/ui/options.js
+++ b/modules/ui/options.js
@@ -77,12 +77,15 @@ document
// show popup with a list of Patreon supportes (updated manually)
async function showSupporters() {
const {supporters} = await import("../dynamic/supporters.js?v=19062022");
+ const list = supporters.split("\n").sort();
+ const columns = window.innerWidth < 800 ? 2 : 5;
+
alertMessage.innerHTML =
- "" + supporters.map(n => `${n} `).join("") + " ";
+ `` + list.map(n => `${n} `).join("") + " ";
$("#alert").dialog({
resizable: false,
title: "Patreon Supporters",
- width: "54vw",
+ width: "min-width",
position: {my: "center", at: "center", of: "svg"}
});
}
@@ -157,9 +160,20 @@ optionsContent.addEventListener("click", function (event) {
});
function mapSizeInputChange() {
+ const $mapWidthInput = byId("mapWidthInput");
+ const $mapHeightInput = byId("mapHeightInput");
+
changeMapSize();
- localStorage.setItem("mapWidth", mapWidthInput.value);
- localStorage.setItem("mapHeight", mapHeightInput.value);
+ localStorage.setItem("mapWidth", $mapWidthInput.value);
+ localStorage.setItem("mapHeight", $mapHeightInput.value);
+
+ const tooWide = +$mapWidthInput.value > window.innerWidth;
+ const tooHigh = +$mapHeightInput.value > window.innerHeight;
+
+ if (tooWide || tooHigh) {
+ const message = `Canvas size is larger than actual window size (${window.innerWidth} x ${window.innerHeight}). It can affect the performance if you are going to create a new map`;
+ tip(message, false, "warn", 4000);
+ }
}
// change svg size on manual size change or window resize, do not change graph size
@@ -534,7 +548,7 @@ function applyStoredOptions() {
options.stateLabelsMode = stateLabelsModeInput.value;
}
-// randomize options if randomization is allowed (not locked or options='default')
+// randomize options if randomization is allowed (not locked or queryParam options='default')
function randomizeOptions() {
const randomize = new URL(window.location.href).searchParams.get("options") === "default"; // ignore stored options
@@ -546,7 +560,7 @@ function randomizeOptions() {
manorsInput.value = 1000;
manorsOutput.value = "auto";
}
- if (randomize || !locked("religions")) religionsInput.value = religionsOutput.value = gauss(5, 2, 2, 10);
+ if (randomize || !locked("religions")) religionsInput.value = religionsOutput.value = gauss(6, 3, 2, 10);
if (randomize || !locked("power")) powerInput.value = powerOutput.value = gauss(4, 2, 0, 10, 2);
if (randomize || !locked("neutral")) neutralInput.value = neutralOutput.value = rn(1 + Math.random(), 1);
if (randomize || !locked("cultures")) culturesInput.value = culturesOutput.value = gauss(12, 3, 5, 30);
@@ -602,17 +616,17 @@ function randomizeCultureSet() {
function setRendering(value) {
viewbox.attr("shape-rendering", value);
- if (value === "optimizeSpeed") {
- // block some styles
- coastline.select("#sea_island").style("filter", "none");
- statesHalo.style("display", "none");
- emblems.style("opacity", 1);
- } else {
- // remove style block
- coastline.select("#sea_island").style("filter", null);
- statesHalo.style("display", null);
- emblems.style("opacity", null);
- }
+ // if (value === "optimizeSpeed") {
+ // // block some styles
+ // coastline.select("#sea_island").style("filter", "none");
+ // statesHalo.style("display", "none");
+ // emblems.style("opacity", 1);
+ // } else {
+ // // remove style block
+ // coastline.select("#sea_island").style("filter", null);
+ // statesHalo.style("display", null);
+ // emblems.style("opacity", null);
+ // }
}
// generate current year and era name
diff --git a/modules/ui/rivers-creator.js b/modules/ui/rivers-creator.js
index 83a4d1b9..a8600917 100644
--- a/modules/ui/rivers-creator.js
+++ b/modules/ui/rivers-creator.js
@@ -74,12 +74,13 @@ function createRiver() {
function addRiver() {
const {rivers, cells} = pack;
- const {addMeandering, getApproximateLength, getWidth, getOffset, getName, getRiverPath, getBasin} = Rivers;
+ const {addMeandering, getApproximateLength, getWidth, getOffset, getName, getRiverPath, getBasin, getNextId} =
+ Rivers;
const riverCells = createRiver.cells;
if (riverCells.length < 2) return tip("Add at least 2 cells", false, "error");
- const riverId = rivers.length ? last(rivers).i + 1 : 1;
+ const riverId = getNextId(rivers);
const parent = cells.r[last(riverCells)] || riverId;
riverCells.forEach(cell => {
@@ -100,12 +101,30 @@ function createRiver() {
const name = getName(mouth);
const basin = getBasin(parent);
- rivers.push({i: riverId, source, mouth, discharge, length, width, widthFactor, sourceWidth, parent, cells: riverCells, basin, name, type: "River"});
+ rivers.push({
+ i: riverId,
+ source,
+ mouth,
+ discharge,
+ length,
+ width,
+ widthFactor,
+ sourceWidth,
+ parent,
+ cells: riverCells,
+ basin,
+ name,
+ type: "River"
+ });
const id = "river" + riverId;
// render river
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
- viewbox.select("#rivers").append("path").attr("id", id).attr("d", getRiverPath(meanderedPoints, widthFactor, sourceWidth));
+ viewbox
+ .select("#rivers")
+ .append("path")
+ .attr("id", id)
+ .attr("d", getRiverPath(meanderedPoints, widthFactor, sourceWidth));
editRiver(id);
}
diff --git a/modules/ui/routes-editor.js b/modules/ui/routes-editor.js
index 785c22a9..ca52b036 100644
--- a/modules/ui/routes-editor.js
+++ b/modules/ui/routes-editor.js
@@ -1,4 +1,7 @@
"use strict";
+
+const CONTROL_POINST_DISTANCE = 10;
+
function editRoute(onClick) {
if (customization) return;
if (!onClick && elSelected && d3.event.target.id === elSelected.attr("id")) return;
@@ -47,13 +50,13 @@ function editRoute(onClick) {
}
function drawControlPoints(node) {
- const l = node.getTotalLength();
- const increment = l / Math.ceil(l / 4);
- for (let i = 0; i <= l; i += increment) {
+ const totalLength = node.getTotalLength();
+ const increment = totalLength / Math.ceil(totalLength / CONTROL_POINST_DISTANCE);
+ for (let i = 0; i <= totalLength; i += increment) {
const point = node.getPointAtLength(i);
addControlPoint([point.x, point.y]);
}
- routeLength.innerHTML = rn(l * distanceScaleInput.value) + " " + distanceUnitInput.value;
+ routeLength.innerHTML = rn(totalLength * distanceScaleInput.value) + " " + distanceUnitInput.value;
}
function addControlPoint(point, before = null) {
diff --git a/modules/ui/stylePresets.js b/modules/ui/stylePresets.js
index 7353b067..20e1612c 100644
--- a/modules/ui/stylePresets.js
+++ b/modules/ui/stylePresets.js
@@ -1,14 +1,27 @@
// UI module to control the style presets
"use strict";
-const systemPresets = ["default", "ancient", "gloom", "light", "watercolor", "clean", "atlas", "cyberpunk", "monochrome"];
+const systemPresets = [
+ "default",
+ "ancient",
+ "gloom",
+ "pale",
+ "light",
+ "watercolor",
+ "clean",
+ "atlas",
+ "cyberpunk",
+ "monochrome"
+];
const customPresetPrefix = "fmgStyle_";
// add style presets to list
{
const systemOptions = systemPresets.map(styleName => `${styleName} `);
const storedStyles = Object.keys(localStorage).filter(key => key.startsWith(customPresetPrefix));
- const customOptions = storedStyles.map(styleName => `${styleName.replace(customPresetPrefix, "")} [custom] `);
+ const customOptions = storedStyles.map(
+ styleName => `${styleName.replace(customPresetPrefix, "")} [custom] `
+ );
const options = systemOptions.join("") + customOptions.join("");
document.getElementById("stylePreset").innerHTML = options;
}
@@ -37,7 +50,8 @@ async function getStylePreset(desiredPreset) {
const isValid = JSON.isValid(storedStyleJSON);
if (isValid) return [desiredPreset, JSON.parse(storedStyleJSON)];
- ERROR && console.error(`Custom style ${desiredPreset} stored in localStorage is not valid. Applying default style`);
+ ERROR &&
+ console.error(`Custom style ${desiredPreset} stored in localStorage is not valid. Applying default style`);
presetToLoad = "default";
}
}
@@ -145,8 +159,31 @@ function addStylePreset() {
"#stateBorders": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter"],
"#provinceBorders": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter"],
"#cells": ["opacity", "stroke", "stroke-width", "filter", "mask"],
- "#gridOverlay": ["opacity", "scale", "dx", "dy", "type", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "transform", "filter", "mask"],
- "#coordinates": ["opacity", "data-size", "font-size", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter", "mask"],
+ "#gridOverlay": [
+ "opacity",
+ "scale",
+ "dx",
+ "dy",
+ "type",
+ "stroke",
+ "stroke-width",
+ "stroke-dasharray",
+ "stroke-linecap",
+ "transform",
+ "filter",
+ "mask"
+ ],
+ "#coordinates": [
+ "opacity",
+ "data-size",
+ "font-size",
+ "stroke",
+ "stroke-width",
+ "stroke-dasharray",
+ "stroke-linecap",
+ "filter",
+ "mask"
+ ],
"#compass": ["opacity", "transform", "filter", "mask", "shape-rendering"],
"#rose": ["transform"],
"#relig": ["opacity", "stroke", "stroke-width", "filter"],
@@ -174,7 +211,17 @@ function addStylePreset() {
"#statesBody": ["opacity", "filter"],
"#statesHalo": ["opacity", "data-width", "stroke-width", "filter"],
"#provs": ["opacity", "fill", "font-size", "font-family", "filter"],
- "#temperature": ["opacity", "font-size", "fill", "fill-opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter"],
+ "#temperature": [
+ "opacity",
+ "font-size",
+ "fill",
+ "fill-opacity",
+ "stroke",
+ "stroke-width",
+ "stroke-dasharray",
+ "stroke-linecap",
+ "filter"
+ ],
"#ice": ["opacity", "fill", "stroke", "stroke-width", "filter"],
"#emblems": ["opacity", "stroke-width", "filter"],
"#texture": ["opacity", "filter", "mask"],
@@ -184,16 +231,65 @@ function addStylePreset() {
"#oceanBase": ["fill"],
"#oceanicPattern": ["href", "opacity"],
"#terrs": ["opacity", "scheme", "terracing", "skip", "relax", "curve", "filter", "mask"],
- "#legend": ["data-size", "font-size", "font-family", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "data-x", "data-y", "data-columns"],
+ "#legend": [
+ "data-size",
+ "font-size",
+ "font-family",
+ "stroke",
+ "stroke-width",
+ "stroke-dasharray",
+ "stroke-linecap",
+ "data-x",
+ "data-y",
+ "data-columns"
+ ],
"#legendBox": ["fill", "fill-opacity"],
"#burgLabels > #cities": ["opacity", "fill", "text-shadow", "data-size", "font-size", "font-family"],
- "#burgIcons > #cities": ["opacity", "fill", "fill-opacity", "size", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap"],
+ "#burgIcons > #cities": [
+ "opacity",
+ "fill",
+ "fill-opacity",
+ "size",
+ "stroke",
+ "stroke-width",
+ "stroke-dasharray",
+ "stroke-linecap"
+ ],
"#anchors > #cities": ["opacity", "fill", "size", "stroke", "stroke-width"],
"#burgLabels > #towns": ["opacity", "fill", "text-shadow", "data-size", "font-size", "font-family"],
- "#burgIcons > #towns": ["opacity", "fill", "fill-opacity", "size", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap"],
+ "#burgIcons > #towns": [
+ "opacity",
+ "fill",
+ "fill-opacity",
+ "size",
+ "stroke",
+ "stroke-width",
+ "stroke-dasharray",
+ "stroke-linecap"
+ ],
"#anchors > #towns": ["opacity", "fill", "size", "stroke", "stroke-width"],
- "#labels > #states": ["opacity", "fill", "stroke", "stroke-width", "text-shadow", "data-size", "font-size", "font-family", "filter"],
- "#labels > #addedLabels": ["opacity", "fill", "stroke", "stroke-width", "text-shadow", "data-size", "font-size", "font-family", "filter"],
+ "#labels > #states": [
+ "opacity",
+ "fill",
+ "stroke",
+ "stroke-width",
+ "text-shadow",
+ "data-size",
+ "font-size",
+ "font-family",
+ "filter"
+ ],
+ "#labels > #addedLabels": [
+ "opacity",
+ "fill",
+ "stroke",
+ "stroke-width",
+ "text-shadow",
+ "data-size",
+ "font-size",
+ "font-family",
+ "filter"
+ ],
"#fogging": ["opacity", "fill", "filter"]
};
@@ -238,7 +334,8 @@ function addStylePreset() {
if (!styleJSON) return tip("Please provide a style JSON", false, "error");
if (!JSON.isValid(styleJSON)) return tip("JSON string is not valid, please check the format", false, "error");
if (!desiredName) return tip("Please provide a preset name", false, "error");
- if (styleSaverTip.innerHTML === "default") return tip("You cannot overwrite default preset, please change the name", false, "error");
+ if (styleSaverTip.innerHTML === "default")
+ return tip("You cannot overwrite default preset, please change the name", false, "error");
const presetName = customPresetPrefix + desiredName;
applyOption(stylePreset, presetName, desiredName + " [custom]");
diff --git a/modules/ui/tools.js b/modules/ui/tools.js
index 12d76bb0..7fbc1ebf 100644
--- a/modules/ui/tools.js
+++ b/modules/ui/tools.js
@@ -74,10 +74,8 @@ toolsContent.addEventListener("click", function (event) {
});
function processFeatureRegeneration(event, button) {
- if (button === "regenerateStateLabels") {
- BurgsAndStates.drawStateLabels();
- if (!layerIsOn("toggleLabels")) toggleLabels();
- } else if (button === "regenerateReliefIcons") {
+ if (button === "regenerateStateLabels") BurgsAndStates.drawStateLabels();
+ else if (button === "regenerateReliefIcons") {
ReliefIcons();
if (!layerIsOn("toggleRelief")) toggleRelief();
} else if (button === "regenerateRoutes") {
@@ -628,10 +626,11 @@ function addRiverOnClick() {
getType,
getWidth,
getOffset,
- getApproximateLength
+ getApproximateLength,
+ getNextId
} = Rivers;
const riverCells = [];
- let riverId = rivers.length ? last(rivers).i + 1 : 1;
+ let riverId = getNextId(rivers);
let parent = riverId;
const initialFlux = grid.cells.prec[cells.g[i]];
diff --git a/styles/default.json b/styles/default.json
index eb4acb69..4c684dc8 100644
--- a/styles/default.json
+++ b/styles/default.json
@@ -226,7 +226,7 @@
"opacity": 0.4,
"data-width": 10,
"stroke-width": 10,
- "filter": "blur(5px)"
+ "filter": "blur(3.5px)"
},
"#provs": {
"opacity": 0.7,
diff --git a/styles/light.json b/styles/light.json
index 5663d6c8..fb9b838c 100644
--- a/styles/light.json
+++ b/styles/light.json
@@ -192,18 +192,18 @@
"filter": null
},
"#roads": {
- "opacity": 0.9,
- "stroke": "#3c1d0b",
- "stroke-width": 1.37,
+ "opacity": 0.8,
+ "stroke": "#95481a",
+ "stroke-width": 0.8,
"stroke-dasharray": 2,
"stroke-linecap": "inherit",
"filter": null,
"mask": null
},
"#trails": {
- "opacity": 0.9,
+ "opacity": 0.8,
"stroke": "#95481a",
- "stroke-width": 0.88,
+ "stroke-width": 0.5,
"stroke-dasharray": ".8 1.6",
"stroke-linecap": "butt",
"filter": null,
diff --git a/styles/pale.json b/styles/pale.json
new file mode 100644
index 00000000..e6acdafd
--- /dev/null
+++ b/styles/pale.json
@@ -0,0 +1,389 @@
+{
+ "#map": {
+ "background-color": "#000000",
+ "filter": null,
+ "data-filter": null
+ },
+ "#armies": {
+ "font-size": 9,
+ "box-size": 4.5,
+ "stroke": "#000",
+ "stroke-width": 0,
+ "fill-opacity": 1,
+ "filter": "url(#dropShadow05)"
+ },
+ "#biomes": {
+ "opacity": 0.6,
+ "filter": null,
+ "mask": "url(#land)"
+ },
+ "#stateBorders": {
+ "opacity": 0.6,
+ "stroke": "#4c483e",
+ "stroke-width": 0.8,
+ "stroke-dasharray": "1 2.5",
+ "stroke-linecap": "square",
+ "filter": null
+ },
+ "#provinceBorders": {
+ "opacity": 0.6,
+ "stroke": "#56566d",
+ "stroke-width": 0.2,
+ "stroke-dasharray": 0.5,
+ "stroke-linecap": "butt",
+ "filter": null
+ },
+ "#cells": {
+ "opacity": null,
+ "stroke": "#808080",
+ "stroke-width": 0.1,
+ "filter": null,
+ "mask": null
+ },
+ "#gridOverlay": {
+ "opacity": 0.5,
+ "scale": 1,
+ "dx": 0,
+ "dy": 0,
+ "type": "pointyHex",
+ "stroke": "#808080",
+ "stroke-width": 1,
+ "stroke-dasharray": null,
+ "stroke-linecap": null,
+ "transform": null,
+ "filter": null,
+ "mask": null
+ },
+ "#coordinates": {
+ "opacity": 0.7,
+ "data-size": 15,
+ "font-size": 15,
+ "stroke": "#734d37",
+ "stroke-width": 1.5,
+ "stroke-dasharray": 5,
+ "stroke-linecap": "square",
+ "filter": null,
+ "mask": ""
+ },
+ "#compass": {
+ "opacity": 0.6,
+ "transform": null,
+ "filter": null,
+ "mask": "url(#water)",
+ "shape-rendering": "optimizespeed"
+ },
+ "#rose": {
+ "transform": null
+ },
+ "#relig": {
+ "opacity": 0.5,
+ "stroke": null,
+ "stroke-width": 0,
+ "filter": null
+ },
+ "#cults": {
+ "opacity": 0.5,
+ "stroke": "#777777",
+ "stroke-width": 0,
+ "stroke-dasharray": null,
+ "stroke-linecap": null,
+ "filter": null
+ },
+ "#landmass": {
+ "opacity": 1,
+ "fill": "#f4f2f0",
+ "filter": null
+ },
+ "#markers": {
+ "opacity": null,
+ "rescale": 1,
+ "filter": null
+ },
+ "#prec": {
+ "opacity": null,
+ "stroke": "#000000",
+ "stroke-width": 0.1,
+ "fill": "#2554ef",
+ "filter": null
+ },
+ "#population": {
+ "opacity": null,
+ "stroke-width": 1.6,
+ "stroke-dasharray": null,
+ "stroke-linecap": "butt",
+ "filter": null
+ },
+ "#rural": {
+ "stroke": "#0000ff"
+ },
+ "#urban": {
+ "stroke": "#ff0000"
+ },
+ "#freshwater": {
+ "opacity": 0.8,
+ "fill": "#98b6cd",
+ "stroke": "#718798",
+ "stroke-width": 0.5,
+ "filter": "url(#dropShadow05)"
+ },
+ "#salt": {
+ "opacity": 0.5,
+ "fill": "#409b8a",
+ "stroke": "#388985",
+ "stroke-width": 0.7,
+ "filter": null
+ },
+ "#sinkhole": {
+ "opacity": 1,
+ "fill": "#5bc9fd",
+ "stroke": "#53a3b0",
+ "stroke-width": 0.7,
+ "filter": null
+ },
+ "#frozen": {
+ "opacity": 0.95,
+ "fill": "#cdd4e7",
+ "stroke": "#cfe0eb",
+ "stroke-width": 0,
+ "filter": null
+ },
+ "#lava": {
+ "opacity": 0.7,
+ "fill": "#90270d",
+ "stroke": "#f93e0c",
+ "stroke-width": 2,
+ "filter": "url(#crumpled)"
+ },
+ "#dry": {
+ "opacity": 1,
+ "fill": "#c9bfa7",
+ "stroke": "#8e816f",
+ "stroke-width": 0.7,
+ "filter": null
+ },
+ "#sea_island": {
+ "opacity": 1,
+ "stroke": "#242424",
+ "stroke-width": 0.1,
+ "filter": "url(#dropShadow)",
+ "auto-filter": 1
+ },
+ "#lake_island": {
+ "opacity": 1,
+ "stroke": "#7c8eaf",
+ "stroke-width": 0.1,
+ "filter": null
+ },
+ "#terrain": {
+ "opacity": 0.8,
+ "set": "simple",
+ "size": 0.7,
+ "density": 0.3,
+ "filter": null,
+ "mask": ""
+ },
+ "#rivers": {
+ "opacity": 1,
+ "filter": null,
+ "fill": "#6dabba"
+ },
+ "#ruler": {
+ "opacity": null,
+ "filter": null
+ },
+ "#roads": {
+ "opacity": 0.9,
+ "stroke": "#d06324",
+ "stroke-width": 0.6,
+ "stroke-dasharray": "1 2",
+ "stroke-linecap": "round",
+ "filter": null,
+ "mask": null
+ },
+ "#trails": {
+ "opacity": 0.9,
+ "stroke": "#d06324",
+ "stroke-width": 0.5,
+ "stroke-dasharray": ".5 2",
+ "stroke-linecap": "round",
+ "filter": null,
+ "mask": null
+ },
+ "#searoutes": {
+ "opacity": 1,
+ "stroke": "#e5edff",
+ "stroke-width": 0.5,
+ "stroke-dasharray": "2 3",
+ "stroke-linecap": "round",
+ "filter": null,
+ "mask": null
+ },
+ "#statesBody": {
+ "opacity": 0.15,
+ "filter": null
+ },
+ "#statesHalo": {
+ "opacity": 0.3,
+ "data-width": 10,
+ "stroke-width": 10,
+ "filter": "blur(3.5px)"
+ },
+ "#provs": {
+ "opacity": 0.4,
+ "fill": "#000000",
+ "font-size": 8,
+ "font-family": "Arima Madurai",
+ "filter": null
+ },
+ "#temperature": {
+ "opacity": null,
+ "font-size": "8px",
+ "fill": "#000000",
+ "fill-opacity": 0.3,
+ "stroke": null,
+ "stroke-width": 1.8,
+ "stroke-dasharray": null,
+ "stroke-linecap": null,
+ "filter": null
+ },
+ "#ice": {
+ "opacity": 0.9,
+ "fill": "#e8f0f6",
+ "stroke": "#e8f0f6",
+ "stroke-width": 0.1,
+ "filter": "url(#dropShadow05)"
+ },
+ "#emblems": {
+ "opacity": 0.9,
+ "stroke-width": 1,
+ "filter": null
+ },
+ "#texture": {
+ "opacity": 0.39,
+ "filter": null,
+ "mask": "url(#land)"
+ },
+ "#textureImage": {
+ "x": 0,
+ "y": 0
+ },
+ "#zones": {
+ "opacity": 0.6,
+ "stroke": "#333333",
+ "stroke-width": 0,
+ "stroke-dasharray": null,
+ "stroke-linecap": "butt",
+ "filter": null,
+ "mask": null
+ },
+ "#oceanLayers": {
+ "filter": "url(#dropShadow05)",
+ "layers": "-6,-3,-1"
+ },
+ "#oceanBase": {
+ "fill": "#7ca4b6"
+ },
+ "#oceanicPattern": {
+ "href": "./images/kiwiroo.png",
+ "opacity": 0.3
+ },
+ "#terrs": {
+ "opacity": 0.7,
+ "scheme": "bright",
+ "terracing": 0,
+ "skip": 2,
+ "relax": 1,
+ "curve": 0,
+ "filter": "",
+ "mask": "url(#land)"
+ },
+ "#legend": {
+ "data-size": 13,
+ "font-size": 13,
+ "font-family": "Arima Madurai",
+ "stroke": "#812929",
+ "stroke-width": 2.5,
+ "stroke-dasharray": "0 4 10 4",
+ "stroke-linecap": "round",
+ "data-x": 54.73,
+ "data-y": 62.98,
+ "data-columns": 8
+ },
+ "#burgLabels > #cities": {
+ "opacity": 0.8,
+ "fill": "#3a3a3a",
+ "text-shadow": "white 0px 0px 4px",
+ "data-size": 7,
+ "font-size": 7,
+ "font-family": "Arima Madurai"
+ },
+ "#burgIcons > #cities": {
+ "opacity": 1,
+ "fill": "#ffffff",
+ "fill-opacity": 0.7,
+ "size": 1.5,
+ "stroke": "#4f4f4f",
+ "stroke-width": 0.2,
+ "stroke-dasharray": "",
+ "stroke-linecap": "butt"
+ },
+ "#anchors > #cities": {
+ "opacity": 1,
+ "fill": "#ffffff",
+ "size": 3,
+ "stroke": "#3e3e4b",
+ "stroke-width": 1.2
+ },
+ "#burgLabels > #towns": {
+ "opacity": 0.8,
+ "fill": "#3e3e4b",
+ "text-shadow": "white 0px 0px 4px",
+ "data-size": 4,
+ "font-size": 4,
+ "font-family": "Arima Madurai"
+ },
+ "#burgIcons > #towns": {
+ "opacity": 1,
+ "fill": "#ffffff",
+ "fill-opacity": 0.7,
+ "size": 0.6,
+ "stroke": "#4f4f4f",
+ "stroke-width": 0.12,
+ "stroke-dasharray": "",
+ "stroke-linecap": "butt"
+ },
+ "#anchors > #towns": {
+ "opacity": 1,
+ "fill": "#ffffff",
+ "size": 1.2,
+ "stroke": "#3e3e4b",
+ "stroke-width": 1
+ },
+ "#labels > #states": {
+ "opacity": 0.8,
+ "fill": "#3e3e3e",
+ "stroke": "#000000",
+ "stroke-width": 0,
+ "text-shadow": "white 0px 0px 6px",
+ "data-size": 14,
+ "font-size": 14,
+ "font-family": "Arima Madurai",
+ "filter": null
+ },
+ "#labels > #addedLabels": {
+ "opacity": 1,
+ "fill": "#f24706",
+ "stroke": "#701b05",
+ "stroke-width": 0.1,
+ "text-shadow": "white 0px 0px 4px",
+ "data-size": 6,
+ "font-size": 6,
+ "font-family": "Arima Madurai",
+ "filter": null
+ },
+ "#fogging": {
+ "opacity": 1,
+ "fill": "#30426f",
+ "filter": null
+ }
+}
diff --git a/sw.js b/sw.js
index ce803d5b..4e214e8e 100644
--- a/sw.js
+++ b/sw.js
@@ -8,7 +8,10 @@ const {ExpirationPlugin} = workbox.expiration;
const DAY = 24 * 60 * 60;
const getPolitics = ({entries, days}) => {
- return [new CacheableResponsePlugin({statuses: [0, 200]}), new ExpirationPlugin({maxEntries: entries, maxAgeSeconds: days * DAY})];
+ return [
+ new CacheableResponsePlugin({statuses: [0, 200]}),
+ new ExpirationPlugin({maxEntries: entries, maxAgeSeconds: days * DAY})
+ ];
};
registerRoute(
@@ -21,7 +24,8 @@ registerRoute(
);
registerRoute(
- ({request, url}) => request.destination === "script" && !url.pathname.endsWith("min.js") && !url.pathname.includes("versioning.js"),
+ ({request, url}) =>
+ request.destination === "script" && !url.pathname.endsWith("min.js") && !url.pathname.includes("versioning.js"),
new CacheFirst({
cacheName: "fmg-scripts",
plugins: getPolitics({entries: 100, days: 30})
diff --git a/utils/numberUtils.js b/utils/numberUtils.js
index e3f143a5..ada7c284 100644
--- a/utils/numberUtils.js
+++ b/utils/numberUtils.js
@@ -20,3 +20,7 @@ function lim(v) {
function normalize(val, min, max) {
return minmax((val - min) / (max - min), 0, 1);
}
+
+function lerp(a, b, t) {
+ return a + (b - a) * t;
+}
diff --git a/versioning.js b/versioning.js
index 89816315..64ad10bd 100644
--- a/versioning.js
+++ b/versioning.js
@@ -1,7 +1,7 @@
"use strict";
// version and caching control
-const version = "1.89.00"; // generator version, update each time
+const version = "1.89.15"; // generator version, update each time
{
document.title += " v" + version;
@@ -28,6 +28,7 @@ const version = "1.89.00"; // generator version, update each time
Latest changes:
+ Religions can be edited and redrawn like cultures
Lock states, provinces, cultures, and religions from regeneration
Heightmap brushes: linear edit option
Data Charts screen