Merge pull request #1 from mosuzi/master

pull original repo
This commit is contained in:
mosuzi 2023-03-28 11:56:02 +08:00 committed by GitHub
commit 8fd9b82554
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 1761 additions and 613 deletions

View file

@ -2,9 +2,9 @@
# Fantasy Map Generator # 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). 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).

View file

@ -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

View file

@ -1,8 +1,8 @@
To get heightmap with correct height scale: To get heightmap with correct height scale:
1. Open tangrams.github.io 1. Open https://tangrams.github.io/heightmapper
2. Toggle off auto-exposure 2. Toggle off auto-exposure
3. Set max elevation to 2000 3. Set max elevation to 2000
4. Set min elevation to -500 4. Set min elevation to -500
5. Find region you like 5. Find region you like
6. Render image 6. Render image
7. Optionally rescale image to a smaller size (e.g. 500x300px) as high resolution is not used 7. Optionally rescale image to a smaller size (e.g. 500x300px) as high resolution is not used

View file

@ -1082,12 +1082,16 @@ tr.battleSurvivors {
font-size: 0.9em; font-size: 0.9em;
} }
#battleBody div.battlePhases,
#battleBottom div.battleTypes { #battleBottom div.battleTypes {
position: fixed; position: fixed;
background-color: #ffffff30; background-color: #ffffff30;
} }
#battleBody div.battlePhases {
position: absolute;
background-color: #ffffff30;
}
#battleBody div.battlePhases > button, #battleBody div.battlePhases > button,
#battleBottom div.battleTypes > button { #battleBottom div.battleTypes > button {
width: 3.2em; width: 3.2em;
@ -2045,6 +2049,7 @@ div.textual span,
} }
#notesLegend { #notesLegend {
width: auto;
height: 87%; height: 87%;
outline: 0; outline: 0;
overflow-y: auto; overflow-y: auto;
@ -2239,7 +2244,6 @@ svg.button {
user-select: none; user-select: none;
} }
.dontAsk { .dontAsk {
margin: 0.9em 0 0 0.6em; margin: 0.9em 0 0 0.6em;
display: inline-flex; display: inline-flex;
@ -2338,7 +2342,7 @@ svg.button {
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
body { body {
background: #25252a; background: #25252a;
} }
} }

View file

@ -3,14 +3,33 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Azgaar's Fantasy Map Generator</title> <title>Azgaar's Fantasy Map Generator</title>
<meta name="application-name" content="Azgaar's Fantasy Map Generator" /> <meta name="application-name" content="Azgaar's Fantasy Map Generator" />
<meta name="author" content="Azgaar (Max Ganiev)" /> <meta name="author" content="Azgaar" />
<meta name="description" content="Azgaar's Fantasy Map Generator and Editor" /> <meta
name="description"
content="Free web app that helps fantasy writers, game masters, and cartographers create and edit fantasy maps"
/>
<meta property="og:url" content="https://azgaar.github.io/Fantasy-Map-Generator" /> <meta property="og:url" content="https://azgaar.github.io/Fantasy-Map-Generator" />
<meta property="og:title" content="Azgaar's Fantasy Map Generator" /> <meta property="og:title" content="Azgaar's Fantasy Map Generator" />
<meta property="og:description" content="Web application generating interactive and customizable maps" /> <meta
property="og:description"
content="Free web app that helps fantasy writers, game masters, and cartographers create and edit fantasy maps"
/>
<meta property="og:image" content="images/preview.png" /> <meta property="og:image" content="images/preview.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta property="twitter:domain" content="azgaar.github.io" />
<meta property="twitter:url" content="https://azgaar.github.io/Fantasy-Map-Generator/" />
<meta name="twitter:title" content="Azgaar's Fantasy Map Generator" />
<meta
name="twitter:description"
content="Free web app that helps fantasy writers, game masters, and cartographers create and edit fantasy maps"
/>
<meta name="twitter:image" content="images/preview.png" />
<link rel="icon" type="image/png" href="images/icons/favicon-32x32.png" sizes="32x32" /> <link rel="icon" type="image/png" href="images/icons/favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="images/icons/favicon-16x16.png" sizes="16x16" /> <link rel="icon" type="image/png" href="images/icons/favicon-16x16.png" sizes="16x16" />
<link rel="apple-touch-icon" href="images/icons/maskable_icon_x192.png" /> <link rel="apple-touch-icon" href="images/icons/maskable_icon_x192.png" />
@ -108,7 +127,7 @@
} }
</style> </style>
<link rel="preload" href="index.css?v=1.89.00" as="style" onload="this.onload=null; this.rel='stylesheet'" /> <link rel="preload" href="index.css?v=1.89.13" as="style" onload="this.onload=null; this.rel='stylesheet'" />
<link rel="preload" href="icons.css" as="style" onload="this.onload=null; this.rel='stylesheet'" /> <link rel="preload" href="icons.css" as="style" onload="this.onload=null; this.rel='stylesheet'" />
<link rel="preload" href="libs/jquery-ui.css" as="style" onload="this.onload=null; this.rel='stylesheet'" /> <link rel="preload" href="libs/jquery-ui.css" as="style" onload="this.onload=null; this.rel='stylesheet'" />
</head> </head>
@ -1237,10 +1256,6 @@
<td><select id="styleStatesBodyFilter" /></td> <td><select id="styleStatesBodyFilter" /></td>
</tr> </tr>
<tr style="margin-top: 1em">
<td><em>Halo is disabled if "Rendering" option is set to "Best performance"</em></td>
</tr>
<tr data-tip="Set states halo effect width"> <tr data-tip="Set states halo effect width">
<td>Halo width</td> <td>Halo width</td>
<td> <td>
@ -1446,7 +1461,7 @@
</p> </p>
<table> <table>
<tr <tr
data-tip="Canvas width and height in pixels. Defines map size on generation, then map size cannot be changed and canvas size changes only visible area. Keep canvas size equal to screen size or less to improve performance. The best aspect ratio for maps is 2:1" data-tip="Canvas width and height in pixels. Defines map size on generation that cannot be changed later. Always keep canvas size equal to your screen size or less. The best option is to use the default value. For full-globe maps use aspect ratio 2:1"
> >
<td></td> <td></td>
<td>Canvas size</td> <td>Canvas size</td>
@ -1458,7 +1473,7 @@
</td> </td>
<td> <td>
<i <i
data-tip="Toggle between screen size and initial canvas size" data-tip="Toggle between the current screen size and the initial canvas size"
id="toggleFullscreen" id="toggleFullscreen"
class="icon-resize-full-alt" class="icon-resize-full-alt"
></i> ></i>
@ -1883,8 +1898,8 @@
<td>Rendering</td> <td>Rendering</td>
<td> <td>
<select id="shapeRendering" data-stored="shapeRendering"> <select id="shapeRendering" data-stored="shapeRendering">
<option value="geometricPrecision" selected>Best quality</option> <option value="geometricPrecision">Best quality</option>
<option value="optimizeSpeed">Best performace</option> <option value="optimizeSpeed" selected>Best performace</option>
</select> </select>
</td> </td>
<td></td> <td></td>
@ -2749,7 +2764,7 @@
<div id="iceEditor" class="dialog" style="display: none"> <div id="iceEditor" class="dialog" style="display: none">
<button id="iceEditStyle" data-tip="Edit style in Style Editor" class="icon-brush"></button> <button id="iceEditStyle" data-tip="Edit style in Style Editor" class="icon-brush"></button>
<button id="iceRandomize" data-tip="Randomize Iceberg shape" class="icon-shuffle"></button> <button id="iceRandomize" data-tip="Randomize Iceberg shape" class="icon-shuffle"></button>
<input id="iceSize" data-tip="Change Iceberg size" type="range" min=".05" max="1" step=".01" /> <input id="iceSize" data-tip="Change Iceberg size" type="range" min=".05" max="2" step=".01" />
<button id="iceNew" data-tip="Add an Iceberg (click on map)" class="icon-plus"></button> <button id="iceNew" data-tip="Add an Iceberg (click on map)" class="icon-plus"></button>
<button <button
id="iceRemove" id="iceRemove"
@ -5602,7 +5617,7 @@
</div> </div>
<div id="iconSelector" style="display: none" class="dialog"> <div id="iconSelector" style="display: none" class="dialog">
<table id="iconTable" class="table pointer" style="font-size: 2em; text-align: center"></table> <table id="iconTable" class="table pointer" style="font-size: 2em; text-align: center; width: 100%"></table>
<div style="font-style: italic; font-size: 1.2em; margin: 0.4em 0 0 0.4em"> <div style="font-style: italic; font-size: 1.2em; margin: 0.4em 0 0 0.4em">
<span>Select from the list or paste a Unicode character here: </span> <span>Select from the list or paste a Unicode character here: </span>
<input id="iconInput" style="width: 2em" /> <input id="iconInput" style="width: 2em" />
@ -7830,7 +7845,7 @@
<script src="utils/colorUtils.js"></script> <script src="utils/colorUtils.js"></script>
<script src="utils/graphUtils.js?v=1.88.02"></script> <script src="utils/graphUtils.js?v=1.88.02"></script>
<script src="utils/nodeUtils.js"></script> <script src="utils/nodeUtils.js"></script>
<script src="utils/numberUtils.js"></script> <script src="utils/numberUtils.js?v=1.89.08"></script>
<script src="utils/polyfills.js"></script> <script src="utils/polyfills.js"></script>
<script src="utils/probabilityUtils.js?v=1.88.00"></script> <script src="utils/probabilityUtils.js?v=1.88.00"></script>
<script src="utils/stringUtils.js"></script> <script src="utils/stringUtils.js"></script>
@ -7841,14 +7856,14 @@
<script src="config/heightmap-templates.js"></script> <script src="config/heightmap-templates.js"></script>
<script src="config/precreated-heightmaps.js"></script> <script src="config/precreated-heightmaps.js"></script>
<script src="modules/heightmap-generator.js?v=1.88.00"></script> <script src="modules/heightmap-generator.js?v=1.88.00"></script>
<script src="modules/ocean-layers.js?v=1.87.15"></script> <script src="modules/ocean-layers.js?v=1.89.08"></script>
<script src="modules/river-generator.js"></script> <script src="modules/river-generator.js?v=1.89.13"></script>
<script src="modules/lakes.js"></script> <script src="modules/lakes.js"></script>
<script src="modules/names-generator.js?v=1.87.14"></script> <script src="modules/names-generator.js?v=1.87.14"></script>
<script src="modules/cultures-generator.js?v=1.89.00"></script> <script src="modules/cultures-generator.js?v=1.89.10"></script>
<script src="modules/burgs-and-states.js?v=1.89.00"></script> <script src="modules/burgs-and-states.js?v=1.89.07"></script>
<script src="modules/routes-generator.js"></script> <script src="modules/routes-generator.js"></script>
<script src="modules/religions-generator.js?v=1.89.00"></script> <script src="modules/religions-generator.js?v=1.89.15"></script>
<script src="modules/military-generator.js"></script> <script src="modules/military-generator.js"></script>
<script src="modules/markers-generator.js?v=1.87.13"></script> <script src="modules/markers-generator.js?v=1.87.13"></script>
<script src="modules/coa-generator.js"></script> <script src="modules/coa-generator.js"></script>
@ -7859,34 +7874,34 @@
<script src="modules/fonts.js"></script> <script src="modules/fonts.js"></script>
<script src="modules/ui/layers.js"></script> <script src="modules/ui/layers.js"></script>
<script src="modules/ui/measurers.js?v=1.87.02"></script> <script src="modules/ui/measurers.js?v=1.87.02"></script>
<script src="modules/ui/stylePresets.js"></script> <script src="modules/ui/stylePresets.js?v=1.89.11"></script>
<script src="modules/ui/general.js?v=1.87.03"></script> <script src="modules/ui/general.js?v=1.87.03"></script>
<script src="modules/ui/options.js?v=1.88.02"></script> <script src="modules/ui/options.js?v=1.88.14"></script>
<script src="main.js?v=1.88.02"></script> <script src="main.js?v=1.89.12"></script>
<script defer src="modules/relief-icons.js"></script> <script defer src="modules/relief-icons.js"></script>
<script defer src="modules/ui/style.js"></script> <script defer src="modules/ui/style.js"></script>
<script defer src="modules/ui/editors.js?v=1.87.07"></script> <script defer src="modules/ui/editors.js?v=1.89.12"></script>
<script defer src="modules/ui/tools.js?v=1.89.00"></script> <script defer src="modules/ui/tools.js?v=1.89.13"></script>
<script defer src="modules/ui/world-configurator.js"></script> <script defer src="modules/ui/world-configurator.js"></script>
<script defer src="modules/ui/heightmap-editor.js?v=1.88.03"></script> <script defer src="modules/ui/heightmap-editor.js?v=1.89.06"></script>
<script defer src="modules/ui/provinces-editor.js?v=1.89.00"></script> <script defer src="modules/ui/provinces-editor.js?v=1.89.00"></script>
<script defer src="modules/ui/biomes-editor.js"></script> <script defer src="modules/ui/biomes-editor.js"></script>
<script defer src="modules/ui/namesbase-editor.js?v=1.87.10"></script> <script defer src="modules/ui/namesbase-editor.js?v=1.87.10"></script>
<script defer src="modules/ui/elevation-profile.js"></script> <script defer src="modules/ui/elevation-profile.js"></script>
<script defer src="modules/ui/temperature-graph.js"></script> <script defer src="modules/ui/temperature-graph.js"></script>
<script defer src="modules/ui/routes-editor.js"></script> <script defer src="modules/ui/routes-editor.js?v=1.89.04"></script>
<script defer src="modules/ui/ice-editor.js"></script> <script defer src="modules/ui/ice-editor.js?v=1.89.08"></script>
<script defer src="modules/ui/lakes-editor.js?v=1.87.10"></script> <script defer src="modules/ui/lakes-editor.js?v=1.87.10"></script>
<script defer src="modules/ui/coastline-editor.js"></script> <script defer src="modules/ui/coastline-editor.js"></script>
<script defer src="modules/ui/labels-editor.js"></script> <script defer src="modules/ui/labels-editor.js"></script>
<script defer src="modules/ui/rivers-editor.js"></script> <script defer src="modules/ui/rivers-editor.js"></script>
<script defer src="modules/ui/rivers-creator.js"></script> <script defer src="modules/ui/rivers-creator.js?v=1.89.13"></script>
<script defer src="modules/ui/relief-editor.js"></script> <script defer src="modules/ui/relief-editor.js"></script>
<script defer src="modules/ui/burg-editor.js"></script> <script defer src="modules/ui/burg-editor.js"></script>
<script defer src="modules/ui/units-editor.js"></script> <script defer src="modules/ui/units-editor.js"></script>
<script defer src="modules/ui/notes-editor.js"></script> <script defer src="modules/ui/notes-editor.js?v=1.89.03"></script>
<script defer src="modules/ui/diplomacy-editor.js?v=1.88.04"></script> <script defer src="modules/ui/diplomacy-editor.js?v=1.88.04"></script>
<script defer src="modules/ui/zones-editor.js"></script> <script defer src="modules/ui/zones-editor.js"></script>
<script defer src="modules/ui/burgs-overview.js"></script> <script defer src="modules/ui/burgs-overview.js"></script>

