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
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).

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:
1. Open tangrams.github.io
1. Open https://tangrams.github.io/heightmapper
2. Toggle off auto-exposure
3. Set max elevation to 2000
4. Set min elevation to -500
5. Find region you like
6. Render image
7. Optionally rescale image to a smaller size (e.g. 500x300px) as high resolution is not used
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;
}
#battleBody div.battlePhases,
#battleBottom div.battleTypes {
position: fixed;
background-color: #ffffff30;
}
#battleBody div.battlePhases {
position: absolute;
background-color: #ffffff30;
}
#battleBody div.battlePhases > button,
#battleBottom div.battleTypes > button {
width: 3.2em;
@ -2045,6 +2049,7 @@ div.textual span,
}
#notesLegend {
width: auto;
height: 87%;
outline: 0;
overflow-y: auto;
@ -2239,7 +2244,6 @@ svg.button {
user-select: none;
}
.dontAsk {
margin: 0.9em 0 0 0.6em;
display: inline-flex;
@ -2338,7 +2342,7 @@ svg.button {
}
@media (prefers-color-scheme: dark) {
body {
background: #25252a;
}
body {
background: #25252a;
}
}

View file

@ -3,14 +3,33 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Azgaar's Fantasy Map Generator</title>
<meta name="application-name" content="Azgaar's Fantasy Map Generator" />
<meta name="author" content="Azgaar (Max Ganiev)" />
<meta name="description" content="Azgaar's Fantasy Map Generator and Editor" />
<meta name="author" content="Azgaar" />
<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: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 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-16x16.png" sizes="16x16" />
<link rel="apple-touch-icon" href="images/icons/maskable_icon_x192.png" />
@ -108,7 +127,7 @@
}
</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="libs/jquery-ui.css" as="style" onload="this.onload=null; this.rel='stylesheet'" />
</head>
@ -1237,10 +1256,6 @@
<td><select id="styleStatesBodyFilter" /></td>
</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">
<td>Halo width</td>
<td>
@ -1446,7 +1461,7 @@
</p>
<table>
<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>Canvas size</td>
@ -1458,7 +1473,7 @@
</td>
<td>
<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"
class="icon-resize-full-alt"
></i>
@ -1883,8 +1898,8 @@
<td>Rendering</td>
<td>
<select id="shapeRendering" data-stored="shapeRendering">
<option value="geometricPrecision" selected>Best quality</option>
<option value="optimizeSpeed">Best performace</option>
<option value="geometricPrecision">Best quality</option>
<option value="optimizeSpeed" selected>Best performace</option>
</select>
</td>
<td></td>
@ -2749,7 +2764,7 @@
<div id="iceEditor" class="dialog" style="display: none">
<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>
<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="iceRemove"
@ -5602,7 +5617,7 @@
</div>
<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">
<span>Select from the list or paste a Unicode character here: </span>
<input id="iconInput" style="width: 2em" />
@ -7830,7 +7845,7 @@
<script src="utils/colorUtils.js"></script>
<script src="utils/graphUtils.js?v=1.88.02"></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/probabilityUtils.js?v=1.88.00"></script>
<script src="utils/stringUtils.js"></script>
@ -7841,14 +7856,14 @@
<script src="config/heightmap-templates.js"></script>
<script src="config/precreated-heightmaps.js"></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/river-generator.js"></script>
<script src="modules/ocean-layers.js?v=1.89.08"></script>
<script src="modules/river-generator.js?v=1.89.13"></script>
<script src="modules/lakes.js"></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/burgs-and-states.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.07"></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/markers-generator.js?v=1.87.13"></script>
<script src="modules/coa-generator.js"></script>
@ -7859,34 +7874,34 @@
<script src="modules/fonts.js"></script>
<script src="modules/ui/layers.js"></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/options.js?v=1.88.02"></script>
<script src="main.js?v=1.88.02"></script>
<script src="modules/ui/options.js?v=1.88.14"></script>
<script src="main.js?v=1.89.12"></script>
<script defer src="modules/relief-icons.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/tools.js?v=1.89.00"></script>
<script defer src="modules/ui/editors.js?v=1.89.12"></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/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/biomes-editor.js"></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/temperature-graph.js"></script>
<script defer src="modules/ui/routes-editor.js"></script>
<script defer src="modules/ui/ice-editor.js"></script>
<script defer src="modules/ui/routes-editor.js?v=1.89.04"></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/coastline-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-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/burg-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/zones-editor.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 urbanization = +document.getElementById("urbanizationInput").value;
let urbanDensity = +document.getElementById("urbanDensityInput").value;
let statesNeutral = 1; // statesEditor growth parameter
applyStoredOptions();
@ -695,6 +694,7 @@ async function generate(options) {
if (shouldRegenerateGrid(grid, precreatedSeed)) grid = precreatedGraph || generateGrid();
else delete grid.cells.h;
grid.cells.h = await HeightmapGenerator.generate(grid);
pack = {}; // reset pack
markFeatures();
markupGridOcean();

View file

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

View file

@ -116,23 +116,25 @@ window.Cultures = (function () {
cultures.forEach(c => (c.base = c.base % nameBases.length));
function selectCultures(c) {
let def = getDefault(c);
if (c === def.length) return def;
if (def.every(d => d.odd === 1)) return def.splice(0, c);
const count = Math.min(c, def.length);
function selectCultures(culturesNumber) {
let def = getDefault(culturesNumber);
const cultures = [];
pack.cultures?.forEach(function (culture) {
if (culture.lock) cultures.push(culture);
});
if (!cultures.length) {
if (culturesNumber === def.length) return def;
if (def.every(d => d.odd === 1)) return def.splice(0, culturesNumber);
}
for (let culture, rnd, i = 0; cultures.length < count && i < 200; i++) {
for (let culture, rnd, i = 0; cultures.length < culturesNumber && def.length > 0;) {
do {
rnd = rand(def.length - 1);
culture = def[rnd];
} while (!P(culture.odd));
i++;
} while (i < 200 && !P(culture.odd));
cultures.push(culture);
def.splice(rnd, 1);
}
@ -507,28 +509,37 @@ window.Cultures = (function () {
// expand cultures across the map (Dijkstra-like algorithm)
const expand = function () {
TIME && console.time("expandCultures");
cells = pack.cells;
const {cells, cultures} = pack;
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
pack.cultures.forEach(function (c) {
if (!c.i || c.removed || c.lock) return;
queue.queue({e: c.center, p: 0, c: c.i});
});
const neutral = (cells.i.length / 5000) * 3000 * neutralInput.value; // limit cost for culture growth
const cost = [];
const neutralRate = byId("neutralRate")?.valueAsNumber || 1;
const neutral = cells.i.length * 0.6 * neutralRate; // limit cost for culture growth
// remove culture from all cells except of locked
for (const cellId of cells.i) {
const culture = cultures[cells.culture[cellId]];
if (culture.lock) continue;
cells.culture[cellId] = 0;
}
for (const culture of cultures) {
if (!culture.i || culture.removed) continue;
queue.queue({e: culture.center, p: 0, c: culture.i});
}
while (queue.length) {
const next = queue.dequeue(),
n = next.e,
p = next.p,
c = next.c;
const type = pack.cultures[c].type;
cells.c[n].forEach(e => {
if (pack.cultures[cells.culture[e]]?.lock) return;
const {e, p, c} = queue.dequeue();
const {type} = pack.cultures[c];
cells.c[e].forEach(e => {
const culture = cells.culture[e];
if (culture?.lock) return; // do not overwrite cell of locked culture
const biome = cells.biome[e];
const biomeCost = getBiomeCost(c, biome, type);
const biomeChangeCost = biome === cells.biome[n] ? 0 : 20; // penalty on biome change
const biomeChangeCost = biome === cells.biome[e] ? 0 : 20; // penalty on biome change
const heightCost = getHeightCost(e, cells.h[e], type);
const riverCost = getRiverCost(cells.r[e], e, type);
const typeCost = getTypeCost(cells.t[e], type);

View file

@ -24,7 +24,7 @@ export function open() {
function insertEditorHtml() {
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 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>
@ -171,6 +171,7 @@ function culturesEditorAddLines() {
value="${c.name}" autocorrect="off" spellcheck="false" />
<span class="icon-cw placeholder"></span>
<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"
class="cultureBase">${getBaseOptions(c.base)}</select>
<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>
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation hide pointer"
style="width: 5em">${si(population)}</div>
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw hide"></span>
style="width: 4em">${si(population)}</div>
${getShapeOptions(selectShape, c.shield)}
</div>`;
continue;
@ -207,6 +207,7 @@ function culturesEditorAddLines() {
<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"
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"
class="cultureBase">${getBaseOptions(c.base)}</select>
<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>
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation hide pointer"
style="width: 5em">${si(population)}</div>
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw hide"></span>
style="width: 4em">${si(population)}</div>
${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>
</div>`;
}
@ -251,7 +251,7 @@ function culturesEditorAddLines() {
$body.querySelectorAll("fill-box").forEach($el => $el.on("click", cultureChangeColor));
$body.querySelectorAll("div > input.cultureName").forEach($el => $el.on("input", cultureChangeName));
$body.querySelectorAll("div > span.icon-cw").forEach($el => $el.on("click", cultureRegenerateName));
$body.querySelectorAll("div > input.cultureExpan").forEach($el => $el.on("input", cultureChangeExpansionism));
$body.querySelectorAll("div > input.cultureExpan").forEach($el => $el.on("change", cultureChangeExpansionism));
$body.querySelectorAll("div > select.cultureType").forEach($el => $el.on("change", cultureChangeType));
$body.querySelectorAll("div > select.cultureBase").forEach($el => $el.on("change", cultureChangeBase));
$body.querySelectorAll("div > select.cultureEmblems").forEach($el => $el.on("change", cultureChangeEmblemsShape));
@ -590,16 +590,23 @@ function drawCultureCenters() {
}
function cultureCenterDrag() {
const $el = d3.select(this);
const cultureId = +this.id.slice(13);
d3.event.on("drag", () => {
const tr = parseTransform(this.getAttribute("transform"));
const x0 = +tr[0] - d3.event.x;
const y0 = +tr[1] - d3.event.y;
function handleDrag() {
const {x, y} = d3.event;
$el.attr("cx", x).attr("cy", y);
this.setAttribute("transform", `translate(${x0 + x},${y0 + y})`);
const cell = findCell(x, y);
if (pack.cells.h[cell] < 20) return; // ignore dragging on water
pack.cultures[cultureId].center = cell;
recalculateCultures();
});
}
const dragDebounced = debounce(handleDrag, 50);
d3.event.on("drag", dragDebounced);
}
function toggleLegend() {
@ -666,17 +673,10 @@ async function showHierarchy() {
function recalculateCultures(must) {
if (!must && !culturesAutoChange.checked) return;
pack.cells.culture = new Uint16Array(pack.cells.i.length);
pack.cultures.forEach(function (c) {
if (!c.i || c.removed) return;
pack.cells.culture[c.center] = c.i;
});
Cultures.expand();
drawCultures();
pack.burgs.forEach(b => (b.culture = pack.cells.culture[b.cell]));
refreshCulturesEditor();
document.querySelector("input.cultureExpan").focus(); // to not trigger hotkeys
}
function enterCultureManualAssignent() {

View file

@ -3,10 +3,10 @@ addListeners();
export function open() {
closeDialogs("#religionsEditor, .stable");
if (!layerIsOn("toggleReligions")) toggleCultures();
if (!layerIsOn("toggleReligions")) toggleReligions();
if (layerIsOn("toggleStates")) toggleStates();
if (layerIsOn("toggleBiomes")) toggleBiomes();
if (layerIsOn("toggleCultures")) toggleReligions();
if (layerIsOn("toggleCultures")) toggleCultures();
if (layerIsOn("toggleProvinces")) toggleProvinces();
refreshReligionsEditor();
@ -23,13 +23,15 @@ export function open() {
function insertEditorHtml() {
const editorHtml = /* html */ `<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 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 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 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 id="religionsBody" class="table" data-type="absolute"></div>
@ -88,6 +90,11 @@ function insertEditorHtml() {
</div>
<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="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>`;
@ -109,6 +116,7 @@ function addListeners() {
byId("religionsManuallyCancel").on("click", () => exitReligionsManualAssignment());
byId("religionsAdd").on("click", enterAddReligionMode);
byId("religionsExport").on("click", downloadReligionsCsv);
byId("religionsRecalculate").on("click", () => recalculateReligions(true));
}
function refreshReligionsEditor() {
@ -166,9 +174,10 @@ function religionsEditorAddLines() {
data-type=""
data-form=""
data-deity=""
data-expansion=""
data-expansionism=""
>
<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"
value="${r.name}" autocorrect="off" spellcheck="false" />
<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>
<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>
<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>
<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>`;
continue;
}
@ -195,6 +206,7 @@ function religionsEditorAddLines() {
data-type="${r.type}"
data-form="${r.form}"
data-deity="${r.deity || ""}"
data-expansion="${r.expansion}"
data-expansionism="${r.expansionism}"
>
<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"
value="${r.deity || ""}" autocorrect="off" spellcheck="false" />
<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>
<div data-tip="${populationTip}" class="religionPopulation hide pointer">${si(population)}</div>
<span
data-tip="Lock religion, will regenerate the origin folk and organized religion if they are not also locked"
class="icon-lock${r.lock ? '' : '-open'} hide"
></span>
<div data-tip="${populationTip}" class="religionPopulation hide pointer" style="width: 5em">${si(
population
)}</div>
${getExpansionColumns(r)}
<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>
</div>`;
}
@ -245,6 +257,8 @@ function religionsEditorAddLines() {
$body.querySelectorAll("div > input.religionDeity").forEach(el => el.on("input", religionChangeDeity));
$body.querySelectorAll("div > span.icon-arrows-cw").forEach(el => el.on("click", regenerateDeity));
$body.querySelectorAll("div > div.religionPopulation").forEach(el => el.on("click", changePopulation));
$body.querySelectorAll("div > select.religionExtent").forEach(el => el.on("change", religionChangeExtent));
$body.querySelectorAll("div > input.religionExpantion").forEach(el => el.on("change", religionChangeExpansionism));
$body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.on("click", religionRemovePrompt));
$body.querySelectorAll("div > span.icon-lock").forEach($el => $el.on("click", updateLockStatus));
$body.querySelectorAll("div > span.icon-lock-open").forEach($el => $el.on("click", updateLockStatus));
@ -253,6 +267,7 @@ function religionsEditorAddLines() {
$body.dataset.type = "absolute";
togglePercentageMode();
}
applySorting(religionsHeader);
$("#religionsEditor").dialog({width: fitContent()});
}
@ -264,6 +279,41 @@ function getTypeOptions(type) {
return options;
}
function getExpansionColumns(r) {
if (r.type === "Folk") {
const tip =
"Folk religions are not competitive and do not expand. Initially they cover all cells of their parent culture, but get ousted by organized religions when they expand";
return /* html */ `
<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 religionId = Number(event.id || event.target.dataset.id);
const $el = $body.querySelector(`div[data-id='${religionId}']`);
@ -272,20 +322,19 @@ const religionHighlightOn = debounce(event => {
if (!layerIsOn("toggleReligions")) return;
if (customization) return;
const animate = d3.transition().duration(1500).ease(d3.easeSinIn);
const animate = d3.transition().duration(2000).ease(d3.easeSinIn);
relig
.select("#religion" + religionId)
.raise()
.transition(animate)
.attr("stroke-width", 2.5)
.attr("stroke", "#c13119");
.attr("stroke", "#d0240f");
debug
.select("#religionsCenter" + religionId)
.raise()
.transition(animate)
.attr("r", 8)
.attr("stroke-width", 2)
.attr("stroke", "#c13119");
.attr("r", 3)
.attr("stroke", "#d0240f");
}, 200);
function religionHighlightOff(event) {
@ -301,8 +350,7 @@ function religionHighlightOff(event) {
debug
.select("#religionsCenter" + religionId)
.transition()
.attr("r", 4)
.attr("stroke-width", 1.2)
.attr("r", 2)
.attr("stroke", null);
}
@ -434,6 +482,20 @@ function changePopulation() {
}
}
function religionChangeExtent() {
const religion = +this.parentNode.dataset.id;
this.parentNode.dataset.expansion = this.value;
pack.religions[religion].expansion = this.value;
recalculateReligions();
}
function religionChangeExpansionism() {
const religion = +this.parentNode.dataset.id;
this.parentNode.dataset.expansionism = this.value;
pack.religions[religion].expansionism = +this.value;
recalculateReligions();
}
function religionRemovePrompt() {
if (customization) return;
@ -471,11 +533,14 @@ function drawReligionCenters() {
const religionCenters = debug
.append("g")
.attr("id", "religionCenters")
.attr("stroke-width", 1.2)
.attr("stroke-width", 0.8)
.attr("stroke", "#444444")
.style("cursor", "move");
const data = pack.religions.filter(r => r.i && r.center && r.cells && !r.removed);
let data = pack.religions.filter(r => r.i && r.center && !r.removed);
const showExtinct = $body.dataset.extinct === "show";
if (!showExtinct) data = data.filter(r => r.cells > 0);
religionCenters
.selectAll("circle")
.data(data)
@ -483,7 +548,7 @@ function drawReligionCenters() {
.append("circle")
.attr("id", d => "religionsCenter" + d.i)
.attr("data-id", d => d.i)
.attr("r", 4)
.attr("r", 2)
.attr("fill", d => d.color)
.attr("cx", d => pack.cells.p[d.center][0])
.attr("cy", d => pack.cells.p[d.center][1])
@ -499,15 +564,23 @@ function drawReligionCenters() {
}
function religionCenterDrag() {
const $el = d3.select(this);
const religionId = +this.dataset.id;
d3.event.on("drag", () => {
const tr = parseTransform(this.getAttribute("transform"));
const x0 = +tr[0] - d3.event.x;
const y0 = +tr[1] - d3.event.y;
function handleDrag() {
const {x, y} = d3.event;
$el.attr("cx", x).attr("cy", y);
this.setAttribute("transform", `translate(${x0 + x},${y0 + y})`);
const cell = findCell(x, y);
if (pack.cells.h[cell] < 20) return; // ignore dragging on water
pack.religions[religionId].center = cell;
});
recalculateReligions();
}
const dragDebounced = debounce(handleDrag, 50);
d3.event.on("drag", dragDebounced);
}
function toggleLegend() {
@ -578,13 +651,14 @@ async function showHierarchy() {
function toggleExtinct() {
$body.dataset.extinct = $body.dataset.extinct !== "show" ? "show" : "hide";
religionsEditorAddLines();
drawReligionCenters();
}
function enterReligionsManualAssignent() {
if (!layerIsOn("toggleReligions")) toggleReligions();
customization = 7;
relig.append("g").attr("id", "temp");
document.querySelectorAll("#religionsBottom > button").forEach(el => (el.style.display = "none"));
document.querySelectorAll("#religionsBottom > *").forEach(el => (el.style.display = "none"));
byId("religionsManuallyButtons").style.display = "inline-block";
debug.select("#religionCenters").style("display", "none");
@ -686,7 +760,7 @@ function exitReligionsManualAssignment(close) {
customization = 0;
relig.select("#temp").remove();
removeCircle();
document.querySelectorAll("#religionsBottom > button").forEach(el => (el.style.display = "inline-block"));
document.querySelectorAll("#religionsBottom > *").forEach(el => (el.style.display = "inline-block"));
byId("religionsManuallyButtons").style.display = "none";
byId("religionsEditor")
@ -740,15 +814,15 @@ function addReligion() {
function downloadReligionsCsv() {
const unit = getAreaUnit("2");
const headers = `Id,Name,Color,Type,Form,Supreme Deity,Area ${unit},Believers,Origins`;
const headers = `Id,Name,Color,Type,Form,Supreme Deity,Area ${unit},Believers,Origins,Potential,Expansionism`;
const lines = Array.from($body.querySelectorAll(":scope > div"));
const data = lines.map($line => {
const {id, name, color, type, form, deity, area, population} = $line.dataset;
const {id, name, color, type, form, deity, area, population, expansion, expansionism} = $line.dataset;
const deityText = '"' + deity + '"';
const {origins} = pack.religions[+id];
const originList = (origins || []).filter(origin => origin).map(origin => pack.religions[origin].name);
const originText = '"' + originList.join(", ") + '"';
return [id, name, color, type, form, deityText, area, population, originText].join(",");
return [id, name, color, type, form, deityText, area, population, originText, expansion, expansionism].join(",");
});
const csvData = [headers].concat(data).join("\n");
@ -773,3 +847,13 @@ function updateLockStatus() {
classList.toggle("icon-lock-open");
classList.toggle("icon-lock");
}
function recalculateReligions(must) {
if (!must && !religionsAutoChange.checked) return;
Religions.recalculate();
drawReligions();
refreshReligionsEditor();
drawReligionCenters();
}

View file

@ -163,8 +163,6 @@ function addListeners() {
const line = $element.parentNode;
const state = +line.dataset.id;
if (classList.contains("stateCapital")) stateChangeCapitalName(state, line, $element.value);
else if (classList.contains("cultureType")) stateChangeType(state, line, $element.value);
else if (classList.contains("statePower")) stateChangeExpansionism(state, line, $element.value);
});
$body.on("change", function (ev) {
@ -173,6 +171,8 @@ function addListeners() {
const line = $element.parentNode;
const state = +line.dataset.id;
if (classList.contains("stateCulture")) stateChangeCulture(state, line, $element.value);
else if (classList.contains("cultureType")) stateChangeType(state, line, $element.value);
else if (classList.contains("statePower")) stateChangeExpansionism(state, line, $element.value);
});
}
@ -883,7 +883,6 @@ function changeStatesGrowthRate() {
const growthRate = +this.value;
byId("statesNeutral").value = growthRate;
byId("statesNeutralNumber").value = growthRate;
statesNeutral = growthRate;
tip("Growth rate: " + growthRate);
recalculateStates(false);
}

View file

@ -1,44 +1,481 @@
const capitalize = text => text.charAt(0).toUpperCase() + text.slice(1);
const format = rawList =>
rawList
.replace(/(?:\r\n|\r|\n)/g, "")
.split(",")
.map(name => capitalize(name.trim()))
.sort();
export const supporters = format(`
Aaron Meyer,Ahmad Amerih,AstralJacks,aymeric,Billy Dean Goehring,Branndon Edwards,Chase Mayers,Curt Flood,cyninge,Dino Princip,
E.M. White,es,Fondue,Fritjof Olsson,Gatsu,Johan Fröberg,Jonathan Moore,Joseph Miranda,Kate,KC138,Luke Nelson,Markus Finster,Massimo Vella,Mikey,
Nathan Mitchell,Paavi1,Pat,Ryan Westcott,Sasquatch,Shawn Spencer,Sizz_TV,Timothée CALLET,UTG community,Vlad Tomash,Wil Sisney,William Merriott,
Xariun,Gun Metal Games,Scott Marner,Spencer Sherman,Valerii Matskevych,Alloyed Clavicle,Stewart Walsh,Ruthlyn Mollett (Javan),Benjamin Mair-Pratt,
Diagonath,Alexander Thomas,Ashley Wilson-Savoury,William Henry,Preston Brooks,JOSHUA QUALTIERI,Hilton Williams,Katharina Haase,Hisham Bedri,
Ian arless,Karnat,Bird,Kevin,Jessica Thomas,Steve Hyatt,Logicspren,Alfred García,Jonathan Killstring,John Ackley,Invad3r233,Norbert Žigmund,Jennifer,
PoliticsBuff,_gfx_,Maggie,Connor McMartin,Jared McDaris,BlastWind,Franc Casanova Ferrer,Dead & Devil,Michael Carmody,Valerie Elise,naikibens220,
Jordon Phillips,William Pucs,The Dungeon Masters,Brady R Rathbun,J,Shadow,Matthew Tiffany,Huw Williams,Joseph Hamilton,FlippantFeline,Tamashi Toh,
kms,Stephen Herron,MidnightMoon,Whakomatic x,Barished,Aaron bateson,Brice Moss,Diklyquill,PatronUser,Michael Greiner,Steven Bennett,Jacob Harrington,
Miguel C.,Reya C.,Giant Monster Games,Noirbard,Brian Drennen,Ben Craigie,Alex Smolin,Endwords,Joshua E Goodwin,SirTobit ,Allen S. Rout,Allen Bull Bear,
Pippa Mitchell,R K,G0atfather,Ryan Lege,Caner Oleas Pekgönenç,Bradley Edwards,Tertiary ,Austin Miller,Jesse Holmes,Jan Dvořák,Marten F,Erin D. Smale,
Maxwell Hill,Drunken_Legends,rob bee,Jesse Holmes,YYako,Detocroix,Anoplexian,Hannah,Paul,Sandra Krohn,Lucid,Richard Keating,Allen Varney,Rick Falkvinge,
Seth Fusion,Adam Butler,Gus,StroboWolf,Sadie Blackthorne,Zewen Senpai,Dell McKnight,Oneiris,Darinius Dragonclaw Studios,Christopher Whitney,Rhodes HvZ,
Jeppe Skov Jensen,María Martín López,Martin Seeger,Annie Rishor,Aram Sabatés,MadNomadMedia,Eric Foley,Vito Martono,James H. Anthony,Kevin Cossutta,
Thirty-OneR,ThatGuyGW,Dee Chiu,MontyBoosh,Achillain,Jaden,SashaTK,Steve Johnson,Pierrick Bertrand,Jared Kennedy,Dylan Devenny,Kyle Robertson,
Andrew Rostaing,Daniel Gill,Char,Jack,Barna Csíkos,Ian Rousseau,Nicholas Grabstas,Tom Van Orden jr,Bryan Brake,Akylos,Riley Seaman,MaxOliver,Evan-DiLeo,
Alex Debus,Joshua Vaught,Kyle S,Eric Moore,Dean Dunakin,Uniquenameosaurus,WarWizardGames,Chance Mena,Jan Ka,Miguel Alejandro,Dalton Clark,Simon Drapeau,
Radovan Zapletal,Jmmat6,Justa Badge,Blargh Blarghmoomoo,Vanessa Anjos,Grant A. Murray,Akirsop,Rikard Wolff,Jake Fish,teco 47,Antiroo,Jakob Siegel,
Guilherme Aguiar,Jarno Hallikainen,Justin Mcclain,Kristin Chernoff,Rowland Kingman,Esther Busch,Grayson McClead,Austin,Hakon the Viking,Chad Riley,
Cooper Counts,Patrick Jones,Clonetone,PlayByMail.Net,Brad Wardell,Lance Saba,Egoensis,Brea Richards,Tiber,Chris Bloom,Maxim Lowe,Aquelion,
Page One Project,Spencer Morris,Paul Ingram,Dust Bunny,Adrian Wright,Eric Alexander Cartaya,GameNight,Thomas Mortensen Hansen,Zklaus,Drinarius,
Ed Wright,Lon Varnadore,Crys Cain,Heaven N Lee,Jeffrey Henning,Lazer Elf,Jordan Bellah,Alex Beard,Kass Frisson,Petro Lombaard,Emanuel Pietri,Rox,
PinkEvil,Gavin Madrigal,Martin Lorber,Prince of Morgoth,Jaryd Armstrong,Andrew Pirkola,ThyHolyDevil,Gary Smith,Tyshaun Wise,Ethan Cook,Jon Stroman,
Nobody679,良义 ,Chris Gray,Phoenix Boatwright,Mackenzie,Milo Cohen,Jason Matthew Wuerfel,Rasmus Legêne,Andrew Hines,Wexxler,Espen Sæverud,Binks,
Dominick Ormsby,Linn Browning,Václav Švec,Alan Buehne,George J.Lekkas,Alexandre Boivin,Tommy Mayfield,Skylar Mangum-Turner,Karen Blythe,Stefan Gugerel,
Mike Conley,Xavier privé,Hope You're Well,Mark Sprietsma,Robert Landry,Nick Mowry,steve hall,Markell,Josh Wren,Neutrix,BLRageQuit,Rocky,
Dario Spadavecchia,Bas Kroot,John Patrick Callahan Jr,Alexandra Vesey,D,Exp1nt,james,Braxton Istace,w,Rurikid,AntiBlock,Redsauz,BigE0021,
Jonathan Williams,ojacid .,Brian Wilson,A Patreon of the Ahts,Shubham Jakhotiya,www15o,Jan Bundesmann,Angelique Badger,Joshua Xiong,Moist mongol,
Frank Fewkes,jason baldrick,Game Master Pro,Andrew Kircher,Preston Mitchell,Chris Kohut,Emarandzeb,Trentin Bergeron,Damon Gallaty,Pleaseworkforonce,
Jordan,William Markus,Sidr Dim,Alexander Whittaker,The Next Level,Patrick Valverde,Markus Peham,Daniel Cooper,the Beagles of Neorbus,Marley Moule,
Maximilian Schielke,Johnathan Xavier Hutchinson,Ele,Rita,Randy Ross,John Wick,RedSpaz,cameron cannon,Ian Grau-Fay,Kyle Barrett,Charlotte Wiland,
David Kaul,E. Jason Davis,Cyberate,Atenfox,Sea Wolf,Holly Loveless,Roekai,Alden Z,angel carrillo,Sam Spoerle,S A Rudy,Bird Law Expert,Mira Cyr,
Aaron Blair,Neyimadd,RLKZ1022,DerWolf,Kenji Yamada,Zion,Robert Rinne,Actual_Dio,Kyarou
`);
export const supporters = `ken burgan
Sera's Nafitlaan
Richard Rogers
Hylobate
Colin deSousa
Aurelia De La Silla
Maciej Kontny
Ricky L Cain
Iggyflare
Garrett Renner
Michael Harris
Joshua Maly
Nigel Guest
Theo Hodges
BERTHEAS Frédéric
lilMoni
Δημήτρης Μάρκογιαννακης
Lee S.
Chris Dibbs
jarrad tait
Jacen Solo
Hannes Rotestam
Preston Hicks
Лонгин
Will Fink
ControlFreq
IllAngel
John Giardina
Thiago Prado
Zhang Dijon
NoBurny
thibault tersinet
scarletsky
Nich Smith
Omegus
Karl Abrahamsson
Sara Fernandes
peetey897
Cooper Janse
G F
Glen Aultman-Bettridge
Nathan Rogers
Benjamin Mock
CadmiumMan
Kirk Edwards
Leigh G
Thom Colyer
Frederik
C pstj
Zachary Pecora
Trevor D'Arcey
Ryan Gauvin
Shawn Moore
Jim Channon
Kyarou
Actual_Dio
Jim B Johnson
Robert Rinne
Zion
Kenji Yamada
DerWolf
RLKZ1022
Neyimadd
Aaron Blair
Mira Cyr
Bird Law Expert
S A Rudy
Sam Spoerle
angel carrillo
Alden Z
Holly Loveless
Sea Wolf
Atenfox
Cyberate
E. Jason Davis
Caro Lyns
David Kaul
Charlotte Wiland
Kyle Barrett
Ian Grau-Fay
cameron cannon
RedSpaz
John Wick
Randy Ross
Rita
Ele
Johnathan Xavier Hutchinson
Andrew Stein
Ghettov Milan
Malke
TameMoon
Daniel Cooper
Markus Peham
The Next Level
Alexander Whittaker
Sidr Dim
William Markus
Jordan
Pleaseworkforonce
Damon Gallaty
Trentin Bergeron
Emarandzeb
Laulajatar
Dale McBane
Chris Kohut
Preston Mitchell
Andrew Kircher
Frank Fewkes
Moist mongol
Joshua Xiong
Jan Bundesmann
www15o
Game Master Pro
jason baldrick
Exp1nt
w
Shubham Jakhotiya
Braxton Istace
LesterThePossum
Rurikid
ojacid .
james
A Patreon of the Ahts
BigE0021
Angelique Badger
Jonathan Williams
AntiBlock
Redsauz
Florian Kelber
John Patrick Callahan Jr
Alexandra Vesey
Bas Kroot
Dzmitry Malyshau
PedanticSteve
Josh Wren
BLRageQuit
Dario Spadavecchia
Neutrix
Markell
Rocky
Robert Landry
Skylar Mangum-Turner
Nick Mowry
Anjen Pai
Hope You're Well
Alexandre Boivin
Racussa
Mike Conley
Karen Blythe
Mark Sprietsma
Xavier privé
Tommy Mayfield
Václav Švec
Binks
Mackenzie
Linn Browning
Writer's Consultant Page by George J.Lekkas
Andrew Hines
Wexxler
Jason Matthew Wuerfel
Milo Cohen
Alan Buehne
Dominick Ormsby
Espen Sæverud
Rasmus Legêne
rbbalderama
Nobody679
Prince of Morgoth
Jaryd Armstrong
Gary Smith
ThyHolyDevil
良义
Andrew Pirkola
Dig
Chris Gray
Tyshaun Wise
Phoenix
Ethan Cook
Jordan Bellah
Petro Lombaard
Kass Frisson
Lazer Elf
Gavin Madrigal
Rox
PinkEvil
Martin Lorber
Emanuel Pietri
Alex Beard
Jeffrey Henning
Eric Alexander Cartaya
Dust Bunny
GameNight
Beingus
Crys Cain
Lon Varnadore
Thomas Mortensen Hansen
Drinarius
Ed Wright
Adrian Wright
Zklaus
Chris Bloom
PlayByMail.Net
Maxim Lowe
Aquelion
Tiber
Daydream1013
Page One Project
Clonetone
Egoensis
Brad Wardell
Heaven N Lee
BarnabyJones
Paul Ingram
Lance Saba
Chad Riley
Austin
Rowland Kingman
Decimus Vitalis
Grayson McClead
Battleturtle1
Kristin Chernoff
Justin Mcclain
Patrick Jones
Esther Busch
Chance Mena
JimmyTheBob
Antiroo
Dalton Clark
Guilherme Aguiar
Simon Drapeau
Akirsop
Radovan Zapletal
Vanessa Anjos
Rikard Wolff
Justa Badge
teco 47
Jake
Miguel Alejandro
Blargh Blarghmoomoo
Jakob Siegel
Grant A. Murray
Jarno Hallikainen
Jan Ka
Joshua Vaught
MaxOliver
WarWizardGames
Evan-DiLeo
Eric Moore
Kyle S
Alex Debus
Uniquenameosaurus
Dean Dunakin
Jack
Bryan Brake
McNeil Atticus Inksmudge
Char
Tom Van Orden jr
Kendall Patterson
Akylos
Barna Csíkos
Nicholas Grabstas
OldFarkas
Riley Seaman
Daniel Gill
Kyle Robertson
Natasha Taylor
Pierrick Bertrand
Jared.K
Dylan Devenny
logic_error
SashaTK
Steve Johnson
MontyBoosh
Achillain
Jaden
Vito Martono
Thirty-OneR
Eric Foley
ThatGuyGW
Dee Chiu
James H. Anthony
Kevin Cossutta
MadNomadMedia
Darinius Dragonclaw Studios
Tsahyla (Triston Lightyear)
Christopher Whitney
María Martín López
Annie Rishor
Aram Sabatés
Jeppe Skov Jensen
Martin Seeger
Oneiris (Oni)
EternalDeiwos
Richard Keating
StroboWolf
Rick Falkvinge
Zewen Senpai
Adam Butler
Kassidy
Sadie Blackthorne
ErrorForever
Seth Fusion
Gus
Paul
Lucid
Allen Varney
Hannah May
Sankroh
Eliot Miller
Detocroix
Meg Ziegler
rob bee
Anoplexian
Marten F
Erin D. Smale
Johnpaul Morrow
Roekai
Drunken_Legends
Jesse Holmes
Maxwell Hill
Jan Dvořák
SirTobit
G0atfather
Allen S. Rout
Pippa Mitchell
Austin Miller
Caner Oleas Pekgönenç
Alison Bull Bear
Bradley Edwards
Tertiary
Daniel
Joshua E Goodwin
Shaun Alexander
Ryan Lege
Myrrhlin
Jesper Cockx
Noirbard
Dice
Brian Drennen
Giant Monster Games
Reya C.
Krk
Endwords
Jacob Harrington
RK
Michael Greiner
Steven Bennett
Brice Moss
Whakomatic x
Stephen Herron
kosmobius
ZizRenanim
Barished
Maur Razimtheth
Aaron bateson
Diklyquill
Shawn Taylor
Brady R Rathbun
FlippantFeline
Shadow
J
Tamashi Toh
Huw Williams
Graves
ShadeByTheSea
The Dungeon Masters
Valerie Elise
Empi3
William Pucs
Michael Carmody
Marco Veldman
naikibens220
Jordon Phillips
_gfx_
F. Casanova
Jared McDaris
BlastWind
Taldonix
Connor McMartin
Nexoness
Guy
Maggie
AdvancedAzrielAngel
Alfred García
Norbert Žigmund
Jennifer
Titanium Tomes
John Ackley
Invad3r233
Jonathan Killstring
Jessica Thomas
Nikita Kondratjuks
Steve Hyatt
PoliticsBuff
Ian arless
Karnat
Hilton Williams
Kevin
Katharina Haase
Hisham Bedri
Bird
JOSHUA QUALTIERI
Preston Brooks
Troy Schuler
DerGeisterbär
L. V. Werneck
Marcus Hellyrr
yami
Daniel Eric Crosby
Augusto Chiarle
Doug Churchman
David Roza
Alexander Thomas
Ashley Wilson-Savoury
Nathan L Myers
Theresa Walsh
JP Roberts III
William Henry
OldbeanOldboy
Javasharp
Diagonath
Gun Metal Games
Scott Marner
Alloyed Clavicle
Valerii Matskevych
Spencer Sherman
Nolan Moore
James Schellenger
Pat
Dino Princip
Shawn Spencer
Timothée CALLET
KC138
Nylian
Kate
Markus Finster
CanadianGold
AstralJacks
Keith Marshall
Scott Davis
Joseph Miranda
Shaptarshi Joarder
Branndon
EP
Johan Fröberg
Sasquatch
Chase Mayers
Sizz_TV
Ryan Westcott
Nathan Mitchell
Curt Flood
Mikey
E.M. White
Billy
Vlad Tomash
Xariun
Luke Nelson
W Maxwell Cassity-Guilliom
Marty H
Aaron Meyer
Max Amillios
chris
cyninge
Omegavoid
Fritjof Olsson
Crazypedia
Duncan Thomson
William Merriott
Gold Tamarin
Lhoris
Jonathan
Jon
Massimo Vella
Feuver
aymeric
Eric Schumann
Rei
Fondue
Paavi1
Wil Sisney
David Patterson`;

View file

@ -304,15 +304,34 @@ window.Religions = (function () {
Heresy: {Heresy: 1}
};
const methods = {
"Random + type": 3,
"Random + ism": 1,
"Supreme + ism": 5,
"Faith of + Supreme": 5,
"Place + ism": 1,
"Culture + ism": 2,
"Place + ian + type": 6,
"Culture + type": 4
const namingMethods = {
Folk: {
"Culture + type": 1
},
Organized: {
"Random + type": 3,
"Random + ism": 1,
"Supreme + ism": 5,
"Faith of + Supreme": 5,
"Place + ism": 1,
"Culture + ism": 2,
"Place + ian + type": 6,
"Culture + type": 4
},
Cult: {
"Burg + ian + type": 2,
"Random + ian + type": 1,
"Type + of the + meaning": 2
},
Heresy: {
"Burg + ian + type": 3,
"Random + ism": 3,
"Random + ian + type": 2,
"Type + of the + meaning": 1
}
};
const types = {
@ -342,381 +361,416 @@ window.Religions = (function () {
}
};
const generate = function () {
const expansionismMap = {
Folk: () => 0,
Organized: () => gauss(5, 3, 0, 10, 1),
Cult: () => gauss(0.5, 0.5, 0, 5, 1),
Heresy: () => gauss(1, 0.5, 0, 5, 1)
};
function generate() {
TIME && console.time("generateReligions");
const {cells, states, cultures} = pack;
const lockedReligions = pack.religions?.filter(r => r.i && r.lock && !r.removed) || [];
const religionIds = new Uint16Array(cells.culture); // cell religion; initially based on culture
const religions = [];
const folkReligions = generateFolkReligions();
const organizedReligions = generateOrganizedReligions(+religionsInput.value, lockedReligions);
// add folk religions
pack.cultures.forEach(c => {
const newId = c.i;
if (!newId) return religions.push({i: 0, name: "No religion"});
const namedReligions = specifyReligions([...folkReligions, ...organizedReligions]);
const indexedReligions = combineReligions(namedReligions, lockedReligions);
const religionIds = expandReligions(indexedReligions);
const religions = defineOrigins(religionIds, indexedReligions);
if (c.removed) {
religions.push({
i: c.i,
name: "Extinct religion for " + c.name,
color: getMixedColor(c.color, 0.1, 0),
removed: true
});
return;
}
if (pack.religions) {
const lockedFolkReligion = pack.religions.find(
r => r.culture === c.i && !r.removed && r.lock && r.type === "Folk"
);
if (lockedFolkReligion) {
for (const i of cells.i) {
if (cells.religion[i] === lockedFolkReligion.i) religionIds[i] = newId;
}
lockedFolkReligion.i = newId;
religions.push(lockedFolkReligion);
return;
}
}
const form = rw(forms.Folk);
const name = c.name + " " + rw(types[form]);
const deity = form === "Animism" ? null : getDeityName(c.i);
const color = getMixedColor(c.color, 0.1, 0); // `url(#hatch${rand(8,13)})`;
religions.push({
i: newId,
name,
color,
culture: newId,
type: "Folk",
form,
deity,
center: c.center,
origins: [0]
});
});
if (religionsInput.value == 0 || pack.cultures.length < 2)
return religions.filter(r => r.i).forEach(r => (r.code = abbreviate(r.name)));
const burgs = pack.burgs.filter(b => b.i && !b.removed);
const sorted =
burgs.length > +religionsInput.value
? burgs.sort((a, b) => b.population - a.population).map(b => b.cell)
: cells.i.filter(i => cells.s[i] > 2).sort((a, b) => cells.s[b] - cells.s[a]);
const religionsTree = d3.quadtree();
const spacing = (graphWidth + graphHeight) / 6 / religionsInput.value; // base min distance between towns
const cultsCount = Math.floor((rand(10, 40) / 100) * religionsInput.value);
const count = +religionsInput.value - cultsCount + religions.length;
function getReligionsInRadius({x, y, r, max}) {
if (max === 0) return [0];
const cellsInRadius = findAll(x, y, r);
const religions = unique(cellsInRadius.map(i => religionIds[i]).filter(r => r));
return religions.length ? religions.slice(0, max) : [0];
}
// restore locked non-folk religions
if (pack.religions) {
const lockedNonFolkReligions = pack.religions.filter(r => r.lock && !r.removed && r.type !== "Folk");
for (const religion of lockedNonFolkReligions) {
const newId = religions.length;
for (const i of cells.i) {
if (cells.religion[i] === religion.i) religionIds[i] = newId;
}
religion.i = newId;
religion.origins = religion.origins.filter(origin => origin < newId);
religionsTree.add(cells.p[religion.center]);
religions.push(religion);
}
}
// generate organized religions
for (let i = 0; religions.length < count && i < 1000; i++) {
let center = sorted[biased(0, sorted.length - 1, 5)]; // religion center
const form = rw(forms.Organized);
const state = cells.state[center];
const culture = cells.culture[center];
const deity = form === "Non-theism" ? null : getDeityName(culture);
let [name, expansion] = getReligionName(form, deity, center);
if (expansion === "state" && !state) expansion = "global";
if (expansion === "culture" && !culture) expansion = "global";
if (expansion === "state" && Math.random() > 0.5) center = states[state].center;
if (expansion === "culture" && Math.random() > 0.5) center = cultures[culture].center;
if (!cells.burg[center] && cells.c[center].some(c => cells.burg[c]))
center = cells.c[center].find(c => cells.burg[c]);
const [x, y] = cells.p[center];
const s = spacing * gauss(1, 0.3, 0.2, 2, 2); // randomize to make the placement not uniform
if (religionsTree.find(x, y, s) !== undefined) continue; // to close to existing religion
// add "Old" to name of the folk religion on this culture
const isFolkBased = expansion === "culture" || P(0.5);
const folk = isFolkBased && religions.find(r => r.culture === culture && r.type === "Folk");
if (folk && expansion === "culture" && folk.name.slice(0, 3) !== "Old") folk.name = "Old " + folk.name;
const origins = folk ? [folk.i] : getReligionsInRadius({x, y, r: 150 / count, max: 2});
const expansionism = rand(3, 8);
const baseColor = religions[culture]?.color || states[state]?.color || getRandomColor();
const color = getMixedColor(baseColor, 0.3, 0);
religions.push({
i: religions.length,
name,
color,
culture,
type: "Organized",
form,
deity,
expansion,
expansionism,
center,
origins
});
religionsTree.add([x, y]);
}
// generate cults
for (let i = 0; religions.length < count + cultsCount && i < 1000; i++) {
const form = rw(forms.Cult);
let center = sorted[biased(0, sorted.length - 1, 1)]; // religion center
if (!cells.burg[center] && cells.c[center].some(c => cells.burg[c]))
center = cells.c[center].find(c => cells.burg[c]);
const [x, y] = cells.p[center];
const s = spacing * gauss(2, 0.3, 1, 3, 2); // randomize to make the placement not uniform
if (religionsTree.find(x, y, s) !== undefined) continue; // to close to existing religion
const culture = cells.culture[center];
const origins = getReligionsInRadius({x, y, r: 300 / count, max: rand(0, 4)});
const deity = getDeityName(culture);
const name = getCultName(form, center);
const expansionism = gauss(1.1, 0.5, 0, 5);
const color = getMixedColor(cultures[culture].color, 0.5, 0); // "url(#hatch7)";
religions.push({
i: religions.length,
name,
color,
culture,
type: "Cult",
form,
deity,
expansion: "global",
expansionism,
center,
origins
});
religionsTree.add([x, y]);
}
expandReligions();
// generate heresies
religions
.filter(r => r.type === "Organized")
.forEach(r => {
if (r.expansionism < 3) return;
const count = gauss(0, 1, 0, 3);
for (let i = 0; i < count; i++) {
let center = ra(cells.i.filter(i => religionIds[i] === r.i && cells.c[i].some(c => religionIds[c] !== r.i)));
if (!center) continue;
if (!cells.burg[center] && cells.c[center].some(c => cells.burg[c]))
center = cells.c[center].find(c => cells.burg[c]);
const [x, y] = cells.p[center];
if (religionsTree.find(x, y, spacing / 10) !== undefined) continue; // to close to other
const culture = cells.culture[center];
const name = getCultName("Heresy", center);
const expansionism = gauss(1.2, 0.5, 0, 5);
const color = getMixedColor(r.color, 0.4, 0.2); // "url(#hatch6)";
religions.push({
i: religions.length,
name,
color,
culture,
type: "Heresy",
form: r.form,
deity: r.deity,
expansion: "global",
expansionism,
center,
origins: [r.i]
});
religionsTree.add([x, y]);
}
});
expandHeresies();
pack.religions = religions;
pack.cells.religion = religionIds;
checkCenters();
cells.religion = religionIds;
pack.religions = religions;
TIME && console.timeEnd("generateReligions");
}
// growth algorithm to assign cells to religions
function expandReligions() {
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const cost = [];
function generateFolkReligions() {
return pack.cultures
.filter(c => c.i && !c.removed)
.map(culture => ({type: "Folk", form: rw(forms.Folk), culture: culture.i, center: culture.center}));
}
religions
.filter(r => !r.lock && (r.type === "Organized" || r.type === "Cult"))
.forEach(r => {
religionIds[r.center] = r.i;
queue.queue({e: r.center, p: 0, r: r.i, s: cells.state[r.center], c: r.culture});
cost[r.center] = 1;
});
function generateOrganizedReligions(desiredReligionNumber, lockedReligions) {
const cells = pack.cells;
const lockedReligionCount = lockedReligions.filter(({type}) => type !== "Folk").length || 0;
const requiredReligionsNumber = desiredReligionNumber - lockedReligionCount;
if (requiredReligionsNumber < 1) return [];
const neutral = (cells.i.length / 5000) * 200 * gauss(1, 0.3, 0.2, 2, 2) * neutralInput.value; // limit cost for organized religions growth
const popCost = d3.max(cells.pop) / 3; // enougth population to spered religion without penalty
const candidateCells = getCandidateCells();
const religionCores = placeReligions();
while (queue.length) {
const {e, p, r, c, s} = queue.dequeue();
const expansion = religions[r].expansion;
const cultsCount = Math.floor((rand(1, 4) / 10) * religionCores.length); // 10-40%
const heresiesCount = Math.floor((rand(0, 3) / 10) * religionCores.length); // 0-30%
const organizedCount = religionCores.length - cultsCount - heresiesCount;
cells.c[e].forEach(nextCell => {
if (expansion === "culture" && c !== cells.culture[nextCell]) return;
if (expansion === "state" && s !== cells.state[nextCell]) return;
if (religions[religionIds[nextCell]]?.lock) return;
const getType = index => {
if (index < organizedCount) return "Organized";
if (index < organizedCount + cultsCount) return "Cult";
return "Heresy";
};
const cultureCost = c !== cells.culture[nextCell] ? 10 : 0;
const stateCost = s !== cells.state[nextCell] ? 10 : 0;
const biomeCost = cells.road[nextCell] ? 1 : biomesData.cost[cells.biome[nextCell]];
const populationCost = Math.max(rn(popCost - cells.pop[nextCell]), 0);
const heightCost = Math.max(cells.h[nextCell], 20) - 20;
const waterCost = cells.h[nextCell] < 20 ? (cells.road[nextCell] ? 50 : 1000) : 0;
const totalCost =
p +
(cultureCost + stateCost + biomeCost + populationCost + heightCost + waterCost) / religions[r].expansionism;
if (totalCost > neutral) return;
return religionCores.map((cellId, index) => {
const type = getType(index);
const form = rw(forms[type]);
const cultureId = cells.culture[cellId];
if (!cost[nextCell] || totalCost < cost[nextCell]) {
if (cells.h[nextCell] >= 20 && cells.culture[nextCell]) religionIds[nextCell] = r; // assign religion to cell
cost[nextCell] = totalCost;
queue.queue({e: nextCell, p: totalCost, r, c, s});
}
});
return {type, form, culture: cultureId, center: cellId};
});
function placeReligions() {
const religionCells = [];
const religionsTree = d3.quadtree();
// pre-populate with locked centers
lockedReligions.forEach(({center}) => religionsTree.add(cells.p[center]));
// min distance between religion inceptions
const spacing = (graphWidth + graphHeight) / 2 / desiredReligionNumber;
for (const cellId of candidateCells) {
const [x, y] = cells.p[cellId];
if (religionsTree.find(x, y, spacing) === undefined) {
religionCells.push(cellId);
religionsTree.add([x, y]);
if (religionCells.length === requiredReligionsNumber) return religionCells;
}
}
WARN && console.warn(`Placed only ${religionCells.length} of ${requiredReligionsNumber} religions`);
return religionCells;
}
function getCandidateCells() {
const validBurgs = pack.burgs.filter(b => b.i && !b.removed);
if (validBurgs.length >= requiredReligionsNumber)
return validBurgs.sort((a, b) => b.population - a.population).map(burg => burg.cell);
return cells.i.filter(i => cells.s[i] > 2).sort((a, b) => cells.s[b] - cells.s[a]);
}
}
function specifyReligions(newReligions) {
const {cells, cultures} = pack;
const rawReligions = newReligions.map(({type, form, culture: cultureId, center}) => {
const supreme = getDeityName(cultureId);
const deity = form === "Non-theism" || form === "Animism" ? null : supreme;
const stateId = cells.state[center];
let [name, expansion] = generateReligionName(type, form, supreme, center);
if (expansion === "state" && !stateId) expansion = "global";
const expansionism = expansionismMap[type]();
const color = getReligionColor(cultures[cultureId], type);
return {name, type, form, culture: cultureId, center, deity, expansion, expansionism, color};
});
return rawReligions;
function getReligionColor(culture, type) {
if (!culture.i) return getRandomColor();
if (type === "Folk") return culture.color;
if (type === "Heresy") return getMixedColor(culture.color, 0.35, 0.2);
if (type === "Cult") return getMixedColor(culture.color, 0.5, 0);
return getMixedColor(culture.color, 0.25, 0.4);
}
}
// indexes, conditionally renames, and abbreviates religions
function combineReligions(namedReligions, lockedReligions) {
const indexedReligions = [{name: "No religion", i: 0}];
const {lockedReligionQueue, highestLockedIndex, codes, numberLockedFolk} = parseLockedReligions();
const maxIndex = Math.max(
highestLockedIndex,
namedReligions.length + lockedReligions.length + 1 - numberLockedFolk
);
for (let index = 1, progress = 0; index < maxIndex; index = indexedReligions.length) {
// place locked religion back at its old index
if (index === lockedReligionQueue[0]?.i) {
const nextReligion = lockedReligionQueue.shift();
indexedReligions.push(nextReligion);
continue;
}
// slot the new religions
if (progress < namedReligions.length) {
const nextReligion = namedReligions[progress];
progress++;
if (
nextReligion.type === "Folk" &&
lockedReligions.some(({type, culture}) => type === "Folk" && culture === nextReligion.culture)
)
continue; // when there is a locked Folk religion for this culture discard duplicate
const newName = renameOld(nextReligion);
const code = abbreviate(newName, codes);
codes.push(code);
indexedReligions.push({...nextReligion, i: index, name: newName, code});
continue;
}
indexedReligions.push({i: index, type: "Folk", culture: 0, name: "Removed religion", removed: true});
}
return indexedReligions;
function parseLockedReligions() {
// copy and sort the locked religions list
const lockedReligionQueue = lockedReligions
.map(religion => {
// and filter their origins to locked religions
let newOrigin = religion.origins.filter(n => lockedReligions.some(({i: index}) => index === n));
if (newOrigin === []) newOrigin = [0];
return {...religion, origins: newOrigin};
})
.sort((a, b) => a.i - b.i);
const highestLockedIndex = Math.max(...lockedReligions.map(r => r.i));
const codes = lockedReligions.length > 0 ? lockedReligions.map(r => r.code) : [];
const numberLockedFolk = lockedReligions.filter(({type}) => type === "Folk").length;
return {lockedReligionQueue, highestLockedIndex, codes, numberLockedFolk};
}
// prepend 'Old' to names of folk religions which have organized competitors
function renameOld({name, type, culture: cultureId}) {
if (type !== "Folk") return name;
const haveOrganized =
namedReligions.some(
({type, culture, expansion}) => culture === cultureId && type === "Organized" && expansion === "culture"
) ||
lockedReligions.some(
({type, culture, expansion}) => culture === cultureId && type === "Organized" && expansion === "culture"
);
if (haveOrganized && name.slice(0, 3) !== "Old") return `Old ${name}`;
return name;
}
}
// finally generate and stores origins trees
function defineOrigins(religionIds, indexedReligions) {
const religionOriginsParamsMap = {
Organized: {clusterSize: 100, maxReligions: 2},
Cult: {clusterSize: 50, maxReligions: 3},
Heresy: {clusterSize: 50, maxReligions: 4}
};
const origins = indexedReligions.map(({i, type, culture: cultureId, expansion, center}) => {
if (i === 0) return null; // no religion
if (type === "Folk") return [0]; // folk religions originate from its parent culture only
const folkReligion = indexedReligions.find(({culture, type}) => type === "Folk" && culture === cultureId);
const isFolkBased = folkReligion && cultureId && expansion === "culture" && each(2)(center);
if (isFolkBased) return [folkReligion.i];
const {clusterSize, maxReligions} = religionOriginsParamsMap[type];
const fallbackOrigin = folkReligion?.i || 0;
return getReligionsInRadius(pack.cells.c, center, religionIds, i, clusterSize, maxReligions, fallbackOrigin);
});
return indexedReligions.map((religion, index) => ({...religion, origins: origins[index]}));
}
function getReligionsInRadius(neighbors, center, religionIds, religionId, clusterSize, maxReligions, fallbackOrigin) {
const foundReligions = new Set();
const queue = [center];
const checked = {};
for (let size = 0; queue.length && size < clusterSize; size++) {
const cellId = queue.shift();
checked[cellId] = true;
for (const neibId of neighbors[cellId]) {
if (checked[neibId]) continue;
checked[neibId] = true;
const neibReligion = religionIds[neibId];
if (neibReligion && neibReligion < religionId) foundReligions.add(neibReligion);
if (foundReligions.size >= maxReligions) return [...foundReligions];
queue.push(neibId);
}
}
// growth algorithm to assign cells to heresies
function expandHeresies() {
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const cost = [];
return foundReligions.size ? [...foundReligions] : [fallbackOrigin];
}
religions
.filter(r => !r.lock && r.type === "Heresy")
.forEach(r => {
const b = religionIds[r.center]; // "base" religion id
religionIds[r.center] = r.i; // heresy id
queue.queue({e: r.center, p: 0, r: r.i, b});
cost[r.center] = 1;
});
// growth algorithm to assign cells to religions
function expandReligions(religions) {
const cells = pack.cells;
const religionIds = spreadFolkReligions(religions);
const neutral = (cells.i.length / 5000) * 500 * neutralInput.value; // limit cost for heresies growth
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const cost = [];
while (queue.length) {
const {e, p, r, b} = queue.dequeue();
const maxExpansionCost = (cells.i.length / 20) * neutralInput.value; // limit cost for organized religions growth
cells.c[e].forEach(nextCell => {
if (religions[religionIds[nextCell]]?.lock) return;
const religionCost = religionIds[nextCell] === b ? 0 : 2000;
const biomeCost = cells.road[nextCell] ? 0 : biomesData.cost[cells.biome[nextCell]];
const heightCost = Math.max(cells.h[nextCell], 20) - 20;
const waterCost = cells.h[nextCell] < 20 ? (cells.road[nextCell] ? 50 : 1000) : 0;
const totalCost =
p + (religionCost + biomeCost + heightCost + waterCost) / Math.max(religions[r].expansionism, 0.1);
const biomePassageCost = cellId => biomesData.cost[cells.biome[cellId]];
if (totalCost > neutral) return;
religions
.filter(r => r.i && !r.lock && r.type !== "Folk" && !r.removed)
.forEach(r => {
religionIds[r.center] = r.i;
queue.queue({e: r.center, p: 0, r: r.i, s: cells.state[r.center]});
cost[r.center] = 1;
});
if (!cost[nextCell] || totalCost < cost[nextCell]) {
if (cells.h[nextCell] >= 20 && cells.culture[nextCell]) religionIds[nextCell] = r; // assign religion to cell
cost[nextCell] = totalCost;
queue.queue({e: nextCell, p: totalCost, r});
}
});
}
}
const religionsMap = new Map(religions.map(r => [r.i, r]));
function checkCenters() {
const codes = religions.map(r => r.code);
religions.forEach(r => {
if (!r.i) return;
r.code = abbreviate(r.name, codes);
const isMainRoad = cellId => cells.road[cellId] - cells.crossroad[cellId] > 4;
const isTrail = cellId => cells.h[cellId] > 19 && cells.road[cellId] - cells.crossroad[cellId] === 1;
const isSeaRoute = cellId => cells.h[cellId] < 20 && cells.road[cellId];
const isWater = cellId => cells.h[cellId] < 20;
// move religion center if it's not within religion area after expansion
if (religionIds[r.center] === r.i) return; // in area
const firstCell = cells.i.find(i => religionIds[i] === r.i);
if (firstCell) r.center = firstCell; // move center, othervise it's an extinct religion
while (queue.length) {
const {e: cellId, p, r, s: state} = queue.dequeue();
const {culture, expansion, expansionism} = religionsMap.get(r);
cells.c[cellId].forEach(nextCell => {
if (expansion === "culture" && culture !== cells.culture[nextCell]) return;
if (expansion === "state" && state !== cells.state[nextCell]) return;
if (religionsMap.get(religionIds[nextCell])?.lock) return;
const cultureCost = culture !== cells.culture[nextCell] ? 10 : 0;
const stateCost = state !== cells.state[nextCell] ? 10 : 0;
const passageCost = getPassageCost(nextCell);
const cellCost = cultureCost + stateCost + passageCost;
const totalCost = p + 10 + cellCost / expansionism;
if (totalCost > maxExpansionCost) return;
if (!cost[nextCell] || totalCost < cost[nextCell]) {
if (cells.culture[nextCell]) religionIds[nextCell] = r; // assign religion to cell
cost[nextCell] = totalCost;
queue.queue({e: nextCell, p: totalCost, r, s: state});
}
});
}
};
return religionIds;
function getPassageCost(cellId) {
if (isWater(cellId)) return isSeaRoute ? 50 : 500;
if (isMainRoad(cellId)) return 1;
const biomeCost = biomePassageCost(cellId);
return isTrail(cellId) ? biomeCost / 1.5 : biomeCost;
}
}
// folk religions initially get all cells of their culture, and locked religions are retained
function spreadFolkReligions(religions) {
const cells = pack.cells;
const hasPrior = cells.religion && true;
const religionIds = new Uint16Array(cells.i.length);
const folkReligions = religions.filter(religion => religion.type === "Folk" && !religion.removed);
const cultureToReligionMap = new Map(folkReligions.map(({i, culture}) => [culture, i]));
for (const cellId of cells.i) {
const oldId = (hasPrior && cells.religion[cellId]) || 0;
if (oldId && religions[oldId]?.lock && !religions[oldId]?.removed) {
religionIds[cellId] = oldId;
continue;
}
const cultureId = cells.culture[cellId];
religionIds[cellId] = cultureToReligionMap.get(cultureId) || 0;
}
return religionIds;
}
function checkCenters() {
const cells = pack.cells;
pack.religions.forEach(r => {
if (!r.i) return;
// move religion center if it's not within religion area after expansion
if (cells.religion[r.center] === r.i) return; // in area
const firstCell = cells.i.find(i => cells.religion[i] === r.i);
const cultureHome = pack.cultures[r.culture]?.center;
if (firstCell) r.center = firstCell; // move center, othervise it's an extinct religion
else if (r.type === "Folk" && cultureHome) r.center = cultureHome; // reset extinct culture centers
});
}
function recalculate() {
const newReligionIds = expandReligions(pack.religions);
pack.cells.religion = newReligionIds;
checkCenters();
}
const add = function (center) {
const {cells, religions} = pack;
const {cells, cultures, religions} = pack;
const religionId = cells.religion[center];
const i = religions.length;
const culture = cells.culture[center];
const color = getMixedColor(religions[religionId].color, 0.3, 0);
const cultureId = cells.culture[center];
const missingFolk =
cultureId !== 0 &&
!religions.some(({type, culture, removed}) => type === "Folk" && culture === cultureId && !removed);
const color = missingFolk ? cultures[cultureId].color : getMixedColor(religions[religionId].color, 0.3, 0);
const type =
religions[religionId].type === "Organized" ? rw({Organized: 4, Cult: 1, Heresy: 2}) : rw({Organized: 5, Cult: 2});
const type = missingFolk
? "Folk"
: religions[religionId].type === "Organized"
? rw({Organized: 4, Cult: 1, Heresy: 2})
: rw({Organized: 5, Cult: 2});
const form = rw(forms[type]);
const deity =
type === "Heresy" ? religions[religionId].deity : form === "Non-theism" ? null : getDeityName(culture);
type === "Heresy"
? religions[religionId].deity
: form === "Non-theism" || form === "Animism"
? null
: getDeityName(cultureId);
let name, expansion;
if (type === "Organized") [name, expansion] = getReligionName(form, deity, center);
else {
name = getCultName(form, center);
expansion = "global";
}
const [name, expansion] = generateReligionName(type, form, deity, center);
const formName = type === "Heresy" ? religions[religionId].form : form;
const code = abbreviate(
name,
religions.map(r => r.code)
);
const influences = getReligionsInRadius(cells.c, center, cells.religion, i, 25, 3, 0);
const origins = type === "Folk" ? [0] : influences;
const i = religions.length;
religions.push({
i,
name,
color,
culture,
culture: cultureId,
type,
form: formName,
deity,
expansion,
expansionism: 0,
expansionism: expansionismMap[type](),
center,
cells: 0,
area: 0,
rural: 0,
urban: 0,
origins: [religionId],
origins,
code
});
cells.religion[center] = i;
};
function updateCultures() {
TIME && console.time("updateCulturesForReligions");
pack.religions = pack.religions.map((religion, index) => {
if (index === 0) {
return religion;
}
if (index === 0) return religion;
return {...religion, culture: pack.cells.culture[religion.center]};
});
TIME && console.timeEnd("updateCulturesForReligions");
}
// get supreme deity name
@ -735,22 +789,24 @@ window.Religions = (function () {
if (a === "Number") return ra(base.number);
if (a === "Being") return ra(base.being);
if (a === "Adjective") return ra(base.adjective);
if (a === "Color + Animal") return ra(base.color) + " " + ra(base.animal);
if (a === "Adjective + Animal") return ra(base.adjective) + " " + ra(base.animal);
if (a === "Adjective + Being") return ra(base.adjective) + " " + ra(base.being);
if (a === "Adjective + Genitive") return ra(base.adjective) + " " + ra(base.genitive);
if (a === "Color + Being") return ra(base.color) + " " + ra(base.being);
if (a === "Color + Genitive") return ra(base.color) + " " + ra(base.genitive);
if (a === "Being + of + Genitive") return ra(base.being) + " of " + ra(base.genitive);
if (a === "Being + of the + Genitive") return ra(base.being) + " of the " + ra(base.theGenitive);
if (a === "Animal + of + Genitive") return ra(base.animal) + " of " + ra(base.genitive);
if (a === "Color + Animal") return `${ra(base.color)} ${ra(base.animal)}`;
if (a === "Adjective + Animal") return `${ra(base.adjective)} ${ra(base.animal)}`;
if (a === "Adjective + Being") return `${ra(base.adjective)} ${ra(base.being)}`;
if (a === "Adjective + Genitive") return `${ra(base.adjective)} ${ra(base.genitive)}`;
if (a === "Color + Being") return `${ra(base.color)} ${ra(base.being)}`;
if (a === "Color + Genitive") return `${ra(base.color)} ${ra(base.genitive)}`;
if (a === "Being + of + Genitive") return `${ra(base.being)} of ${ra(base.genitive)}`;
if (a === "Being + of the + Genitive") return `${ra(base.being)} of the ${ra(base.theGenitive)}`;
if (a === "Animal + of + Genitive") return `${ra(base.animal)} of ${ra(base.genitive)}`;
if (a === "Adjective + Being + of + Genitive")
return ra(base.adjective) + " " + ra(base.being) + " of " + ra(base.genitive);
return `${ra(base.adjective)} ${ra(base.being)} of ${ra(base.genitive)}`;
if (a === "Adjective + Animal + of + Genitive")
return ra(base.adjective) + " " + ra(base.animal) + " of " + ra(base.genitive);
return `${ra(base.adjective)} ${ra(base.animal)} of ${ra(base.genitive)}`;
ERROR && console.error("Unkown generation approach");
}
function getReligionName(form, deity, center) {
function generateReligionName(variety, form, deity, center) {
const {cells, cultures, burgs, states} = pack;
const random = () => Names.getCulture(cells.culture[center], null, null, "", 0);
@ -766,7 +822,7 @@ window.Religions = (function () {
return adj ? getAdjective(name) : name;
};
const m = rw(methods);
const m = rw(namingMethods[variety]);
if (m === "Random + type") return [random() + " " + type(), "global"];
if (m === "Random + ism") return [trimVowels(random()) + "ism", "global"];
if (m === "Supreme + ism" && deity) return [trimVowels(supreme()) + "ism", "global"];
@ -776,24 +832,11 @@ window.Religions = (function () {
if (m === "Culture + ism") return [trimVowels(culture()) + "ism", "culture"];
if (m === "Place + ian + type") return [place("adj") + " " + type(), "state"];
if (m === "Culture + type") return [culture() + " " + type(), "culture"];
if (m === "Burg + ian + type") return [`${place("adj")} ${type()}`, "global"];
if (m === "Random + ian + type") return [`${getAdjective(random())} ${type()}`, "global"];
if (m === "Type + of the + meaning") return [`${type()} of the ${generateMeaning()}`, "global"];
return [trimVowels(random()) + "ism", "global"]; // else
}
function getCultName(form, center) {
const cells = pack.cells;
const type = function () {
return rw(types[form]);
};
const random = function () {
return trimVowels(Names.getCulture(cells.culture[center], null, null, "", 0).split(/[ ,]+/)[0]);
};
const burg = function () {
return trimVowels(pack.burgs[cells.burg[center]].name.split(/[ ,]+/)[0]);
};
if (cells.burg[center]) return burg() + "ian " + type();
if (Math.random() > 0.5) return random() + "ian " + type();
return type() + " of the " + generateMeaning();
}
return {generate, add, getDeityName, updateCultures};
return {generate, add, getDeityName, updateCultures, recalculate};
})();

View file

@ -48,7 +48,9 @@ window.Rivers = (function () {
cells.fl[i] += prec[cells.g[i]] / cellsNumberModifier; // add flux from precipitation
// create lake outlet if lake is not in deep depression and flux > evaporation
const lakes = lakeOutCells[i] ? features.filter(feature => i === feature.outCell && feature.flux > feature.evaporation) : [];
const lakes = lakeOutCells[i]
? features.filter(feature => i === feature.outCell && feature.flux > feature.evaporation)
: [];
for (const lake of lakes) {
const lakeCell = cells.c[i].find(c => h[c] < 20 && cells.f[c] === lake.i);
cells.fl[lakeCell] += Math.max(lake.flux - lake.evaporation, 0); // not evaporated lake water drains to outlet
@ -191,7 +193,18 @@ window.Rivers = (function () {
const length = getApproximateLength(meanderedPoints);
const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, 0));
pack.rivers.push({i: riverId, source, mouth, discharge, length, width, widthFactor, sourceWidth: 0, parent, cells: riverCells});
pack.rivers.push({
i: riverId,
source,
mouth,
discharge,
length,
width,
widthFactor,
sourceWidth: 0,
parent,
cells: riverCells
});
}
}
@ -479,6 +492,10 @@ window.Rivers = (function () {
return getBasin(parent);
};
const getNextId = function (rivers) {
return rivers.length ? Math.max(...rivers.map(r => r.i)) + 1 : 1;
};
return {
generate,
alterHeights,
@ -493,6 +510,7 @@ window.Rivers = (function () {
getOffset,
getApproximateLength,
getRiverPoints,
remove
remove,
getNextId
};
})();

View file

@ -1176,18 +1176,18 @@ function refreshAllEditors() {
// dynamically loaded editors
async function editStates() {
if (customization) return;
const Editor = await import("../dynamic/editors/states-editor.js?v=12062022");
const Editor = await import("../dynamic/editors/states-editor.js?v=1.89.05");
Editor.open();
}
async function editCultures() {
if (customization) return;
const Editor = await import("../dynamic/editors/cultures-editor.js?v=1.88.06");
const Editor = await import("../dynamic/editors/cultures-editor.js?v=1.89.09");
Editor.open();
}
async function editReligions() {
if (customization) return;
const Editor = await import("../dynamic/editors/religions-editor.js?v=1.88.06");
const Editor = await import("../dynamic/editors/religions-editor.js?v=1.89.10");
Editor.open();
}

View file

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

View file

@ -67,11 +67,11 @@ function editIce() {
function addIcebergOnClick() {
const [x, y] = d3.mouse(this);
const i = findGridCell(x, y, grid);
const c = grid.points[i];
const s = +document.getElementById("iceSize").value;
const [cx, cy] = grid.points[i];
const size = +document.getElementById("iceSize")?.value || 1;
const points = getGridPolygon(i).map(p => [(p[0] + (c[0] - p[0]) / s) | 0, (p[1] + (c[1] - p[1]) / s) | 0]);
const iceberg = ice.append("polygon").attr("points", points).attr("cell", i).attr("size", s);
const points = getGridPolygon(i).map(([x, y]) => [rn(lerp(cx, x, size), 2), rn(lerp(cy, y, size), 2)]);
const iceberg = ice.append("polygon").attr("points", points).attr("cell", i).attr("size", size);
iceberg.call(d3.drag().on("drag", dragElement));
if (d3.event.shiftKey === false) toggleAdd();
}

View file

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

View file

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

View file

@ -77,12 +77,15 @@ document
// show popup with a list of Patreon supportes (updated manually)
async function showSupporters() {
const {supporters} = await import("../dynamic/supporters.js?v=19062022");
const list = supporters.split("\n").sort();
const columns = window.innerWidth < 800 ? 2 : 5;
alertMessage.innerHTML =
"<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({
resizable: false,
title: "Patreon Supporters",
width: "54vw",
width: "min-width",
position: {my: "center", at: "center", of: "svg"}
});
}
@ -157,9 +160,20 @@ optionsContent.addEventListener("click", function (event) {
});
function mapSizeInputChange() {
const $mapWidthInput = byId("mapWidthInput");
const $mapHeightInput = byId("mapHeightInput");
changeMapSize();
localStorage.setItem("mapWidth", mapWidthInput.value);
localStorage.setItem("mapHeight", mapHeightInput.value);
localStorage.setItem("mapWidth", $mapWidthInput.value);
localStorage.setItem("mapHeight", $mapHeightInput.value);
const tooWide = +$mapWidthInput.value > window.innerWidth;
const tooHigh = +$mapHeightInput.value > window.innerHeight;
if (tooWide || tooHigh) {
const message = `Canvas size is larger than actual window size (${window.innerWidth} x ${window.innerHeight}). It can affect the performance if you are going to create a new map`;
tip(message, false, "warn", 4000);
}
}
// change svg size on manual size change or window resize, do not change graph size
@ -534,7 +548,7 @@ function applyStoredOptions() {
options.stateLabelsMode = stateLabelsModeInput.value;
}
// randomize options if randomization is allowed (not locked or options='default')
// randomize options if randomization is allowed (not locked or queryParam options='default')
function randomizeOptions() {
const randomize = new URL(window.location.href).searchParams.get("options") === "default"; // ignore stored options
@ -546,7 +560,7 @@ function randomizeOptions() {
manorsInput.value = 1000;
manorsOutput.value = "auto";
}
if (randomize || !locked("religions")) religionsInput.value = religionsOutput.value = gauss(5, 2, 2, 10);
if (randomize || !locked("religions")) religionsInput.value = religionsOutput.value = gauss(6, 3, 2, 10);
if (randomize || !locked("power")) powerInput.value = powerOutput.value = gauss(4, 2, 0, 10, 2);
if (randomize || !locked("neutral")) neutralInput.value = neutralOutput.value = rn(1 + Math.random(), 1);
if (randomize || !locked("cultures")) culturesInput.value = culturesOutput.value = gauss(12, 3, 5, 30);
@ -602,17 +616,17 @@ function randomizeCultureSet() {
function setRendering(value) {
viewbox.attr("shape-rendering", value);
if (value === "optimizeSpeed") {
// block some styles
coastline.select("#sea_island").style("filter", "none");
statesHalo.style("display", "none");
emblems.style("opacity", 1);
} else {
// remove style block
coastline.select("#sea_island").style("filter", null);
statesHalo.style("display", null);
emblems.style("opacity", null);
}
// if (value === "optimizeSpeed") {
// // block some styles
// coastline.select("#sea_island").style("filter", "none");
// statesHalo.style("display", "none");
// emblems.style("opacity", 1);
// } else {
// // remove style block
// coastline.select("#sea_island").style("filter", null);
// statesHalo.style("display", null);
// emblems.style("opacity", null);
// }
}
// generate current year and era name

View file

@ -74,12 +74,13 @@ function createRiver() {
function addRiver() {
const {rivers, cells} = pack;
const {addMeandering, getApproximateLength, getWidth, getOffset, getName, getRiverPath, getBasin} = Rivers;
const {addMeandering, getApproximateLength, getWidth, getOffset, getName, getRiverPath, getBasin, getNextId} =
Rivers;
const riverCells = createRiver.cells;
if (riverCells.length < 2) return tip("Add at least 2 cells", false, "error");
const riverId = rivers.length ? last(rivers).i + 1 : 1;
const riverId = getNextId(rivers);
const parent = cells.r[last(riverCells)] || riverId;
riverCells.forEach(cell => {
@ -100,12 +101,30 @@ function createRiver() {
const name = getName(mouth);
const basin = getBasin(parent);
rivers.push({i: riverId, source, mouth, discharge, length, width, widthFactor, sourceWidth, parent, cells: riverCells, basin, name, type: "River"});
rivers.push({
i: riverId,
source,
mouth,
discharge,
length,
width,
widthFactor,
sourceWidth,
parent,
cells: riverCells,
basin,
name,
type: "River"
});
const id = "river" + riverId;
// render river
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
viewbox.select("#rivers").append("path").attr("id", id).attr("d", getRiverPath(meanderedPoints, widthFactor, sourceWidth));
viewbox
.select("#rivers")
.append("path")
.attr("id", id)
.attr("d", getRiverPath(meanderedPoints, widthFactor, sourceWidth));
editRiver(id);
}

View file

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

View file

@ -1,14 +1,27 @@
// UI module to control the style presets
"use strict";
const systemPresets = ["default", "ancient", "gloom", "light", "watercolor", "clean", "atlas", "cyberpunk", "monochrome"];
const systemPresets = [
"default",
"ancient",
"gloom",
"pale",
"light",
"watercolor",
"clean",
"atlas",
"cyberpunk",
"monochrome"
];
const customPresetPrefix = "fmgStyle_";
// add style presets to list
{
const systemOptions = systemPresets.map(styleName => `<option value="${styleName}">${styleName}</option>`);
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("");
document.getElementById("stylePreset").innerHTML = options;
}
@ -37,7 +50,8 @@ async function getStylePreset(desiredPreset) {
const isValid = JSON.isValid(storedStyleJSON);
if (isValid) return [desiredPreset, JSON.parse(storedStyleJSON)];
ERROR && console.error(`Custom style ${desiredPreset} stored in localStorage is not valid. Applying default style`);
ERROR &&
console.error(`Custom style ${desiredPreset} stored in localStorage is not valid. Applying default style`);
presetToLoad = "default";
}
}
@ -145,8 +159,31 @@ function addStylePreset() {
"#stateBorders": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter"],
"#provinceBorders": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter"],
"#cells": ["opacity", "stroke", "stroke-width", "filter", "mask"],
"#gridOverlay": ["opacity", "scale", "dx", "dy", "type", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "transform", "filter", "mask"],
"#coordinates": ["opacity", "data-size", "font-size", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter", "mask"],
"#gridOverlay": [
"opacity",
"scale",
"dx",
"dy",
"type",
"stroke",
"stroke-width",
"stroke-dasharray",
"stroke-linecap",
"transform",
"filter",
"mask"
],
"#coordinates": [
"opacity",
"data-size",
"font-size",
"stroke",
"stroke-width",
"stroke-dasharray",
"stroke-linecap",
"filter",
"mask"
],
"#compass": ["opacity", "transform", "filter", "mask", "shape-rendering"],
"#rose": ["transform"],
"#relig": ["opacity", "stroke", "stroke-width", "filter"],
@ -174,7 +211,17 @@ function addStylePreset() {
"#statesBody": ["opacity", "filter"],
"#statesHalo": ["opacity", "data-width", "stroke-width", "filter"],
"#provs": ["opacity", "fill", "font-size", "font-family", "filter"],
"#temperature": ["opacity", "font-size", "fill", "fill-opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter"],
"#temperature": [
"opacity",
"font-size",
"fill",
"fill-opacity",
"stroke",
"stroke-width",
"stroke-dasharray",
"stroke-linecap",
"filter"
],
"#ice": ["opacity", "fill", "stroke", "stroke-width", "filter"],
"#emblems": ["opacity", "stroke-width", "filter"],
"#texture": ["opacity", "filter", "mask"],
@ -184,16 +231,65 @@ function addStylePreset() {
"#oceanBase": ["fill"],
"#oceanicPattern": ["href", "opacity"],
"#terrs": ["opacity", "scheme", "terracing", "skip", "relax", "curve", "filter", "mask"],
"#legend": ["data-size", "font-size", "font-family", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "data-x", "data-y", "data-columns"],
"#legend": [
"data-size",
"font-size",
"font-family",
"stroke",
"stroke-width",
"stroke-dasharray",
"stroke-linecap",
"data-x",
"data-y",
"data-columns"
],
"#legendBox": ["fill", "fill-opacity"],
"#burgLabels > #cities": ["opacity", "fill", "text-shadow", "data-size", "font-size", "font-family"],
"#burgIcons > #cities": ["opacity", "fill", "fill-opacity", "size", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap"],
"#burgIcons > #cities": [
"opacity",
"fill",
"fill-opacity",
"size",
"stroke",
"stroke-width",
"stroke-dasharray",
"stroke-linecap"
],
"#anchors > #cities": ["opacity", "fill", "size", "stroke", "stroke-width"],
"#burgLabels > #towns": ["opacity", "fill", "text-shadow", "data-size", "font-size", "font-family"],
"#burgIcons > #towns": ["opacity", "fill", "fill-opacity", "size", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap"],
"#burgIcons > #towns": [
"opacity",
"fill",
"fill-opacity",
"size",
"stroke",
"stroke-width",
"stroke-dasharray",
"stroke-linecap"
],
"#anchors > #towns": ["opacity", "fill", "size", "stroke", "stroke-width"],
"#labels > #states": ["opacity", "fill", "stroke", "stroke-width", "text-shadow", "data-size", "font-size", "font-family", "filter"],
"#labels > #addedLabels": ["opacity", "fill", "stroke", "stroke-width", "text-shadow", "data-size", "font-size", "font-family", "filter"],
"#labels > #states": [
"opacity",
"fill",
"stroke",
"stroke-width",
"text-shadow",
"data-size",
"font-size",
"font-family",
"filter"
],
"#labels > #addedLabels": [
"opacity",
"fill",
"stroke",
"stroke-width",
"text-shadow",
"data-size",
"font-size",
"font-family",
"filter"
],
"#fogging": ["opacity", "fill", "filter"]
};
@ -238,7 +334,8 @@ function addStylePreset() {
if (!styleJSON) return tip("Please provide a style JSON", false, "error");
if (!JSON.isValid(styleJSON)) return tip("JSON string is not valid, please check the format", false, "error");
if (!desiredName) return tip("Please provide a preset name", false, "error");
if (styleSaverTip.innerHTML === "default") return tip("You cannot overwrite default preset, please change the name", false, "error");
if (styleSaverTip.innerHTML === "default")
return tip("You cannot overwrite default preset, please change the name", false, "error");
const presetName = customPresetPrefix + desiredName;
applyOption(stylePreset, presetName, desiredName + " [custom]");

View file

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

View file

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

View file

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

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 getPolitics = ({entries, days}) => {
return [new CacheableResponsePlugin({statuses: [0, 200]}), new ExpirationPlugin({maxEntries: entries, maxAgeSeconds: days * DAY})];
return [
new CacheableResponsePlugin({statuses: [0, 200]}),
new ExpirationPlugin({maxEntries: entries, maxAgeSeconds: days * DAY})
];
};
registerRoute(
@ -21,7 +24,8 @@ registerRoute(
);
registerRoute(
({request, url}) => request.destination === "script" && !url.pathname.endsWith("min.js") && !url.pathname.includes("versioning.js"),
({request, url}) =>
request.destination === "script" && !url.pathname.endsWith("min.js") && !url.pathname.includes("versioning.js"),
new CacheFirst({
cacheName: "fmg-scripts",
plugins: getPolitics({entries: 100, days: 30})

View file

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

View file

@ -1,7 +1,7 @@
"use strict";
// version and caching control
const version = "1.89.00"; // generator version, update each time
const version = "1.89.15"; // generator version, update each time
{
document.title += " v" + version;
@ -28,6 +28,7 @@ const version = "1.89.00"; // generator version, update each time
<ul>
<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>Heightmap brushes: linear edit option</li>
<li>Data Charts screen</li>