View file

@ -191,7 +191,6 @@ let populationRate = +document.getElementById("populationRateInput").value;
let distanceScale = +document.getElementById("distanceScaleInput").value; let distanceScale = +document.getElementById("distanceScaleInput").value;
let urbanization = +document.getElementById("urbanizationInput").value; let urbanization = +document.getElementById("urbanizationInput").value;
let urbanDensity = +document.getElementById("urbanDensityInput").value; let urbanDensity = +document.getElementById("urbanDensityInput").value;
let statesNeutral = 1; // statesEditor growth parameter
applyStoredOptions(); applyStoredOptions();
@ -695,6 +694,7 @@ async function generate(options) {
if (shouldRegenerateGrid(grid, precreatedSeed)) grid = precreatedGraph || generateGrid(); if (shouldRegenerateGrid(grid, precreatedSeed)) grid = precreatedGraph || generateGrid();
else delete grid.cells.h; else delete grid.cells.h;
grid.cells.h = await HeightmapGenerator.generate(grid); grid.cells.h = await HeightmapGenerator.generate(grid);
pack = {}; // reset pack
markFeatures(); markFeatures();
markupGridOcean(); markupGridOcean();

View file

@ -359,7 +359,7 @@ window.BurgsAndStates = (function () {
TIME && console.timeEnd("drawBurgs"); 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 () { const expandStates = function () {
TIME && console.time("expandStates"); TIME && console.time("expandStates");
const {cells, states, cultures, burgs} = pack; const {cells, states, cultures, burgs} = pack;
@ -367,18 +367,28 @@ window.BurgsAndStates = (function () {
cells.state = cells.state || new Uint16Array(cells.i.length); cells.state = cells.state || new Uint16Array(cells.i.length);
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const cost = []; const cost = [];
const neutral = (cells.i.length / 5000) * 2500 * neutralInput.value * statesNeutral; // limit cost for state growth
states const globalNeutralRate = byId("neutralInput")?.valueAsNumber || 1;
.filter(s => s.i && !s.removed) const statesNeutralRate = byId("statesNeutral")?.valueAsNumber || 1;
.forEach(s => { const neutral = (cells.i.length / 2) * globalNeutralRate * statesNeutralRate; // limit cost for state growth
const capitalCell = burgs[s.capital].cell;
cells.state[capitalCell] = s.i; // remove state from all cells except of locked
const cultureCenter = cultures[s.culture].center; for (const cellId of cells.i) {
const b = cells.biome[cultureCenter]; // state native biome const state = states[cells.state[cellId]];
queue.queue({e: s.center, p: 0, s: s.i, b}); if (state.lock) continue;
cost[s.center] = 1; 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) { while (queue.length) {
const next = queue.dequeue(); const next = queue.dequeue();
@ -608,7 +618,7 @@ window.BurgsAndStates = (function () {
if (list && !list.includes(state.i)) continue; if (list && !list.includes(state.i)) continue;
byId(`stateLabel${state.i}`)?.remove(); 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"); const example = g.append("text").attr("x", 0).attr("x", 0).text("Average");

View file

@ -116,23 +116,25 @@ window.Cultures = (function () {
cultures.forEach(c => (c.base = c.base % nameBases.length)); cultures.forEach(c => (c.base = c.base % nameBases.length));
function selectCultures(c) { function selectCultures(culturesNumber) {
let def = getDefault(c); let def = getDefault(culturesNumber);
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);
const cultures = []; const cultures = [];
pack.cultures?.forEach(function (culture) { pack.cultures?.forEach(function (culture) {
if (culture.lock) cultures.push(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 { do {
rnd = rand(def.length - 1); rnd = rand(def.length - 1);
culture = def[rnd]; culture = def[rnd];
} while (!P(culture.odd)); i++;
} while (i < 200 && !P(culture.odd));
cultures.push(culture); cultures.push(culture);
def.splice(rnd, 1); def.splice(rnd, 1);
} }
@ -507,28 +509,37 @@ window.Cultures = (function () {
// expand cultures across the map (Dijkstra-like algorithm) // expand cultures across the map (Dijkstra-like algorithm)
const expand = function () { const expand = function () {
TIME && console.time("expandCultures"); TIME && console.time("expandCultures");
cells = pack.cells; const {cells, cultures} = pack;
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); 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 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) { while (queue.length) {
const next = queue.dequeue(), const {e, p, c} = queue.dequeue();
n = next.e, const {type} = pack.cultures[c];
p = next.p,
c = next.c; cells.c[e].forEach(e => {
const type = pack.cultures[c].type; const culture = cells.culture[e];
cells.c[n].forEach(e => { if (culture?.lock) return; // do not overwrite cell of locked culture
if (pack.cultures[cells.culture[e]]?.lock) return;
const biome = cells.biome[e]; const biome = cells.biome[e];
const biomeCost = getBiomeCost(c, biome, type); 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 heightCost = getHeightCost(e, cells.h[e], type);
const riverCost = getRiverCost(cells.r[e], e, type); const riverCost = getRiverCost(cells.r[e], e, type);
const typeCost = getTypeCost(cells.t[e], type); const typeCost = getTypeCost(cells.t[e], type);

View file

@ -24,7 +24,7 @@ export function open() {
function insertEditorHtml() { function insertEditorHtml() {
const editorHtml = /* html */ `<div id="culturesEditor" class="dialog stable"> const editorHtml = /* html */ `<div id="culturesEditor" class="dialog stable">
<div id="culturesHeader" class="header" style="grid-template-columns: 10em 7em 8em 4em 8em 5em 8em 8em"> <div id="culturesHeader" class="header" style="grid-template-columns: 10em 7em 9em 4em 8em 5em 7em 8em">
<div data-tip="Click to sort by culture name" class="sortable alphabetically" data-sortby="name">Culture&nbsp;</div> <div data-tip="Click to sort by culture name" class="sortable alphabetically" data-sortby="name">Culture&nbsp;</div>
<div data-tip="Click to sort by type" class="sortable alphabetically" data-sortby="type">Type&nbsp;</div> <div data-tip="Click to sort by type" class="sortable alphabetically" data-sortby="type">Type&nbsp;</div>
<div data-tip="Click to sort by culture namesbase" class="sortable" data-sortby="base">Namesbase&nbsp;</div> <div data-tip="Click to sort by culture namesbase" class="sortable" data-sortby="base">Namesbase&nbsp;</div>
@ -171,6 +171,7 @@ function culturesEditorAddLines() {
value="${c.name}" autocorrect="off" spellcheck="false" /> value="${c.name}" autocorrect="off" spellcheck="false" />
<span class="icon-cw placeholder"></span> <span class="icon-cw placeholder"></span>
<select class="cultureType placeholder">${getTypeOptions(c.type)}</select> <select class="cultureType placeholder">${getTypeOptions(c.type)}</select>
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw hide"></span>
<select data-tip="Culture namesbase. Click to change. Click on arrows to re-generate names" <select data-tip="Culture namesbase. Click to change. Click on arrows to re-generate names"
class="cultureBase">${getBaseOptions(c.base)}</select> class="cultureBase">${getBaseOptions(c.base)}</select>
<span data-tip="Cells count" class="icon-check-empty hide"></span> <span data-tip="Cells count" class="icon-check-empty hide"></span>
@ -181,8 +182,7 @@ function culturesEditorAddLines() {
<div data-tip="Culture area" class="cultureArea hide" style="width: 6em">${si(area)} ${unit}</div> <div data-tip="Culture area" class="cultureArea hide" style="width: 6em">${si(area)} ${unit}</div>
<span data-tip="${populationTip}" class="icon-male hide"></span> <span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation hide pointer" <div data-tip="${populationTip}" class="culturePopulation hide pointer"
style="width: 5em">${si(population)}</div> style="width: 4em">${si(population)}</div>
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw hide"></span>
${getShapeOptions(selectShape, c.shield)} ${getShapeOptions(selectShape, c.shield)}
</div>`; </div>`;
continue; continue;
@ -207,6 +207,7 @@ function culturesEditorAddLines() {
<span data-tip="Regenerate culture name" class="icon-cw hiddenIcon" style="visibility: hidden"></span> <span data-tip="Regenerate culture name" class="icon-cw hiddenIcon" style="visibility: hidden"></span>
<select data-tip="Culture type. Defines growth model. Click to change" <select data-tip="Culture type. Defines growth model. Click to change"
class="cultureType">${getTypeOptions(c.type)}</select> class="cultureType">${getTypeOptions(c.type)}</select>
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw hide"></span>
<select data-tip="Culture namesbase. Click to change. Click on arrows to re-generate names" <select data-tip="Culture namesbase. Click to change. Click on arrows to re-generate names"
class="cultureBase">${getBaseOptions(c.base)}</select> class="cultureBase">${getBaseOptions(c.base)}</select>
<span data-tip="Cells count" class="icon-check-empty hide"></span> <span data-tip="Cells count" class="icon-check-empty hide"></span>
@ -225,10 +226,9 @@ function culturesEditorAddLines() {
<div data-tip="Culture area" class="cultureArea hide" style="width: 6em">${si(area)} ${unit}</div> <div data-tip="Culture area" class="cultureArea hide" style="width: 6em">${si(area)} ${unit}</div>
<span data-tip="${populationTip}" class="icon-male hide"></span> <span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation hide pointer" <div data-tip="${populationTip}" class="culturePopulation hide pointer"
style="width: 5em">${si(population)}</div> style="width: 4em">${si(population)}</div>
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw hide"></span>
${getShapeOptions(selectShape, c.shield)} ${getShapeOptions(selectShape, c.shield)}
<span data-tip="Lock culture" class="icon-lock${c.lock ? '' : '-open'} hide"></span> <span data-tip="Lock culture" class="icon-lock${c.lock ? "" : "-open"} hide"></span>
<span data-tip="Remove culture" class="icon-trash-empty hide"></span> <span data-tip="Remove culture" class="icon-trash-empty hide"></span>
</div>`; </div>`;
} }
@ -251,7 +251,7 @@ function culturesEditorAddLines() {
$body.querySelectorAll("fill-box").forEach($el => $el.on("click", cultureChangeColor)); $body.querySelectorAll("fill-box").forEach($el => $el.on("click", cultureChangeColor));
$body.querySelectorAll("div > input.cultureName").forEach($el => $el.on("input", cultureChangeName)); $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 > 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.cultureType").forEach($el => $el.on("change", cultureChangeType));
$body.querySelectorAll("div > select.cultureBase").forEach($el => $el.on("change", cultureChangeBase)); $body.querySelectorAll("div > select.cultureBase").forEach($el => $el.on("change", cultureChangeBase));
$body.querySelectorAll("div > select.cultureEmblems").forEach($el => $el.on("change", cultureChangeEmblemsShape)); $body.querySelectorAll("div > select.cultureEmblems").forEach($el => $el.on("change", cultureChangeEmblemsShape));
@ -590,16 +590,23 @@ function drawCultureCenters() {
} }
function cultureCenterDrag() { function cultureCenterDrag() {
const $el = d3.select(this);
const cultureId = +this.id.slice(13); 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; 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); const cell = findCell(x, y);
if (pack.cells.h[cell] < 20) return; // ignore dragging on water if (pack.cells.h[cell] < 20) return; // ignore dragging on water
pack.cultures[cultureId].center = cell; pack.cultures[cultureId].center = cell;
recalculateCultures(); recalculateCultures();
}); }
const dragDebounced = debounce(handleDrag, 50);
d3.event.on("drag", dragDebounced);
} }
function toggleLegend() { function toggleLegend() {
@ -666,17 +673,10 @@ async function showHierarchy() {
function recalculateCultures(must) { function recalculateCultures(must) {
if (!must && !culturesAutoChange.checked) return; 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(); Cultures.expand();
drawCultures(); drawCultures();
pack.burgs.forEach(b => (b.culture = pack.cells.culture[b.cell])); pack.burgs.forEach(b => (b.culture = pack.cells.culture[b.cell]));
refreshCulturesEditor(); refreshCulturesEditor();
document.querySelector("input.cultureExpan").focus(); // to not trigger hotkeys
} }
function enterCultureManualAssignent() { function enterCultureManualAssignent() {

View file

@ -3,10 +3,10 @@ addListeners();
export function open() { export function open() {
closeDialogs("#religionsEditor, .stable"); closeDialogs("#religionsEditor, .stable");
if (!layerIsOn("toggleReligions")) toggleCultures(); if (!layerIsOn("toggleReligions")) toggleReligions();
if (layerIsOn("toggleStates")) toggleStates(); if (layerIsOn("toggleStates")) toggleStates();
if (layerIsOn("toggleBiomes")) toggleBiomes(); if (layerIsOn("toggleBiomes")) toggleBiomes();
if (layerIsOn("toggleCultures")) toggleReligions(); if (layerIsOn("toggleCultures")) toggleCultures();
if (layerIsOn("toggleProvinces")) toggleProvinces(); if (layerIsOn("toggleProvinces")) toggleProvinces();
refreshReligionsEditor(); refreshReligionsEditor();
@ -23,13 +23,15 @@ export function open() {
function insertEditorHtml() { function insertEditorHtml() {
const editorHtml = /* html */ `<div id="religionsEditor" class="dialog stable"> const editorHtml = /* html */ `<div id="religionsEditor" class="dialog stable">
<div id="religionsHeader" class="header" style="grid-template-columns: 13em 6em 7em 18em 5em 6em"> <div id="religionsHeader" class="header" style="grid-template-columns: 13em 6em 7em 18em 6em 7em 6em 7em">
<div data-tip="Click to sort by religion name" class="sortable alphabetically" data-sortby="name">Religion&nbsp;</div> <div data-tip="Click to sort by religion name" class="sortable alphabetically" data-sortby="name">Religion&nbsp;</div>
<div data-tip="Click to sort by religion type" class="sortable alphabetically icon-sort-name-down" data-sortby="type">Type&nbsp;</div> <div data-tip="Click to sort by religion type" class="sortable alphabetically icon-sort-name-down" data-sortby="type">Type&nbsp;</div>
<div data-tip="Click to sort by religion form" class="sortable alphabetically hide" data-sortby="form">Form&nbsp;</div> <div data-tip="Click to sort by religion form" class="sortable alphabetically hide" data-sortby="form">Form&nbsp;</div>
<div data-tip="Click to sort by supreme deity" class="sortable alphabetically hide" data-sortby="deity">Supreme Deity&nbsp;</div> <div data-tip="Click to sort by supreme deity" class="sortable alphabetically hide" data-sortby="deity">Supreme Deity&nbsp;</div>
<div data-tip="Click to sort by religion area" class="sortable hide" data-sortby="area">Area&nbsp;</div> <div data-tip="Click to sort by religion area" class="sortable hide" data-sortby="area">Area&nbsp;</div>
<div data-tip="Click to sort by number of believers (religion area population)" class="sortable hide" data-sortby="population">Believers&nbsp;</div> <div data-tip="Click to sort by number of believers (religion area population)" class="sortable hide" data-sortby="population">Believers&nbsp;</div>
<div data-tip="Click to sort by potential extent type" class="sortable alphabetically hide" data-sortby="expansion">Potential&nbsp;</div>
<div data-tip="Click to sort by expansionism" class="sortable hide" data-sortby="expansionism">Expansion&nbsp;</div>
</div> </div>
<div id="religionsBody" class="table" data-type="absolute"></div> <div id="religionsBody" class="table" data-type="absolute"></div>
@ -88,6 +90,11 @@ function insertEditorHtml() {
</div> </div>
<button id="religionsAdd" data-tip="Add a new religion. Hold Shift to add multiple" class="icon-plus"></button> <button id="religionsAdd" data-tip="Add a new religion. Hold Shift to add multiple" class="icon-plus"></button>
<button id="religionsExport" data-tip="Download religions-related data" class="icon-download"></button> <button id="religionsExport" data-tip="Download religions-related data" class="icon-download"></button>
<button id="religionsRecalculate" data-tip="Recalculate religions based on current values of growth-related attributes" class="icon-retweet"></button>
<span data-tip="Allow religion center, extent, and expansionism changes to take an immediate effect">
<input id="religionsAutoChange" class="checkbox" type="checkbox" />
<label for="religionsAutoChange" class="checkbox-label"><i>auto-apply changes</i></label>
</span>
</div> </div>
</div>`; </div>`;
@ -109,6 +116,7 @@ function addListeners() {
byId("religionsManuallyCancel").on("click", () => exitReligionsManualAssignment()); byId("religionsManuallyCancel").on("click", () => exitReligionsManualAssignment());
byId("religionsAdd").on("click", enterAddReligionMode); byId("religionsAdd").on("click", enterAddReligionMode);
byId("religionsExport").on("click", downloadReligionsCsv); byId("religionsExport").on("click", downloadReligionsCsv);
byId("religionsRecalculate").on("click", () => recalculateReligions(true));
} }
function refreshReligionsEditor() { function refreshReligionsEditor() {
@ -166,9 +174,10 @@ function religionsEditorAddLines() {
data-type="" data-type=""
data-form="" data-form=""
data-deity="" data-deity=""
data-expansion=""
data-expansionism="" data-expansionism=""
> >
<svg width="11" height="11" class="placeholder"></svg> <svg width="9" height="9" class="placeholder"></svg>
<input data-tip="Religion name. Click and type to change" class="religionName italic" style="width: 11em" <input data-tip="Religion name. Click and type to change" class="religionName italic" style="width: 11em"
value="${r.name}" autocorrect="off" spellcheck="false" /> value="${r.name}" autocorrect="off" spellcheck="false" />
<select data-tip="Religion type" class="religionType placeholder" style="width: 5em"> <select data-tip="Religion type" class="religionType placeholder" style="width: 5em">
@ -178,9 +187,11 @@ function religionsEditorAddLines() {
<span data-tip="Click to re-generate supreme deity" class="icon-arrows-cw placeholder hide"></span> <span data-tip="Click to re-generate supreme deity" class="icon-arrows-cw placeholder hide"></span>
<input data-tip="Religion supreme deity" class="religionDeity placeholder hide" style="width: 17em" value="" autocorrect="off" spellcheck="false" /> <input data-tip="Religion supreme deity" class="religionDeity placeholder hide" style="width: 17em" value="" autocorrect="off" spellcheck="false" />
<span data-tip="Religion area" style="padding-right: 4px" class="icon-map-o hide"></span> <span data-tip="Religion area" style="padding-right: 4px" class="icon-map-o hide"></span>
<div data-tip="Religion area" class="religionArea hide" style="width: 5em">${si(area) + unit}</div> <div data-tip="Religion area" class="religionArea hide" style="width: 6em">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male hide"></span> <span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="religionPopulation hide pointer">${si(population)}</div> <div data-tip="${populationTip}" class="religionPopulation hide pointer" style="width: 5em">${si(
population
)}</div>
</div>`; </div>`;
continue; continue;
} }
@ -195,6 +206,7 @@ function religionsEditorAddLines() {
data-type="${r.type}" data-type="${r.type}"
data-form="${r.form}" data-form="${r.form}"
data-deity="${r.deity || ""}" data-deity="${r.deity || ""}"
data-expansion="${r.expansion}"
data-expansionism="${r.expansionism}" data-expansionism="${r.expansionism}"
> >
<fill-box fill="${r.color}"></fill-box> <fill-box fill="${r.color}"></fill-box>
@ -209,13 +221,13 @@ function religionsEditorAddLines() {
<input data-tip="Religion supreme deity" class="religionDeity hide" style="width: 17em" <input data-tip="Religion supreme deity" class="religionDeity hide" style="width: 17em"
value="${r.deity || ""}" autocorrect="off" spellcheck="false" /> value="${r.deity || ""}" autocorrect="off" spellcheck="false" />
<span data-tip="Religion area" style="padding-right: 4px" class="icon-map-o hide"></span> <span data-tip="Religion area" style="padding-right: 4px" class="icon-map-o hide"></span>
<div data-tip="Religion area" class="religionArea hide" style="width: 5em">${si(area) + unit}</div> <div data-tip="Religion area" class="religionArea hide" style="width: 6em">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male hide"></span> <span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="religionPopulation hide pointer">${si(population)}</div> <div data-tip="${populationTip}" class="religionPopulation hide pointer" style="width: 5em">${si(
<span population
data-tip="Lock religion, will regenerate the origin folk and organized religion if they are not also locked" )}</div>
class="icon-lock${r.lock ? '' : '-open'} hide" ${getExpansionColumns(r)}
></span> <span data-tip="Lock this religion" class="icon-lock${r.lock ? "" : "-open"} hide"></span>
<span data-tip="Remove religion" class="icon-trash-empty hide"></span> <span data-tip="Remove religion" class="icon-trash-empty hide"></span>
</div>`; </div>`;
} }
@ -245,6 +257,8 @@ function religionsEditorAddLines() {
$body.querySelectorAll("div > input.religionDeity").forEach(el => el.on("input", religionChangeDeity)); $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 > span.icon-arrows-cw").forEach(el => el.on("click", regenerateDeity));
$body.querySelectorAll("div > div.religionPopulation").forEach(el => el.on("click", changePopulation)); $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-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").forEach($el => $el.on("click", updateLockStatus));
$body.querySelectorAll("div > span.icon-lock-open").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"; $body.dataset.type = "absolute";
togglePercentageMode(); togglePercentageMode();
} }
applySorting(religionsHeader); applySorting(religionsHeader);
$("#religionsEditor").dialog({width: fitContent()}); $("#religionsEditor").dialog({width: fitContent()});
} }
@ -264,6 +279,41 @@ function getTypeOptions(type) {
return options; 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 */ `
<span data-tip="${tip}" class="icon-resize-full-alt hide" style="padding-right: 2px"></span>
<span data-tip="${tip}" class="religionExtent hide" style="width: 5em">culture</span>
<span data-tip="${tip}" class="icon-resize-full hide"></span>
<input data-tip="${tip}" class="religionExpantion hide" disabled type="number" value='0' />`;
}
return /* html */ `
<span data-tip="Potential religion extent" class="icon-resize-full-alt hide" style="padding-right: 2px"></span>
<select data-tip="Potential religion extent" class="religionExtent hide" style="width: 5em">
${getExtentOptions(r.expansion)}
</select>
<span data-tip="Religion expansionism. Defines competitive size" class="icon-resize-full hide"></span>
<input
data-tip="Religion expansionism. Defines competitive size. Click to change, then click Recalculate to apply change"
class="religionExpantion hide"
type="number"
min="0"
max="99"
step=".1"
value=${r.expansionism}
/>`;
}
function getExtentOptions(type) {
let options = "";
const types = ["global", "state", "culture"];
types.forEach(t => (options += `<option ${type === t ? "selected" : ""} value="${t}">${t}</option>`));
return options;
}
const religionHighlightOn = debounce(event => { const religionHighlightOn = debounce(event => {
const religionId = Number(event.id || event.target.dataset.id); const religionId = Number(event.id || event.target.dataset.id);
const $el = $body.querySelector(`div[data-id='${religionId}']`); const $el = $body.querySelector(`div[data-id='${religionId}']`);
@ -272,20 +322,19 @@ const religionHighlightOn = debounce(event => {
if (!layerIsOn("toggleReligions")) return; if (!layerIsOn("toggleReligions")) return;
if (customization) return; if (customization) return;
const animate = d3.transition().duration(1500).ease(d3.easeSinIn); const animate = d3.transition().duration(2000).ease(d3.easeSinIn);
relig relig
.select("#religion" + religionId) .select("#religion" + religionId)
.raise() .raise()
.transition(animate) .transition(animate)
.attr("stroke-width", 2.5) .attr("stroke-width", 2.5)
.attr("stroke", "#c13119"); .attr("stroke", "#d0240f");
debug debug
.select("#religionsCenter" + religionId) .select("#religionsCenter" + religionId)
.raise() .raise()
.transition(animate) .transition(animate)
.attr("r", 8) .attr("r", 3)
.attr("stroke-width", 2) .attr("stroke", "#d0240f");
.attr("stroke", "#c13119");
}, 200); }, 200);
function religionHighlightOff(event) { function religionHighlightOff(event) {
@ -301,8 +350,7 @@ function religionHighlightOff(event) {
debug debug
.select("#religionsCenter" + religionId) .select("#religionsCenter" + religionId)
.transition() .transition()
.attr("r", 4) .attr("r", 2)
.attr("stroke-width", 1.2)
.attr("stroke", null); .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() { function religionRemovePrompt() {
if (customization) return; if (customization) return;
@ -471,11 +533,14 @@ function drawReligionCenters() {
const religionCenters = debug const religionCenters = debug
.append("g") .append("g")
.attr("id", "religionCenters") .attr("id", "religionCenters")
.attr("stroke-width", 1.2) .attr("stroke-width", 0.8)
.attr("stroke", "#444444") .attr("stroke", "#444444")
.style("cursor", "move"); .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 religionCenters
.selectAll("circle") .selectAll("circle")
.data(data) .data(data)
@ -483,7 +548,7 @@ function drawReligionCenters() {
.append("circle") .append("circle")
.attr("id", d => "religionsCenter" + d.i) .attr("id", d => "religionsCenter" + d.i)
.attr("data-id", d => d.i) .attr("data-id", d => d.i)
.attr("r", 4) .attr("r", 2)
.attr("fill", d => d.color) .attr("fill", d => d.color)
.attr("cx", d => pack.cells.p[d.center][0]) .attr("cx", d => pack.cells.p[d.center][0])
.attr("cy", d => pack.cells.p[d.center][1]) .attr("cy", d => pack.cells.p[d.center][1])
@ -499,15 +564,23 @@ function drawReligionCenters() {
} }
function religionCenterDrag() { function religionCenterDrag() {
const $el = d3.select(this);
const religionId = +this.dataset.id; 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; 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); const cell = findCell(x, y);
if (pack.cells.h[cell] < 20) return; // ignore dragging on water if (pack.cells.h[cell] < 20) return; // ignore dragging on water
pack.religions[religionId].center = cell; pack.religions[religionId].center = cell;
}); recalculateReligions();
}
const dragDebounced = debounce(handleDrag, 50);
d3.event.on("drag", dragDebounced);
} }
function toggleLegend() { function toggleLegend() {
@ -578,13 +651,14 @@ async function showHierarchy() {
function toggleExtinct() { function toggleExtinct() {
$body.dataset.extinct = $body.dataset.extinct !== "show" ? "show" : "hide"; $body.dataset.extinct = $body.dataset.extinct !== "show" ? "show" : "hide";
religionsEditorAddLines(); religionsEditorAddLines();
drawReligionCenters();
} }
function enterReligionsManualAssignent() { function enterReligionsManualAssignent() {
if (!layerIsOn("toggleReligions")) toggleReligions(); if (!layerIsOn("toggleReligions")) toggleReligions();
customization = 7; customization = 7;
relig.append("g").attr("id", "temp"); 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"; byId("religionsManuallyButtons").style.display = "inline-block";
debug.select("#religionCenters").style("display", "none"); debug.select("#religionCenters").style("display", "none");
@ -686,7 +760,7 @@ function exitReligionsManualAssignment(close) {
customization = 0; customization = 0;
relig.select("#temp").remove(); relig.select("#temp").remove();
removeCircle(); 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("religionsManuallyButtons").style.display = "none";
byId("religionsEditor") byId("religionsEditor")
@ -740,15 +814,15 @@ function addReligion() {
function downloadReligionsCsv() { function downloadReligionsCsv() {
const unit = getAreaUnit("2"); 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 lines = Array.from($body.querySelectorAll(":scope > div"));
const data = lines.map($line => { 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 deityText = '"' + deity + '"';
const {origins} = pack.religions[+id]; const {origins} = pack.religions[+id];
const originList = (origins || []).filter(origin => origin).map(origin => pack.religions[origin].name); const originList = (origins || []).filter(origin => origin).map(origin => pack.religions[origin].name);
const originText = '"' + originList.join(", ") + '"'; 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"); const csvData = [headers].concat(data).join("\n");
@ -773,3 +847,13 @@ function updateLockStatus() {
classList.toggle("icon-lock-open"); classList.toggle("icon-lock-open");
classList.toggle("icon-lock"); classList.toggle("icon-lock");
} }
function recalculateReligions(must) {
if (!must && !religionsAutoChange.checked) return;
Religions.recalculate();
drawReligions();
refreshReligionsEditor();
drawReligionCenters();
}

View file

@ -163,8 +163,6 @@ function addListeners() {
const line = $element.parentNode; const line = $element.parentNode;
const state = +line.dataset.id; const state = +line.dataset.id;
if (classList.contains("stateCapital")) stateChangeCapitalName(state, line, $element.value); 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) { $body.on("change", function (ev) {
@ -173,6 +171,8 @@ function addListeners() {
const line = $element.parentNode; const line = $element.parentNode;
const state = +line.dataset.id; const state = +line.dataset.id;
if (classList.contains("stateCulture")) stateChangeCulture(state, line, $element.value); 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; const growthRate = +this.value;
byId("statesNeutral").value = growthRate; byId("statesNeutral").value = growthRate;
byId("statesNeutralNumber").value = growthRate; byId("statesNeutralNumber").value = growthRate;
statesNeutral = growthRate;
tip("Growth rate: " + growthRate); tip("Growth rate: " + growthRate);
recalculateStates(false); recalculateStates(false);
} }

View file

@ -1,44 +1,481 @@
const capitalize = text => text.charAt(0).toUpperCase() + text.slice(1); export const supporters = `ken burgan
Sera's Nafitlaan
const format = rawList => Richard Rogers
rawList Hylobate
.replace(/(?:\r\n|\r|\n)/g, "") Colin deSousa
.split(",") Aurelia De La Silla
.map(name => capitalize(name.trim())) Maciej Kontny
.sort(); Ricky L Cain
Iggyflare
export const supporters = format(` Garrett Renner
Aaron Meyer,Ahmad Amerih,AstralJacks,aymeric,Billy Dean Goehring,Branndon Edwards,Chase Mayers,Curt Flood,cyninge,Dino Princip, Michael Harris
E.M. White,es,Fondue,Fritjof Olsson,Gatsu,Johan Fröberg,Jonathan Moore,Joseph Miranda,Kate,KC138,Luke Nelson,Markus Finster,Massimo Vella,Mikey, Joshua Maly
Nathan Mitchell,Paavi1,Pat,Ryan Westcott,Sasquatch,Shawn Spencer,Sizz_TV,Timothée CALLET,UTG community,Vlad Tomash,Wil Sisney,William Merriott, Nigel Guest
Xariun,Gun Metal Games,Scott Marner,Spencer Sherman,Valerii Matskevych,Alloyed Clavicle,Stewart Walsh,Ruthlyn Mollett (Javan),Benjamin Mair-Pratt, Theo Hodges
Diagonath,Alexander Thomas,Ashley Wilson-Savoury,William Henry,Preston Brooks,JOSHUA QUALTIERI,Hilton Williams,Katharina Haase,Hisham Bedri, BERTHEAS Frédéric
Ian arless,Karnat,Bird,Kevin,Jessica Thomas,Steve Hyatt,Logicspren,Alfred García,Jonathan Killstring,John Ackley,Invad3r233,Norbert Žigmund,Jennifer, lilMoni
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, Lee S.
kms,Stephen Herron,MidnightMoon,Whakomatic x,Barished,Aaron bateson,Brice Moss,Diklyquill,PatronUser,Michael Greiner,Steven Bennett,Jacob Harrington, Chris Dibbs
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, jarrad tait
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, Jacen Solo
Maxwell Hill,Drunken_Legends,rob bee,Jesse Holmes,YYako,Detocroix,Anoplexian,Hannah,Paul,Sandra Krohn,Lucid,Richard Keating,Allen Varney,Rick Falkvinge, Hannes Rotestam
Seth Fusion,Adam Butler,Gus,StroboWolf,Sadie Blackthorne,Zewen Senpai,Dell McKnight,Oneiris,Darinius Dragonclaw Studios,Christopher Whitney,Rhodes HvZ, Preston Hicks
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, Will Fink
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, ControlFreq
Alex Debus,Joshua Vaught,Kyle S,Eric Moore,Dean Dunakin,Uniquenameosaurus,WarWizardGames,Chance Mena,Jan Ka,Miguel Alejandro,Dalton Clark,Simon Drapeau, IllAngel
Radovan Zapletal,Jmmat6,Justa Badge,Blargh Blarghmoomoo,Vanessa Anjos,Grant A. Murray,Akirsop,Rikard Wolff,Jake Fish,teco 47,Antiroo,Jakob Siegel, John Giardina
Guilherme Aguiar,Jarno Hallikainen,Justin Mcclain,Kristin Chernoff,Rowland Kingman,Esther Busch,Grayson McClead,Austin,Hakon the Viking,Chad Riley, Thiago Prado
Cooper Counts,Patrick Jones,Clonetone,PlayByMail.Net,Brad Wardell,Lance Saba,Egoensis,Brea Richards,Tiber,Chris Bloom,Maxim Lowe,Aquelion, Zhang Dijon
Page One Project,Spencer Morris,Paul Ingram,Dust Bunny,Adrian Wright,Eric Alexander Cartaya,GameNight,Thomas Mortensen Hansen,Zklaus,Drinarius, NoBurny
Ed Wright,Lon Varnadore,Crys Cain,Heaven N Lee,Jeffrey Henning,Lazer Elf,Jordan Bellah,Alex Beard,Kass Frisson,Petro Lombaard,Emanuel Pietri,Rox, thibault tersinet
PinkEvil,Gavin Madrigal,Martin Lorber,Prince of Morgoth,Jaryd Armstrong,Andrew Pirkola,ThyHolyDevil,Gary Smith,Tyshaun Wise,Ethan Cook,Jon Stroman, scarletsky
Nobody679,良义 ,Chris Gray,Phoenix Boatwright,Mackenzie,Milo Cohen,Jason Matthew Wuerfel,Rasmus Legêne,Andrew Hines,Wexxler,Espen Sæverud,Binks, Nich Smith
Dominick Ormsby,Linn Browning,Václav Švec,Alan Buehne,George J.Lekkas,Alexandre Boivin,Tommy Mayfield,Skylar Mangum-Turner,Karen Blythe,Stefan Gugerel, Omegus
Mike Conley,Xavier privé,Hope You're Well,Mark Sprietsma,Robert Landry,Nick Mowry,steve hall,Markell,Josh Wren,Neutrix,BLRageQuit,Rocky, Karl Abrahamsson
Dario Spadavecchia,Bas Kroot,John Patrick Callahan Jr,Alexandra Vesey,D,Exp1nt,james,Braxton Istace,w,Rurikid,AntiBlock,Redsauz,BigE0021, Sara Fernandes
Jonathan Williams,ojacid .,Brian Wilson,A Patreon of the Ahts,Shubham Jakhotiya,www15o,Jan Bundesmann,Angelique Badger,Joshua Xiong,Moist mongol, peetey897
Frank Fewkes,jason baldrick,Game Master Pro,Andrew Kircher,Preston Mitchell,Chris Kohut,Emarandzeb,Trentin Bergeron,Damon Gallaty,Pleaseworkforonce, Cooper Janse
Jordan,William Markus,Sidr Dim,Alexander Whittaker,The Next Level,Patrick Valverde,Markus Peham,Daniel Cooper,the Beagles of Neorbus,Marley Moule, G F
Maximilian Schielke,Johnathan Xavier Hutchinson,Ele,Rita,Randy Ross,John Wick,RedSpaz,cameron cannon,Ian Grau-Fay,Kyle Barrett,Charlotte Wiland, Glen Aultman-Bettridge
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, Nathan Rogers
Aaron Blair,Neyimadd,RLKZ1022,DerWolf,Kenji Yamada,Zion,Robert Rinne,Actual_Dio,Kyarou 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`;

View file

@ -304,15 +304,34 @@ window.Religions = (function () {
Heresy: {Heresy: 1} Heresy: {Heresy: 1}
}; };
const methods = { const namingMethods = {
"Random + type": 3, Folk: {
"Random + ism": 1, "Culture + type": 1
"Supreme + ism": 5, },
"Faith of + Supreme": 5,
"Place + ism": 1, Organized: {
"Culture + ism": 2, "Random + type": 3,
"Place + ian + type": 6, "Random + ism": 1,
"Culture + type": 4 "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 = { 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"); 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 folkReligions = generateFolkReligions();
const religions = []; const organizedReligions = generateOrganizedReligions(+religionsInput.value, lockedReligions);
// add folk religions const namedReligions = specifyReligions([...folkReligions, ...organizedReligions]);
pack.cultures.forEach(c => { const indexedReligions = combineReligions(namedReligions, lockedReligions);
const newId = c.i; const religionIds = expandReligions(indexedReligions);
if (!newId) return religions.push({i: 0, name: "No religion"}); const religions = defineOrigins(religionIds, indexedReligions);
if (c.removed) { pack.religions = religions;
religions.push({ pack.cells.religion = religionIds;
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();
checkCenters(); checkCenters();
cells.religion = religionIds;
pack.religions = religions;
TIME && console.timeEnd("generateReligions"); TIME && console.timeEnd("generateReligions");
}
// growth algorithm to assign cells to religions function generateFolkReligions() {
function expandReligions() { return pack.cultures
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); .filter(c => c.i && !c.removed)
const cost = []; .map(culture => ({type: "Folk", form: rw(forms.Folk), culture: culture.i, center: culture.center}));
}
religions function generateOrganizedReligions(desiredReligionNumber, lockedReligions) {
.filter(r => !r.lock && (r.type === "Organized" || r.type === "Cult")) const cells = pack.cells;
.forEach(r => { const lockedReligionCount = lockedReligions.filter(({type}) => type !== "Folk").length || 0;
religionIds[r.center] = r.i; const requiredReligionsNumber = desiredReligionNumber - lockedReligionCount;
queue.queue({e: r.center, p: 0, r: r.i, s: cells.state[r.center], c: r.culture}); if (requiredReligionsNumber < 1) return [];
cost[r.center] = 1;
});
const neutral = (cells.i.length / 5000) * 200 * gauss(1, 0.3, 0.2, 2, 2) * neutralInput.value; // limit cost for organized religions growth const candidateCells = getCandidateCells();
const popCost = d3.max(cells.pop) / 3; // enougth population to spered religion without penalty const religionCores = placeReligions();
while (queue.length) { const cultsCount = Math.floor((rand(1, 4) / 10) * religionCores.length); // 10-40%
const {e, p, r, c, s} = queue.dequeue(); const heresiesCount = Math.floor((rand(0, 3) / 10) * religionCores.length); // 0-30%
const expansion = religions[r].expansion; const organizedCount = religionCores.length - cultsCount - heresiesCount;
cells.c[e].forEach(nextCell => { const getType = index => {
if (expansion === "culture" && c !== cells.culture[nextCell]) return; if (index < organizedCount) return "Organized";
if (expansion === "state" && s !== cells.state[nextCell]) return; if (index < organizedCount + cultsCount) return "Cult";
if (religions[religionIds[nextCell]]?.lock) return; return "Heresy";
};
const cultureCost = c !== cells.culture[nextCell] ? 10 : 0; return religionCores.map((cellId, index) => {
const stateCost = s !== cells.state[nextCell] ? 10 : 0; const type = getType(index);
const biomeCost = cells.road[nextCell] ? 1 : biomesData.cost[cells.biome[nextCell]]; const form = rw(forms[type]);
const populationCost = Math.max(rn(popCost - cells.pop[nextCell]), 0); const cultureId = cells.culture[cellId];
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;
if (!cost[nextCell] || totalCost < cost[nextCell]) { return {type, form, culture: cultureId, center: cellId};
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}); 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 return foundReligions.size ? [...foundReligions] : [fallbackOrigin];
function expandHeresies() { }
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const cost = [];
religions // growth algorithm to assign cells to religions
.filter(r => !r.lock && r.type === "Heresy") function expandReligions(religions) {
.forEach(r => { const cells = pack.cells;
const b = religionIds[r.center]; // "base" religion id const religionIds = spreadFolkReligions(religions);
religionIds[r.center] = r.i; // heresy id
queue.queue({e: r.center, p: 0, r: r.i, b});
cost[r.center] = 1;
});
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 maxExpansionCost = (cells.i.length / 20) * neutralInput.value; // limit cost for organized religions growth
const {e, p, r, b} = queue.dequeue();
cells.c[e].forEach(nextCell => { const biomePassageCost = cellId => biomesData.cost[cells.biome[cellId]];
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);
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]) { const religionsMap = new Map(religions.map(r => [r.i, r]));
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});
}
});
}
}
function checkCenters() { const isMainRoad = cellId => cells.road[cellId] - cells.crossroad[cellId] > 4;
const codes = religions.map(r => r.code); const isTrail = cellId => cells.h[cellId] > 19 && cells.road[cellId] - cells.crossroad[cellId] === 1;
religions.forEach(r => { const isSeaRoute = cellId => cells.h[cellId] < 20 && cells.road[cellId];
if (!r.i) return; const isWater = cellId => cells.h[cellId] < 20;
r.code = abbreviate(r.name, codes);
// move religion center if it's not within religion area after expansion while (queue.length) {
if (religionIds[r.center] === r.i) return; // in area const {e: cellId, p, r, s: state} = queue.dequeue();
const firstCell = cells.i.find(i => religionIds[i] === r.i); const {culture, expansion, expansionism} = religionsMap.get(r);
if (firstCell) r.center = firstCell; // move center, othervise it's an extinct religion
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 add = function (center) {
const {cells, religions} = pack; const {cells, cultures, religions} = pack;
const religionId = cells.religion[center]; const religionId = cells.religion[center];
const i = religions.length;
const culture = cells.culture[center]; const cultureId = cells.culture[center];
const color = getMixedColor(religions[religionId].color, 0.3, 0); 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 = const type = missingFolk
religions[religionId].type === "Organized" ? rw({Organized: 4, Cult: 1, Heresy: 2}) : rw({Organized: 5, Cult: 2}); ? "Folk"
: religions[religionId].type === "Organized"
? rw({Organized: 4, Cult: 1, Heresy: 2})
: rw({Organized: 5, Cult: 2});
const form = rw(forms[type]); const form = rw(forms[type]);
const deity = 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; const [name, expansion] = generateReligionName(type, form, deity, center);
if (type === "Organized") [name, expansion] = getReligionName(form, deity, center);
else {
name = getCultName(form, center);
expansion = "global";
}
const formName = type === "Heresy" ? religions[religionId].form : form; const formName = type === "Heresy" ? religions[religionId].form : form;
const code = abbreviate( const code = abbreviate(
name, name,
religions.map(r => r.code) 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({ religions.push({
i, i,
name, name,
color, color,
culture, culture: cultureId,
type, type,
form: formName, form: formName,
deity, deity,
expansion, expansion,
expansionism: 0, expansionism: expansionismMap[type](),
center, center,
cells: 0, cells: 0,
area: 0, area: 0,
rural: 0, rural: 0,
urban: 0, urban: 0,
origins: [religionId], origins,
code code
}); });
cells.religion[center] = i; cells.religion[center] = i;
}; };
function updateCultures() { function updateCultures() {
TIME && console.time("updateCulturesForReligions");
pack.religions = pack.religions.map((religion, index) => { pack.religions = pack.religions.map((religion, index) => {
if (index === 0) { if (index === 0) return religion;
return religion;
}
return {...religion, culture: pack.cells.culture[religion.center]}; return {...religion, culture: pack.cells.culture[religion.center]};
}); });
TIME && console.timeEnd("updateCulturesForReligions");
} }
// get supreme deity name // get supreme deity name
@ -735,22 +789,24 @@ window.Religions = (function () {
if (a === "Number") return ra(base.number); if (a === "Number") return ra(base.number);
if (a === "Being") return ra(base.being); if (a === "Being") return ra(base.being);
if (a === "Adjective") return ra(base.adjective); if (a === "Adjective") return ra(base.adjective);
if (a === "Color + Animal") return ra(base.color) + " " + ra(base.animal); 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 + Animal") return `${ra(base.adjective)} ${ra(base.animal)}`;
if (a === "Adjective + Being") return ra(base.adjective) + " " + ra(base.being); if (a === "Adjective + Being") return `${ra(base.adjective)} ${ra(base.being)}`;
if (a === "Adjective + Genitive") return ra(base.adjective) + " " + ra(base.genitive); 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 + Being") return `${ra(base.color)} ${ra(base.being)}`;
if (a === "Color + Genitive") return ra(base.color) + " " + ra(base.genitive); 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 + 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 === "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 === "Animal + of + Genitive") return `${ra(base.animal)} of ${ra(base.genitive)}`;
if (a === "Adjective + Being + of + 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") 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 {cells, cultures, burgs, states} = pack;
const random = () => Names.getCulture(cells.culture[center], null, null, "", 0); const random = () => Names.getCulture(cells.culture[center], null, null, "", 0);
@ -766,7 +822,7 @@ window.Religions = (function () {
return adj ? getAdjective(name) : name; 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 + type") return [random() + " " + type(), "global"];
if (m === "Random + ism") return [trimVowels(random()) + "ism", "global"]; if (m === "Random + ism") return [trimVowels(random()) + "ism", "global"];
if (m === "Supreme + ism" && deity) return [trimVowels(supreme()) + "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 === "Culture + ism") return [trimVowels(culture()) + "ism", "culture"];
if (m === "Place + ian + type") return [place("adj") + " " + type(), "state"]; if (m === "Place + ian + type") return [place("adj") + " " + type(), "state"];
if (m === "Culture + type") return [culture() + " " + type(), "culture"]; 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 return [trimVowels(random()) + "ism", "global"]; // else
} }
function getCultName(form, center) { return {generate, add, getDeityName, updateCultures, recalculate};
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};
})(); })();

View file

@ -48,7 +48,9 @@ window.Rivers = (function () {
cells.fl[i] += prec[cells.g[i]] / cellsNumberModifier; // add flux from precipitation cells.fl[i] += prec[cells.g[i]] / cellsNumberModifier; // add flux from precipitation
// create lake outlet if lake is not in deep depression and flux > evaporation // 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) { for (const lake of lakes) {
const lakeCell = cells.c[i].find(c => h[c] < 20 && cells.f[c] === lake.i); 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 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 length = getApproximateLength(meanderedPoints);
const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, 0)); 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); return getBasin(parent);
}; };
const getNextId = function (rivers) {
return rivers.length ? Math.max(...rivers.map(r => r.i)) + 1 : 1;
};
return { return {
generate, generate,
alterHeights, alterHeights,
@ -493,6 +510,7 @@ window.Rivers = (function () {
getOffset, getOffset,
getApproximateLength, getApproximateLength,
getRiverPoints, getRiverPoints,
remove remove,
getNextId
}; };
})(); })();

View file

@ -1176,18 +1176,18 @@ function refreshAllEditors() {
// dynamically loaded editors // dynamically loaded editors
async function editStates() { async function editStates() {
if (customization) return; 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(); Editor.open();
} }
async function editCultures() { async function editCultures() {
if (customization) return; 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(); Editor.open();
} }
async function editReligions() { async function editReligions() {
if (customization) return; 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(); Editor.open();
} }

View file

@ -204,6 +204,13 @@ function editHeightmap(options) {
INFO && console.group("Edit Heightmap"); INFO && console.group("Edit Heightmap");
TIME && console.time("regenerateErasedData"); TIME && console.time("regenerateErasedData");
// remove data
pack.cultures = [];
pack.burgs = [];
pack.states = [];
pack.provinces = [];
pack.religions = [];
const erosionAllowed = allowErosion.checked; const erosionAllowed = allowErosion.checked;
markFeatures(); markFeatures();
markupGridOcean(); markupGridOcean();
@ -231,8 +238,10 @@ function editHeightmap(options) {
Lakes.defineGroup(); Lakes.defineGroup();
defineBiomes(); defineBiomes();
rankCells(); rankCells();
Cultures.generate(); Cultures.generate();
Cultures.expand(); Cultures.expand();
BurgsAndStates.generate(); BurgsAndStates.generate();
Religions.generate(); Religions.generate();
BurgsAndStates.defineStateForms(); BurgsAndStates.defineStateForms();

View file

@ -67,11 +67,11 @@ function editIce() {
function addIcebergOnClick() { function addIcebergOnClick() {
const [x, y] = d3.mouse(this); const [x, y] = d3.mouse(this);
const i = findGridCell(x, y, grid); const i = findGridCell(x, y, grid);
const c = grid.points[i]; const [cx, cy] = grid.points[i];
const s = +document.getElementById("iceSize").value; 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 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", s); const iceberg = ice.append("polygon").attr("points", points).attr("cell", i).attr("size", size);
iceberg.call(d3.drag().on("drag", dragElement)); iceberg.call(d3.drag().on("drag", dragElement));
if (d3.event.shiftKey === false) toggleAdd(); if (d3.event.shiftKey === false) toggleAdd();
} }

View file

@ -671,11 +671,10 @@ function toggleIce(event) {
} }
function drawIce() { function drawIce() {
const cells = grid.cells, const {cells, vertices} = grid;
vertices = grid.vertices, const {temp, h} = cells;
n = cells.i.length, const n = cells.i.length;
temp = cells.temp,
h = cells.h;
const used = new Uint8Array(cells.i.length); const used = new Uint8Array(cells.i.length);
Math.random = aleaPRNG(seed); Math.random = aleaPRNG(seed);
@ -700,23 +699,22 @@ function drawIce() {
continue; continue;
} }
const tNormalized = normalize(t, -8, 2);
const randomFactor = t > -5 ? 0.4 + rand() * 1.2 : 1;
// mildly cold: iceberd // 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 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 let size = 1 - tNormalized; // iceberg size: 0 = zero size, 1 = full size
size = Math.min(size * (0.4 + rand() * 1.2), 0.95); // randomize iceberg size if (cells.t[i] === -1) size /= 1.3; // coasline: smaller icebers
resizePolygon(i, size); resizePolygon(i, minmax(rn(size * randomFactor, 2), 0.08, 1));
} }
function resizePolygon(i, s) { function resizePolygon(i, size) {
const c = grid.points[i]; const [cx, cy] = 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]); const points = getGridPolygon(i).map(([x, y]) => [rn(lerp(cx, x, size), 2), rn(lerp(cy, y, size), 2)]);
ice ice.append("polygon").attr("points", points).attr("cell", i).attr("size", size);
.append("polygon")
.attr("points", points)
.attr("cell", i)
.attr("size", rn(1 - s, 2));
} }
// connect vertices to chain // connect vertices to chain

View file

@ -42,12 +42,11 @@ function editNotes(id, name) {
$("#notesEditor").dialog({ $("#notesEditor").dialog({
title: "Notes Editor", title: "Notes Editor",
width: "minmax(80vw, 540px)", width: window.innerWidth * 0.8,
height: window.innerHeight * 0.75, height: window.innerHeight * 0.75,
position: {my: "center", at: "center", of: "svg"}, position: {my: "center", at: "center", of: "svg"},
close: removeEditor close: removeEditor
}); });
$("[aria-describedby='notesEditor']").css("top", "10vh");
if (modules.editNotes) return; if (modules.editNotes) return;
modules.editNotes = true; modules.editNotes = true;

View file

@ -77,12 +77,15 @@ document
// show popup with a list of Patreon supportes (updated manually) // show popup with a list of Patreon supportes (updated manually)
async function showSupporters() { async function showSupporters() {
const {supporters} = await import("../dynamic/supporters.js?v=19062022"); const {supporters} = await import("../dynamic/supporters.js?v=19062022");
const list = supporters.split("\n").sort();
const columns = window.innerWidth < 800 ? 2 : 5;
alertMessage.innerHTML = alertMessage.innerHTML =
"<ul style='column-count: 5; column-gap: 2em'>" + supporters.map(n => `<li>${n}</li>`).join("") + "</ul>"; `<ul style='column-count: ${columns}; column-gap: 2em'>` + list.map(n => `<li>${n}</li>`).join("") + "</ul>";
$("#alert").dialog({ $("#alert").dialog({
resizable: false, resizable: false,
title: "Patreon Supporters", title: "Patreon Supporters",
width: "54vw", width: "min-width",
position: {my: "center", at: "center", of: "svg"} position: {my: "center", at: "center", of: "svg"}
}); });
} }
@ -157,9 +160,20 @@ optionsContent.addEventListener("click", function (event) {
}); });
function mapSizeInputChange() { function mapSizeInputChange() {
const $mapWidthInput = byId("mapWidthInput");
const $mapHeightInput = byId("mapHeightInput");
changeMapSize(); changeMapSize();
localStorage.setItem("mapWidth", mapWidthInput.value); localStorage.setItem("mapWidth", $mapWidthInput.value);
localStorage.setItem("mapHeight", mapHeightInput.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 // 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; 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() { function randomizeOptions() {
const randomize = new URL(window.location.href).searchParams.get("options") === "default"; // ignore stored options const randomize = new URL(window.location.href).searchParams.get("options") === "default"; // ignore stored options
@ -546,7 +560,7 @@ function randomizeOptions() {
manorsInput.value = 1000; manorsInput.value = 1000;
manorsOutput.value = "auto"; 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("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("neutral")) neutralInput.value = neutralOutput.value = rn(1 + Math.random(), 1);
if (randomize || !locked("cultures")) culturesInput.value = culturesOutput.value = gauss(12, 3, 5, 30); if (randomize || !locked("cultures")) culturesInput.value = culturesOutput.value = gauss(12, 3, 5, 30);
@ -602,17 +616,17 @@ function randomizeCultureSet() {
function setRendering(value) { function setRendering(value) {
viewbox.attr("shape-rendering", value); viewbox.attr("shape-rendering", value);
if (value === "optimizeSpeed") { // if (value === "optimizeSpeed") {
// block some styles // // block some styles
coastline.select("#sea_island").style("filter", "none"); // coastline.select("#sea_island").style("filter", "none");
statesHalo.style("display", "none"); // statesHalo.style("display", "none");
emblems.style("opacity", 1); // emblems.style("opacity", 1);
} else { // } else {
// remove style block // // remove style block
coastline.select("#sea_island").style("filter", null); // coastline.select("#sea_island").style("filter", null);
statesHalo.style("display", null); // statesHalo.style("display", null);
emblems.style("opacity", null); // emblems.style("opacity", null);
} // }
} }
// generate current year and era name // generate current year and era name

View file

@ -74,12 +74,13 @@ function createRiver() {
function addRiver() { function addRiver() {
const {rivers, cells} = pack; 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; const riverCells = createRiver.cells;
if (riverCells.length < 2) return tip("Add at least 2 cells", false, "error"); 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; const parent = cells.r[last(riverCells)] || riverId;
riverCells.forEach(cell => { riverCells.forEach(cell => {
@ -100,12 +101,30 @@ function createRiver() {
const name = getName(mouth); const name = getName(mouth);
const basin = getBasin(parent); 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; const id = "river" + riverId;
// render river // render river
lineGen.curve(d3.curveCatmullRom.alpha(0.1)); 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); editRiver(id);
} }

View file

@ -1,4 +1,7 @@
"use strict"; "use strict";
const CONTROL_POINST_DISTANCE = 10;
function editRoute(onClick) { function editRoute(onClick) {
if (customization) return; if (customization) return;
if (!onClick && elSelected && d3.event.target.id === elSelected.attr("id")) return; if (!onClick && elSelected && d3.event.target.id === elSelected.attr("id")) return;
@ -47,13 +50,13 @@ function editRoute(onClick) {
} }
function drawControlPoints(node) { function drawControlPoints(node) {
const l = node.getTotalLength(); const totalLength = node.getTotalLength();
const increment = l / Math.ceil(l / 4); const increment = totalLength / Math.ceil(totalLength / CONTROL_POINST_DISTANCE);
for (let i = 0; i <= l; i += increment) { for (let i = 0; i <= totalLength; i += increment) {
const point = node.getPointAtLength(i); const point = node.getPointAtLength(i);
addControlPoint([point.x, point.y]); 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) { function addControlPoint(point, before = null) {

View file

@ -1,14 +1,27 @@
// UI module to control the style presets // UI module to control the style presets
"use strict"; "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_"; const customPresetPrefix = "fmgStyle_";
// add style presets to list // add style presets to list
{ {
const systemOptions = systemPresets.map(styleName => `<option value="${styleName}">${styleName}</option>`); const systemOptions = systemPresets.map(styleName => `<option value="${styleName}">${styleName}</option>`);
const storedStyles = Object.keys(localStorage).filter(key => key.startsWith(customPresetPrefix)); const storedStyles = Object.keys(localStorage).filter(key => key.startsWith(customPresetPrefix));
const customOptions = storedStyles.map(styleName => `<option value="${styleName}">${styleName.replace(customPresetPrefix, "")} [custom]</option>`); const customOptions = storedStyles.map(
styleName => `<option value="${styleName}">${styleName.replace(customPresetPrefix, "")} [custom]</option>`
);
const options = systemOptions.join("") + customOptions.join(""); const options = systemOptions.join("") + customOptions.join("");
document.getElementById("stylePreset").innerHTML = options; document.getElementById("stylePreset").innerHTML = options;
} }
@ -37,7 +50,8 @@ async function getStylePreset(desiredPreset) {
const isValid = JSON.isValid(storedStyleJSON); const isValid = JSON.isValid(storedStyleJSON);
if (isValid) return [desiredPreset, JSON.parse(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"; presetToLoad = "default";
} }
} }
@ -145,8 +159,31 @@ function addStylePreset() {
"#stateBorders": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter"], "#stateBorders": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter"],
"#provinceBorders": ["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"], "#cells": ["opacity", "stroke", "stroke-width", "filter", "mask"],
"#gridOverlay": ["opacity", "scale", "dx", "dy", "type", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "transform", "filter", "mask"], "#gridOverlay": [
"#coordinates": ["opacity", "data-size", "font-size", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter", "mask"], "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"], "#compass": ["opacity", "transform", "filter", "mask", "shape-rendering"],
"#rose": ["transform"], "#rose": ["transform"],
"#relig": ["opacity", "stroke", "stroke-width", "filter"], "#relig": ["opacity", "stroke", "stroke-width", "filter"],
@ -174,7 +211,17 @@ function addStylePreset() {
"#statesBody": ["opacity", "filter"], "#statesBody": ["opacity", "filter"],
"#statesHalo": ["opacity", "data-width", "stroke-width", "filter"], "#statesHalo": ["opacity", "data-width", "stroke-width", "filter"],
"#provs": ["opacity", "fill", "font-size", "font-family", "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"], "#ice": ["opacity", "fill", "stroke", "stroke-width", "filter"],
"#emblems": ["opacity", "stroke-width", "filter"], "#emblems": ["opacity", "stroke-width", "filter"],
"#texture": ["opacity", "filter", "mask"], "#texture": ["opacity", "filter", "mask"],
@ -184,16 +231,65 @@ function addStylePreset() {
"#oceanBase": ["fill"], "#oceanBase": ["fill"],
"#oceanicPattern": ["href", "opacity"], "#oceanicPattern": ["href", "opacity"],
"#terrs": ["opacity", "scheme", "terracing", "skip", "relax", "curve", "filter", "mask"], "#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"], "#legendBox": ["fill", "fill-opacity"],
"#burgLabels > #cities": ["opacity", "fill", "text-shadow", "data-size", "font-size", "font-family"], "#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"], "#anchors > #cities": ["opacity", "fill", "size", "stroke", "stroke-width"],
"#burgLabels > #towns": ["opacity", "fill", "text-shadow", "data-size", "font-size", "font-family"], "#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"], "#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 > #states": [
"#labels > #addedLabels": ["opacity", "fill", "stroke", "stroke-width", "text-shadow", "data-size", "font-size", "font-family", "filter"], "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"] "#fogging": ["opacity", "fill", "filter"]
}; };
@ -238,7 +334,8 @@ function addStylePreset() {
if (!styleJSON) return tip("Please provide a style JSON", false, "error"); 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 (!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 (!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; const presetName = customPresetPrefix + desiredName;
applyOption(stylePreset, presetName, desiredName + " [custom]"); applyOption(stylePreset, presetName, desiredName + " [custom]");

View file

@ -74,10 +74,8 @@ toolsContent.addEventListener("click", function (event) {
}); });
function processFeatureRegeneration(event, button) { function processFeatureRegeneration(event, button) {
if (button === "regenerateStateLabels") { if (button === "regenerateStateLabels") BurgsAndStates.drawStateLabels();
BurgsAndStates.drawStateLabels(); else if (button === "regenerateReliefIcons") {
if (!layerIsOn("toggleLabels")) toggleLabels();
} else if (button === "regenerateReliefIcons") {
ReliefIcons(); ReliefIcons();
if (!layerIsOn("toggleRelief")) toggleRelief(); if (!layerIsOn("toggleRelief")) toggleRelief();
} else if (button === "regenerateRoutes") { } else if (button === "regenerateRoutes") {
@ -628,10 +626,11 @@ function addRiverOnClick() {
getType, getType,
getWidth, getWidth,
getOffset, getOffset,
getApproximateLength getApproximateLength,
getNextId
} = Rivers; } = Rivers;
const riverCells = []; const riverCells = [];
let riverId = rivers.length ? last(rivers).i + 1 : 1; let riverId = getNextId(rivers);
let parent = riverId; let parent = riverId;
const initialFlux = grid.cells.prec[cells.g[i]]; const initialFlux = grid.cells.prec[cells.g[i]];

View file

@ -226,7 +226,7 @@
"opacity": 0.4, "opacity": 0.4,
"data-width": 10, "data-width": 10,
"stroke-width": 10, "stroke-width": 10,
"filter": "blur(5px)" "filter": "blur(3.5px)"
}, },
"#provs": { "#provs": {
"opacity": 0.7, "opacity": 0.7,

View file

@ -192,18 +192,18 @@
"filter": null "filter": null
}, },
"#roads": { "#roads": {
"opacity": 0.9, "opacity": 0.8,
"stroke": "#3c1d0b", "stroke": "#95481a",
"stroke-width": 1.37, "stroke-width": 0.8,
"stroke-dasharray": 2, "stroke-dasharray": 2,
"stroke-linecap": "inherit", "stroke-linecap": "inherit",
"filter": null, "filter": null,
"mask": null "mask": null
}, },
"#trails": { "#trails": {
"opacity": 0.9, "opacity": 0.8,
"stroke": "#95481a", "stroke": "#95481a",
"stroke-width": 0.88, "stroke-width": 0.5,
"stroke-dasharray": ".8 1.6", "stroke-dasharray": ".8 1.6",
"stroke-linecap": "butt", "stroke-linecap": "butt",
"filter": null, "filter": null,

389
styles/pale.json Normal file
View file

@ -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
}
}

8
sw.js
View file

@ -8,7 +8,10 @@ const {ExpirationPlugin} = workbox.expiration;
const DAY = 24 * 60 * 60; const DAY = 24 * 60 * 60;
const getPolitics = ({entries, days}) => { 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( registerRoute(
@ -21,7 +24,8 @@ registerRoute(
); );
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({ new CacheFirst({
cacheName: "fmg-scripts", cacheName: "fmg-scripts",
plugins: getPolitics({entries: 100, days: 30}) plugins: getPolitics({entries: 100, days: 30})

View file

@ -20,3 +20,7 @@ function lim(v) {
function normalize(val, min, max) { function normalize(val, min, max) {
return minmax((val - min) / (max - min), 0, 1); return minmax((val - min) / (max - min), 0, 1);
} }
function lerp(a, b, t) {
return a + (b - a) * t;
}

View file

@ -1,7 +1,7 @@
"use strict"; "use strict";
// version and caching control // 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; document.title += " v" + version;
@ -28,6 +28,7 @@ const version = "1.89.00"; // generator version, update each time
<ul> <ul>
<strong>Latest changes:</strong> <strong>Latest changes:</strong>
<li>Religions can be edited and redrawn like cultures</li>
<li>Lock states, provinces, cultures, and religions from regeneration</li> <li>Lock states, provinces, cultures, and religions from regeneration</li>
<li>Heightmap brushes: linear edit option</li> <li>Heightmap brushes: linear edit option</li>
<li>Data Charts screen</li> <li>Data Charts screen</li>