mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-16 09:21:24 +01:00
Add the ability to lock states, provinces, cultures, and religions: refactoring (#910)
* Add the ability to lock states, provinces, cultures, and religions (#902) * Add the basis for locking everything, code and test the culture locking * Got the religion generator working, but not the tree. There are cycles being generated * Religions work now, including the tree view * Got the states and provinces working as well, all good and ready * Refresh the province editor when regenerating * Implement the versioning steps * Fix the state naming and color changing even when locked * The fix did not work with loaded maps, fix that too * Fix a few more bugs and address the PR feedback * Fix the state expanding event when they're locked bug * Implement some logic to ignore state being locked when regenerating provinces directly. * refactor(#902): start with states regenertion * refactor(#902): locked states cells to be assigned on start * refactor(#902): lock state - keep label * refactor(#902): lock provinces * refactor(#902): regenerate states - update provinces * refactor(#902): regenerate cultures * refactor(#902): regenerate religions Co-authored-by: Guillaume St-Pierre <gstpierre01@gmail.com> Co-authored-by: Azgaar <maxganiev@yandex.com>
This commit is contained in:
parent
4ab0311d01
commit
dada419f20
14 changed files with 497 additions and 281 deletions
|
|
@ -1437,7 +1437,9 @@ div.states .icon-trash-empty,
|
|||
div.states .icon-eye,
|
||||
div.states .icon-pin,
|
||||
div.states .icon-flag-empty,
|
||||
div.states .icon-cw {
|
||||
div.states .icon-cw,
|
||||
div.states .icon-lock,
|
||||
div.states .icon-lock-open {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
|
|
|||
27
index.html
27
index.html
|
|
@ -108,7 +108,7 @@
|
|||
}
|
||||
</style>
|
||||
|
||||
<link rel="preload" href="index.css?v=1.88.03" as="style" onload="this.onload=null; this.rel='stylesheet'" />
|
||||
<link rel="preload" href="index.css?v=1.89.00" 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>
|
||||
|
|
@ -2017,11 +2017,14 @@
|
|||
</button>
|
||||
<button
|
||||
id="regenerateStates"
|
||||
data-tip="Click to select new capitals and regenerate states. Emblems and military forces will be regenerated as well, burgs will remain as they are"
|
||||
data-tip="Click to select new capitals and regenerate non-locked states. Emblems and military forces will be regenerated as well, burgs will remain as they are"
|
||||
>
|
||||
States
|
||||
</button>
|
||||
<button id="regenerateProvinces" data-tip="Click to regenerate provinces. States will remain as they are">
|
||||
<button
|
||||
id="regenerateProvinces"
|
||||
data-tip="Click to regenerate non-locked provinces. States will remain as they are"
|
||||
>
|
||||
Provinces
|
||||
</button>
|
||||
<button
|
||||
|
|
@ -2031,8 +2034,8 @@
|
|||
Burgs
|
||||
</button>
|
||||
<button id="regenerateEmblems" data-tip="Click to regenerate all emblems">Emblems</button>
|
||||
<button id="regenerateReligions" data-tip="Click to regenerate religions">Religions</button>
|
||||
<button id="regenerateCultures" data-tip="Click to regenerate cultures">Cultures</button>
|
||||
<button id="regenerateReligions" data-tip="Click to regenerate non-locked religions">Religions</button>
|
||||
<button id="regenerateCultures" data-tip="Click to regenerate non-locked cultures">Cultures</button>
|
||||
<button
|
||||
id="regenerateMilitary"
|
||||
data-tip="Click to recalculate military forces based on current military options"
|
||||
|
|
@ -2040,7 +2043,7 @@
|
|||
Military
|
||||
</button>
|
||||
<button id="regenerateIce" data-tip="Click to icebergs and glaciers">Ice</button>
|
||||
<button id="regenerateMarkers" data-tip="Click to regenerate markers">
|
||||
<button id="regenerateMarkers" data-tip="Click to regenerate unlocked markers">
|
||||
Markers <i id="configRegenerateMarkers" class="icon-cog" data-tip="Click to set number multiplier"></i>
|
||||
</button>
|
||||
<button
|
||||
|
|
@ -7842,10 +7845,10 @@
|
|||
<script src="modules/river-generator.js"></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.87.14"></script>
|
||||
<script src="modules/burgs-and-states.js?v=1.87.10"></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/routes-generator.js"></script>
|
||||
<script src="modules/religions-generator.js"></script>
|
||||
<script src="modules/religions-generator.js?v=1.89.00"></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>
|
||||
|
|
@ -7864,11 +7867,11 @@
|
|||
|
||||
<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.88.06"></script>
|
||||
<script defer src="modules/ui/tools.js?v=1.88.05"></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/world-configurator.js"></script>
|
||||
<script defer src="modules/ui/heightmap-editor.js?v=1.88.03"></script>
|
||||
<script defer src="modules/ui/provinces-editor.js"></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>
|
||||
|
|
|
|||
|
|
@ -364,7 +364,7 @@ window.BurgsAndStates = (function () {
|
|||
TIME && console.time("expandStates");
|
||||
const {cells, states, cultures, burgs} = pack;
|
||||
|
||||
cells.state = new Uint16Array(cells.i.length);
|
||||
cells.state = cells.state || new Uint16Array(cells.i.length);
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||
const cost = [];
|
||||
const neutral = (cells.i.length / 5000) * 2500 * neutralInput.value * statesNeutral; // limit cost for state growth
|
||||
|
|
@ -386,7 +386,9 @@ window.BurgsAndStates = (function () {
|
|||
const {type, culture} = states[s];
|
||||
|
||||
cells.c[e].forEach(e => {
|
||||
if (cells.state[e] && e === states[cells.state[e]].center) return; // do not overwrite capital cells
|
||||
const state = states[cells.state[e]];
|
||||
if (state.lock) return; // do not overwrite cell of locked states
|
||||
if (cells.state[e] && e === state.center) return; // do not overwrite capital cells
|
||||
|
||||
const cultureCost = culture === cells.culture[e] ? -9 : 100;
|
||||
const populationCost = cells.h[e] < 20 ? 0 : cells.s[e] ? Math.max(20 - cells.s[e], 0) : 5000;
|
||||
|
|
@ -451,11 +453,12 @@ window.BurgsAndStates = (function () {
|
|||
|
||||
for (const i of cells.i) {
|
||||
if (cells.h[i] < 20 || cells.burg[i]) continue; // do not overwrite burgs
|
||||
if (pack.states[cells.state[i]]?.lock) continue; // do not overwrite cells of locks states
|
||||
if (cells.c[i].some(c => burgs[cells.burg[c]].capital)) continue; // do not overwrite near capital
|
||||
const neibs = cells.c[i].filter(c => cells.h[c] >= 20);
|
||||
const adversaries = neibs.filter(c => cells.state[c] !== cells.state[i]);
|
||||
const adversaries = neibs.filter(c => !pack.states[cells.state[c]]?.lock && cells.state[c] !== cells.state[i]);
|
||||
if (adversaries.length < 2) continue;
|
||||
const buddies = neibs.filter(c => cells.state[c] === cells.state[i]);
|
||||
const buddies = neibs.filter(c => !pack.states[cells.state[c]]?.lock && cells.state[c] === cells.state[i]);
|
||||
if (buddies.length > 2) continue;
|
||||
if (adversaries.length <= buddies.length) continue;
|
||||
cells.state[i] = cells.state[adversaries[0]];
|
||||
|
|
@ -498,7 +501,8 @@ window.BurgsAndStates = (function () {
|
|||
const mode = options.stateLabelsMode || "auto";
|
||||
|
||||
for (const s of states) {
|
||||
if (!s.i || s.removed || !s.cells || (list && !list.includes(s.i))) continue;
|
||||
if (!s.i || s.removed || s.lock || !s.cells || (list && !list.includes(s.i))) continue;
|
||||
|
||||
const used = [];
|
||||
const visualCenter = findCell(s.pole[0], s.pole[1]);
|
||||
const start = cells.state[visualCenter] === s.i ? visualCenter : s.center;
|
||||
|
|
@ -598,10 +602,13 @@ window.BurgsAndStates = (function () {
|
|||
const displayed = layerIsOn("toggleLabels");
|
||||
if (!displayed) toggleLabels();
|
||||
|
||||
if (!list) {
|
||||
// remove all labels and textpaths
|
||||
g.selectAll("text").remove();
|
||||
t.selectAll("path[id*='stateLabel']").remove();
|
||||
// remove state labels to be redrawn
|
||||
for (const state of pack.states) {
|
||||
if (!state.i || state.removed || state.lock) continue;
|
||||
if (list && !list.includes(state.i)) continue;
|
||||
|
||||
byId(`stateLabel${state.i}`)?.remove();
|
||||
byId(`textPath_stateLabel6${state.i}`)?.remove();
|
||||
}
|
||||
|
||||
const example = g.append("text").attr("x", 0).attr("x", 0).text("Average");
|
||||
|
|
@ -612,11 +619,6 @@ window.BurgsAndStates = (function () {
|
|||
const state = states[p[0]];
|
||||
const {name, fullName} = state;
|
||||
|
||||
if (list) {
|
||||
t.select("#textPath_stateLabel" + id).remove();
|
||||
g.select("#stateLabel" + id).remove();
|
||||
}
|
||||
|
||||
const path = p[1].length > 1 ? round(lineGen(p[1])) : `M${p[1][0][0] - 50},${p[1][0][1]}h${100}`;
|
||||
const textPath = t
|
||||
.append("path")
|
||||
|
|
@ -752,7 +754,7 @@ window.BurgsAndStates = (function () {
|
|||
|
||||
// assign basic color using greedy coloring algorithm
|
||||
pack.states.forEach(s => {
|
||||
if (!s.i || s.removed) return;
|
||||
if (!s.i || s.removed || s.lock) return;
|
||||
const neibs = s.neighbors;
|
||||
s.color = colors.find(c => neibs.every(n => pack.states[n].color !== c));
|
||||
if (!s.color) s.color = getRandomColor();
|
||||
|
|
@ -761,7 +763,7 @@ window.BurgsAndStates = (function () {
|
|||
|
||||
// randomize each already used color a bit
|
||||
colors.forEach(c => {
|
||||
const sameColored = pack.states.filter(s => s.color === c);
|
||||
const sameColored = pack.states.filter(s => s.color === c && !s.lock);
|
||||
sameColored.forEach((s, d) => {
|
||||
if (!d) return;
|
||||
s.color = getMixedColor(s.color);
|
||||
|
|
@ -990,7 +992,7 @@ window.BurgsAndStates = (function () {
|
|||
// select a forms for listed or all valid states
|
||||
const defineStateForms = function (list) {
|
||||
TIME && console.time("defineStateForms");
|
||||
const states = pack.states.filter(s => s.i && !s.removed);
|
||||
const states = pack.states.filter(s => s.i && !s.removed && !s.lock);
|
||||
if (states.length < 1) return;
|
||||
|
||||
const generic = {Monarchy: 25, Republic: 2, Union: 1};
|
||||
|
|
@ -1142,20 +1144,33 @@ window.BurgsAndStates = (function () {
|
|||
return adjName ? `${getAdjective(s.name)} ${s.formName}` : `${s.formName} of ${s.name}`;
|
||||
};
|
||||
|
||||
const generateProvinces = function (regenerate) {
|
||||
const generateProvinces = function (regenerate = false, regenerateInLockedStates = false) {
|
||||
TIME && console.time("generateProvinces");
|
||||
const localSeed = regenerate ? generateSeed() : seed;
|
||||
Math.random = aleaPRNG(localSeed);
|
||||
|
||||
const {cells, states, burgs} = pack;
|
||||
const provinces = (pack.provinces = [0]);
|
||||
cells.province = new Uint16Array(cells.i.length); // cell state
|
||||
const percentage = +provincesInput.value;
|
||||
const provinces = [0];
|
||||
const provinceIds = new Uint16Array(cells.i.length);
|
||||
|
||||
if (states.length < 2 || !percentage) {
|
||||
states.forEach(s => (s.provinces = []));
|
||||
return;
|
||||
} // no provinces
|
||||
const isProvinceLocked = province => province.lock || (!regenerateInLockedStates && states[province.state]?.lock);
|
||||
const isProvinceCellLocked = cell => provinceIds[cell] && isProvinceLocked(provinces[provinceIds[cell]]);
|
||||
|
||||
if (regenerate) {
|
||||
pack.provinces.forEach(province => {
|
||||
if (!province.i || province.removed || !isProvinceLocked(province)) return;
|
||||
|
||||
const newId = provinces.length;
|
||||
for (const i of cells.i) {
|
||||
if (cells.province[i] === province.i) provinceIds[i] = newId;
|
||||
}
|
||||
|
||||
province.i = newId;
|
||||
provinces.push(province);
|
||||
});
|
||||
}
|
||||
|
||||
const percentage = +provincesInput.value;
|
||||
|
||||
const max = percentage == 100 ? 1000 : gauss(20, 5, 5, 100) * percentage ** 0.5; // max growth
|
||||
|
||||
|
|
@ -1168,21 +1183,24 @@ window.BurgsAndStates = (function () {
|
|||
Wild: {Territory: 10, Land: 5, Region: 2, Tribe: 1, Clan: 1, Dependency: 1, Area: 1}
|
||||
};
|
||||
|
||||
// generate provinces for a selected burgs
|
||||
// generate provinces for selected burgs
|
||||
states.forEach(s => {
|
||||
s.provinces = [];
|
||||
if (!s.i || s.removed) return;
|
||||
if (provinces.length) s.provinces = provinces.filter(p => p.state === s.i).map(p => p.i); // locked provinces ids
|
||||
if (s.lock && !regenerateInLockedStates) return; // don't regenerate provinces of a locked state
|
||||
|
||||
const stateBurgs = burgs
|
||||
.filter(b => b.state === s.i && !b.removed)
|
||||
.filter(b => b.state === s.i && !b.removed && !provinceIds[b.cell])
|
||||
.sort((a, b) => b.population * gauss(1, 0.2, 0.5, 1.5, 3) - a.population)
|
||||
.sort((a, b) => b.capital - a.capital);
|
||||
if (stateBurgs.length < 2) return; // at least 2 provinces are required
|
||||
const provincesNumber = Math.max(Math.ceil((stateBurgs.length * percentage) / 100), 2);
|
||||
|
||||
const form = Object.assign({}, forms[s.form]);
|
||||
|
||||
for (let i = 0; i < provincesNumber; i++) {
|
||||
const province = provinces.length;
|
||||
s.provinces.push(province);
|
||||
const provinceId = provinces.length;
|
||||
const center = stateBurgs[i].cell;
|
||||
const burg = stateBurgs[i].i;
|
||||
const c = stateBurgs[i].culture;
|
||||
|
|
@ -1196,27 +1214,29 @@ window.BurgsAndStates = (function () {
|
|||
const type = getType(center, burg.port);
|
||||
const coa = COA.generate(stateBurgs[i].coa, kinship, null, type);
|
||||
coa.shield = COA.getShield(c, s.i);
|
||||
provinces.push({i: province, state: s.i, center, burg, name, formName, fullName, color, coa});
|
||||
|
||||
s.provinces.push(provinceId);
|
||||
provinces.push({i: provinceId, state: s.i, center, burg, name, formName, fullName, color, coa});
|
||||
}
|
||||
});
|
||||
|
||||
// expand generated provinces
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||
const cost = [];
|
||||
provinces.forEach(function (p) {
|
||||
if (!p.i || p.removed) return;
|
||||
cells.province[p.center] = p.i;
|
||||
|
||||
provinces.forEach(p => {
|
||||
if (!p.i || p.removed || isProvinceLocked(p)) return;
|
||||
provinceIds[p.center] = p.i;
|
||||
queue.queue({e: p.center, p: 0, province: p.i, state: p.state});
|
||||
cost[p.center] = 1;
|
||||
});
|
||||
|
||||
while (queue.length) {
|
||||
const next = queue.dequeue(),
|
||||
n = next.e,
|
||||
p = next.p,
|
||||
province = next.province,
|
||||
state = next.state;
|
||||
cells.c[n].forEach(function (e) {
|
||||
const {e, p, province, state} = queue.dequeue();
|
||||
|
||||
cells.c[e].forEach(e => {
|
||||
if (isProvinceCellLocked(e)) return; // do not overwrite cell of locked provinces
|
||||
|
||||
const land = cells.h[e] >= 20;
|
||||
if (!land && !cells.t[e]) return; // cannot pass deep ocean
|
||||
if (land && cells.state[e] !== state) return;
|
||||
|
|
@ -1225,7 +1245,7 @@ window.BurgsAndStates = (function () {
|
|||
|
||||
if (totalCost > max) return;
|
||||
if (!cost[e] || totalCost < cost[e]) {
|
||||
if (land) cells.province[e] = province; // assign province to a cell
|
||||
if (land) provinceIds[e] = province; // assign province to a cell
|
||||
cost[e] = totalCost;
|
||||
queue.queue({e, p: totalCost, province, state});
|
||||
}
|
||||
|
|
@ -1235,20 +1255,29 @@ window.BurgsAndStates = (function () {
|
|||
// justify provinces shapes a bit
|
||||
for (const i of cells.i) {
|
||||
if (cells.burg[i]) continue; // do not overwrite burgs
|
||||
const neibs = cells.c[i].filter(c => cells.state[c] === cells.state[i]).map(c => cells.province[c]);
|
||||
const adversaries = neibs.filter(c => c !== cells.province[i]);
|
||||
if (isProvinceCellLocked(i)) continue; // do not overwrite cell of locked provinces
|
||||
|
||||
const neibs = cells.c[i]
|
||||
.filter(c => cells.state[c] === cells.state[i] && !isProvinceCellLocked(c))
|
||||
.map(c => provinceIds[c]);
|
||||
const adversaries = neibs.filter(c => c !== provinceIds[i]);
|
||||
if (adversaries.length < 2) continue;
|
||||
const buddies = neibs.filter(c => c === cells.province[i]).length;
|
||||
|
||||
const buddies = neibs.filter(c => c === provinceIds[i]).length;
|
||||
if (buddies.length > 2) continue;
|
||||
|
||||
const competitors = adversaries.map(p => adversaries.reduce((s, v) => (v === p ? s + 1 : s), 0));
|
||||
const max = d3.max(competitors);
|
||||
if (buddies >= max) continue;
|
||||
cells.province[i] = adversaries[competitors.indexOf(max)];
|
||||
|
||||
provinceIds[i] = adversaries[competitors.indexOf(max)];
|
||||
}
|
||||
|
||||
// add "wild" provinces if some cells don't have a province assigned
|
||||
const noProvince = Array.from(cells.i).filter(i => cells.state[i] && !cells.province[i]); // cells without province assigned
|
||||
const noProvince = Array.from(cells.i).filter(i => cells.state[i] && !provinceIds[i]); // cells without province assigned
|
||||
states.forEach(s => {
|
||||
if (!s.i || s.removed) return;
|
||||
if (s.lock && !regenerateInLockedStates) return;
|
||||
if (!s.provinces.length) return;
|
||||
|
||||
const coreProvinceNames = s.provinces.map(p => provinces[p]?.name);
|
||||
|
|
@ -1261,36 +1290,34 @@ window.BurgsAndStates = (function () {
|
|||
return spliced[0] ? `New ${spliced[0]}` : null;
|
||||
};
|
||||
|
||||
let stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !cells.province[i]);
|
||||
let stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !provinceIds[i]);
|
||||
while (stateNoProvince.length) {
|
||||
// add new province
|
||||
const province = provinces.length;
|
||||
const provinceId = provinces.length;
|
||||
const burgCell = stateNoProvince.find(i => cells.burg[i]);
|
||||
const center = burgCell ? burgCell : stateNoProvince[0];
|
||||
const burg = burgCell ? cells.burg[burgCell] : 0;
|
||||
cells.province[center] = province;
|
||||
provinceIds[center] = provinceId;
|
||||
|
||||
// expand province
|
||||
const cost = [];
|
||||
cost[center] = 1;
|
||||
queue.queue({e: center, p: 0});
|
||||
while (queue.length) {
|
||||
const next = queue.dequeue(),
|
||||
n = next.e,
|
||||
p = next.p;
|
||||
const {e, p} = queue.dequeue();
|
||||
|
||||
cells.c[n].forEach(function (e) {
|
||||
if (cells.province[e]) return;
|
||||
const land = cells.h[e] >= 20;
|
||||
if (cells.state[e] && cells.state[e] !== s.i) return;
|
||||
const ter = land ? (cells.state[e] === s.i ? 3 : 20) : cells.t[e] ? 10 : 30;
|
||||
cells.c[e].forEach(nextCellId => {
|
||||
if (provinceIds[nextCellId]) return;
|
||||
const land = cells.h[nextCellId] >= 20;
|
||||
if (cells.state[nextCellId] && cells.state[nextCellId] !== s.i) return;
|
||||
const ter = land ? (cells.state[nextCellId] === s.i ? 3 : 20) : cells.t[nextCellId] ? 10 : 30;
|
||||
const totalCost = p + ter;
|
||||
|
||||
if (totalCost > max) return;
|
||||
if (!cost[e] || totalCost < cost[e]) {
|
||||
if (land && cells.state[e] === s.i) cells.province[e] = province; // assign province to a cell
|
||||
cost[e] = totalCost;
|
||||
queue.queue({e, p: totalCost});
|
||||
if (!cost[nextCellId] || totalCost < cost[nextCellId]) {
|
||||
if (land && cells.state[nextCellId] === s.i) provinceIds[nextCellId] = provinceId; // assign province to a cell
|
||||
cost[nextCellId] = totalCost;
|
||||
queue.queue({e: nextCellId, p: totalCost});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -1300,7 +1327,7 @@ window.BurgsAndStates = (function () {
|
|||
const f = pack.features[cells.f[center]];
|
||||
const color = getMixedColor(s.color);
|
||||
|
||||
const provCells = stateNoProvince.filter(i => cells.province[i] === province);
|
||||
const provCells = stateNoProvince.filter(i => provinceIds[i] === provinceId);
|
||||
const singleIsle = provCells.length === f.cells && !provCells.find(i => cells.f[i] !== f.i);
|
||||
const isleGroup = !singleIsle && !provCells.find(i => pack.features[cells.f[i]].group !== "isle");
|
||||
const colony = !singleIsle && !isleGroup && P(0.5) && !isPassable(s.center, center);
|
||||
|
|
@ -1327,8 +1354,8 @@ window.BurgsAndStates = (function () {
|
|||
const coa = COA.generate(s.coa, kinship, dominion, type);
|
||||
coa.shield = COA.getShield(c, s.i);
|
||||
|
||||
provinces.push({i: province, state: s.i, center, burg, name, formName, fullName, color, coa});
|
||||
s.provinces.push(province);
|
||||
provinces.push({i: provinceId, state: s.i, center, burg, name, formName, fullName, color, coa});
|
||||
s.provinces.push(provinceId);
|
||||
|
||||
// check if there is a land way within the same state between two cells
|
||||
function isPassable(from, to) {
|
||||
|
|
@ -1349,10 +1376,13 @@ window.BurgsAndStates = (function () {
|
|||
}
|
||||
|
||||
// re-check
|
||||
stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !cells.province[i]);
|
||||
stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !provinceIds[i]);
|
||||
}
|
||||
});
|
||||
|
||||
cells.province = provinceIds;
|
||||
pack.provinces = provinces;
|
||||
|
||||
TIME && console.timeEnd("generateProvinces");
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ window.Cultures = (function () {
|
|||
const generate = function () {
|
||||
TIME && console.time("generateCultures");
|
||||
cells = pack.cells;
|
||||
cells.culture = new Uint16Array(cells.i.length); // cell cultures
|
||||
|
||||
const cultureIds = new Uint16Array(cells.i.length); // cell cultures
|
||||
let count = Math.min(+culturesInput.value, +culturesSet.selectedOptions[0].dataset.max);
|
||||
|
||||
const populated = cells.i.filter(i => cells.s[i]); // populated cells
|
||||
|
|
@ -15,9 +16,12 @@ window.Cultures = (function () {
|
|||
if (!count) {
|
||||
WARN && console.warn(`There are no populated cells. Cannot generate cultures`);
|
||||
pack.cultures = [{name: "Wildlands", i: 0, base: 1, shield: "round"}];
|
||||
alertMessage.innerHTML = /* html */ ` The climate is harsh and people cannot live in this world.<br />
|
||||
cells.culture = cultureIds;
|
||||
|
||||
alertMessage.innerHTML = /* html */ `The climate is harsh and people cannot live in this world.<br />
|
||||
No cultures, states and burgs will be created.<br />
|
||||
Please consider changing climate settings in the World Configurator`;
|
||||
|
||||
$("#alert").dialog({
|
||||
resizable: false,
|
||||
title: "Extreme climate warning",
|
||||
|
|
@ -51,10 +55,25 @@ window.Cultures = (function () {
|
|||
const emblemShape = document.getElementById("emblemShape").value;
|
||||
|
||||
const codes = [];
|
||||
|
||||
cultures.forEach(function (c, i) {
|
||||
const newId = i + 1;
|
||||
|
||||
if (c.lock) {
|
||||
codes.push(c.code);
|
||||
centers.add(c.center);
|
||||
|
||||
for (const i of cells.i) {
|
||||
if (cells.culture[i] === c.i) cultureIds[i] = newId;
|
||||
}
|
||||
|
||||
c.i = newId;
|
||||
return;
|
||||
}
|
||||
|
||||
const cell = (c.center = placeCenter(c.sort ? c.sort : i => cells.s[i]));
|
||||
centers.add(cells.p[cell]);
|
||||
c.i = i + 1;
|
||||
c.i = newId;
|
||||
delete c.odd;
|
||||
delete c.sort;
|
||||
c.color = colors[i];
|
||||
|
|
@ -63,20 +82,27 @@ window.Cultures = (function () {
|
|||
c.origins = [0];
|
||||
c.code = abbreviate(c.name, codes);
|
||||
codes.push(c.code);
|
||||
cells.culture[cell] = i + 1;
|
||||
cultureIds[cell] = newId;
|
||||
if (emblemShape === "random") c.shield = getRandomShield();
|
||||
});
|
||||
|
||||
cells.culture = cultureIds;
|
||||
|
||||
function placeCenter(v) {
|
||||
let c,
|
||||
spacing = (graphWidth + graphHeight) / 2 / count;
|
||||
const sorted = [...populated].sort((a, b) => v(b) - v(a)),
|
||||
max = Math.floor(sorted.length / 2);
|
||||
do {
|
||||
c = sorted[biased(0, max, 5)];
|
||||
let spacing = (graphWidth + graphHeight) / 2 / count;
|
||||
const MAX_ATTEMPTS = 100;
|
||||
|
||||
const sorted = [...populated].sort((a, b) => v(b) - v(a));
|
||||
const max = Math.floor(sorted.length / 2);
|
||||
|
||||
let cellId = 0;
|
||||
for (let i = 0; i < MAX_ATTEMPTS; i++) {
|
||||
cellId = sorted[biased(0, max, 5)];
|
||||
spacing *= 0.9;
|
||||
} while (centers.find(cells.p[c][0], cells.p[c][1], spacing) !== undefined);
|
||||
return c;
|
||||
if (!cultureIds[cellId] && !centers.find(cells.p[cellId][0], cells.p[cellId][1], spacing)) break;
|
||||
}
|
||||
|
||||
return cellId;
|
||||
}
|
||||
|
||||
// the first culture with id 0 is for wildlands
|
||||
|
|
@ -98,6 +124,10 @@ window.Cultures = (function () {
|
|||
const count = Math.min(c, def.length);
|
||||
const cultures = [];
|
||||
|
||||
pack.cultures?.forEach(function (culture) {
|
||||
if (culture.lock) cultures.push(culture);
|
||||
});
|
||||
|
||||
for (let culture, rnd, i = 0; cultures.length < count && i < 200; i++) {
|
||||
do {
|
||||
rnd = rand(def.length - 1);
|
||||
|
|
@ -481,7 +511,7 @@ window.Cultures = (function () {
|
|||
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||
pack.cultures.forEach(function (c) {
|
||||
if (!c.i || c.removed) return;
|
||||
if (!c.i || c.removed || c.lock) return;
|
||||
queue.queue({e: c.center, p: 0, c: c.i});
|
||||
});
|
||||
|
||||
|
|
@ -493,7 +523,9 @@ window.Cultures = (function () {
|
|||
p = next.p,
|
||||
c = next.c;
|
||||
const type = pack.cultures[c].type;
|
||||
cells.c[n].forEach(function (e) {
|
||||
cells.c[n].forEach(e => {
|
||||
if (pack.cultures[cells.culture[e]]?.lock) return;
|
||||
|
||||
const biome = cells.biome[e];
|
||||
const biomeCost = getBiomeCost(c, biome, type);
|
||||
const biomeChangeCost = biome === cells.biome[n] ? 0 : 20; // penalty on biome change
|
||||
|
|
|
|||
|
|
@ -228,6 +228,7 @@ function culturesEditorAddLines() {
|
|||
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>
|
||||
${getShapeOptions(selectShape, c.shield)}
|
||||
<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>`;
|
||||
}
|
||||
|
|
@ -257,6 +258,8 @@ function culturesEditorAddLines() {
|
|||
$body.querySelectorAll("div > div.culturePopulation").forEach($el => $el.on("click", changePopulation));
|
||||
$body.querySelectorAll("div > span.icon-arrows-cw").forEach($el => $el.on("click", cultureRegenerateBurgs));
|
||||
$body.querySelectorAll("div > span.icon-trash-empty").forEach($el => $el.on("click", cultureRemovePrompt));
|
||||
$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));
|
||||
|
||||
const $culturesHeader = byId("culturesHeader");
|
||||
$culturesHeader.querySelector("div[data-sortby='emblems']").style.display = selectShape ? "inline-block" : "none";
|
||||
|
|
@ -928,3 +931,15 @@ async function uploadCulturesData() {
|
|||
drawCultures();
|
||||
refreshCulturesEditor();
|
||||
}
|
||||
|
||||
function updateLockStatus() {
|
||||
if (customization) return;
|
||||
|
||||
const cultureId = +this.parentNode.dataset.id;
|
||||
const classList = this.classList;
|
||||
const c = pack.cultures[cultureId];
|
||||
c.lock = !c.lock;
|
||||
|
||||
classList.toggle("icon-lock-open");
|
||||
classList.toggle("icon-lock");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -212,6 +212,10 @@ function religionsEditorAddLines() {
|
|||
<div data-tip="Religion area" class="religionArea hide" style="width: 5em">${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>
|
||||
<span data-tip="Remove religion" class="icon-trash-empty hide"></span>
|
||||
</div>`;
|
||||
}
|
||||
|
|
@ -242,6 +246,8 @@ function religionsEditorAddLines() {
|
|||
$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 > 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));
|
||||
|
||||
if ($body.dataset.type === "percentage") {
|
||||
$body.dataset.type = "absolute";
|
||||
|
|
@ -755,3 +761,15 @@ function closeReligionsEditor() {
|
|||
exitReligionsManualAssignment("close");
|
||||
exitAddReligionMode();
|
||||
}
|
||||
|
||||
function updateLockStatus() {
|
||||
if (customization) return;
|
||||
|
||||
const religionId = +this.parentNode.dataset.id;
|
||||
const classList = this.classList;
|
||||
const r = pack.religions[religionId];
|
||||
r.lock = !r.lock;
|
||||
|
||||
classList.toggle("icon-lock-open");
|
||||
classList.toggle("icon-lock");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -153,6 +153,8 @@ function addListeners() {
|
|||
else if (classList.contains("statePopulation")) changePopulation(stateId);
|
||||
else if (classList.contains("icon-pin")) toggleFog(stateId, classList);
|
||||
else if (classList.contains("icon-trash-empty")) stateRemovePrompt(stateId);
|
||||
else if (classList.contains("icon-lock") || classList.contains("icon-lock-open"))
|
||||
updateLockStatus(stateId, classList);
|
||||
});
|
||||
|
||||
$body.on("input", function (ev) {
|
||||
|
|
@ -288,6 +290,9 @@ function statesEditorAddLines() {
|
|||
<span data-tip="Cells count" class="icon-check-empty ${hidden} show hide"></span>
|
||||
<div data-tip="Cells count" class="stateCells ${hidden} show hide">${s.cells}</div>
|
||||
<span data-tip="Toggle state focus" class="icon-pin ${focused ? "" : " inactive"} hide"></span>
|
||||
<span data-tip="Lock the state to protect it from re-generation" class="icon-lock${
|
||||
s.lock ? "" : "-open"
|
||||
} hide"></span>
|
||||
<span data-tip="Remove the state" class="icon-trash-empty hide"></span>
|
||||
</div>`;
|
||||
}
|
||||
|
|
@ -1362,3 +1367,11 @@ function closeStatesEditor() {
|
|||
debug.selectAll(".highlight").remove();
|
||||
$body.innerHTML = "";
|
||||
}
|
||||
|
||||
function updateLockStatus(stateId, classList) {
|
||||
const s = pack.states[stateId];
|
||||
s.lock = !s.lock;
|
||||
|
||||
classList.toggle("icon-lock-open");
|
||||
classList.toggle("icon-lock");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ window.Markers = (function () {
|
|||
let candidates = Array.from(list(pack));
|
||||
let quantity = getQuantity(candidates, min, each, multiplier);
|
||||
// uncomment for debugging:
|
||||
// console.log(`${icon} ${type}: each ${each} of ${candidates.length}, min ${min} candidates. Got ${quantity}`);
|
||||
// console.info(`${icon} ${type}: each ${each} of ${candidates.length}, min ${min} candidates. Got ${quantity}`);
|
||||
|
||||
while (quantity && candidates.length) {
|
||||
const [cell] = extractAnyElement(candidates);
|
||||
|
|
|
|||
|
|
@ -344,15 +344,15 @@ window.Religions = (function () {
|
|||
|
||||
const generate = function () {
|
||||
TIME && console.time("generateReligions");
|
||||
const cells = pack.cells,
|
||||
states = pack.states,
|
||||
cultures = pack.cultures;
|
||||
const religions = (pack.religions = []);
|
||||
cells.religion = new Uint16Array(cells.culture); // cell religion; initially based on culture
|
||||
const {cells, states, cultures} = pack;
|
||||
|
||||
const religionIds = new Uint16Array(cells.culture); // cell religion; initially based on culture
|
||||
const religions = [];
|
||||
|
||||
// add folk religions
|
||||
pack.cultures.forEach(c => {
|
||||
if (!c.i) return religions.push({i: 0, name: "No religion"});
|
||||
const newId = c.i;
|
||||
if (!newId) return religions.push({i: 0, name: "No religion"});
|
||||
|
||||
if (c.removed) {
|
||||
religions.push({
|
||||
|
|
@ -364,11 +364,37 @@ window.Religions = (function () {
|
|||
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: c.i, name, color, culture: c.i, type: "Folk", form, deity, center: c.center, origins: [0]});
|
||||
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)
|
||||
|
|
@ -379,6 +405,7 @@ window.Religions = (function () {
|
|||
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);
|
||||
|
|
@ -387,10 +414,26 @@ window.Religions = (function () {
|
|||
function getReligionsInRadius({x, y, r, max}) {
|
||||
if (max === 0) return [0];
|
||||
const cellsInRadius = findAll(x, y, r);
|
||||
const religions = unique(cellsInRadius.map(i => cells.religion[i]).filter(r => 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
|
||||
|
|
@ -482,9 +525,7 @@ window.Religions = (function () {
|
|||
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 => cells.religion[i] === r.i && cells.c[i].some(c => cells.religion[c] !== r.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]);
|
||||
|
|
@ -513,9 +554,110 @@ window.Religions = (function () {
|
|||
});
|
||||
|
||||
expandHeresies();
|
||||
|
||||
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 = [];
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
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
|
||||
|
||||
while (queue.length) {
|
||||
const {e, p, r, c, s} = queue.dequeue();
|
||||
const expansion = religions[r].expansion;
|
||||
|
||||
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 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;
|
||||
|
||||
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});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// growth algorithm to assign cells to heresies
|
||||
function expandHeresies() {
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||
const cost = [];
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
const neutral = (cells.i.length / 5000) * 500 * neutralInput.value; // limit cost for heresies growth
|
||||
|
||||
while (queue.length) {
|
||||
const {e, p, r, b} = queue.dequeue();
|
||||
|
||||
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);
|
||||
|
||||
if (totalCost > neutral) return;
|
||||
|
||||
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});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function checkCenters() {
|
||||
const codes = religions.map(r => r.code);
|
||||
religions.forEach(r => {
|
||||
if (!r.i) return;
|
||||
r.code = abbreviate(r.name, codes);
|
||||
|
||||
// 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
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const add = function (center) {
|
||||
|
|
@ -566,117 +708,6 @@ window.Religions = (function () {
|
|||
cells.religion[center] = i;
|
||||
};
|
||||
|
||||
// growth algorithm to assign cells to religions
|
||||
const expandReligions = function () {
|
||||
const cells = pack.cells,
|
||||
religions = pack.religions;
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||
const cost = [];
|
||||
|
||||
religions
|
||||
.filter(r => r.type === "Organized" || r.type === "Cult")
|
||||
.forEach(r => {
|
||||
cells.religion[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;
|
||||
});
|
||||
|
||||
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
|
||||
|
||||
while (queue.length) {
|
||||
const next = queue.dequeue(),
|
||||
n = next.e,
|
||||
p = next.p,
|
||||
r = next.r,
|
||||
c = next.c,
|
||||
s = next.s;
|
||||
const expansion = religions[r].expansion;
|
||||
|
||||
cells.c[n].forEach(function (e) {
|
||||
if (expansion === "culture" && c !== cells.culture[e]) return;
|
||||
if (expansion === "state" && s !== cells.state[e]) return;
|
||||
|
||||
const cultureCost = c !== cells.culture[e] ? 10 : 0;
|
||||
const stateCost = s !== cells.state[e] ? 10 : 0;
|
||||
const biomeCost = cells.road[e] ? 1 : biomesData.cost[cells.biome[e]];
|
||||
const populationCost = Math.max(rn(popCost - cells.pop[e]), 0);
|
||||
const heightCost = Math.max(cells.h[e], 20) - 20;
|
||||
const waterCost = cells.h[e] < 20 ? (cells.road[e] ? 50 : 1000) : 0;
|
||||
const totalCost =
|
||||
p +
|
||||
(cultureCost + stateCost + biomeCost + populationCost + heightCost + waterCost) / religions[r].expansionism;
|
||||
if (totalCost > neutral) return;
|
||||
|
||||
if (!cost[e] || totalCost < cost[e]) {
|
||||
if (cells.h[e] >= 20 && cells.culture[e]) cells.religion[e] = r; // assign religion to cell
|
||||
cost[e] = totalCost;
|
||||
queue.queue({e, p: totalCost, r, c, s});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// growth algorithm to assign cells to heresies
|
||||
const expandHeresies = function () {
|
||||
const cells = pack.cells,
|
||||
religions = pack.religions;
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||
const cost = [];
|
||||
|
||||
religions
|
||||
.filter(r => r.type === "Heresy")
|
||||
.forEach(r => {
|
||||
const b = cells.religion[r.center]; // "base" religion id
|
||||
cells.religion[r.center] = r.i; // heresy id
|
||||
queue.queue({e: r.center, p: 0, r: r.i, b});
|
||||
cost[r.center] = 1;
|
||||
});
|
||||
|
||||
const neutral = (cells.i.length / 5000) * 500 * neutralInput.value; // limit cost for heresies growth
|
||||
|
||||
while (queue.length) {
|
||||
const next = queue.dequeue(),
|
||||
n = next.e,
|
||||
p = next.p,
|
||||
r = next.r,
|
||||
b = next.b;
|
||||
|
||||
cells.c[n].forEach(function (e) {
|
||||
const religionCost = cells.religion[e] === b ? 0 : 2000;
|
||||
const biomeCost = cells.road[e] ? 0 : biomesData.cost[cells.biome[e]];
|
||||
const heightCost = Math.max(cells.h[e], 20) - 20;
|
||||
const waterCost = cells.h[e] < 20 ? (cells.road[e] ? 50 : 1000) : 0;
|
||||
const totalCost =
|
||||
p + (religionCost + biomeCost + heightCost + waterCost) / Math.max(religions[r].expansionism, 0.1);
|
||||
|
||||
if (totalCost > neutral) return;
|
||||
|
||||
if (!cost[e] || totalCost < cost[e]) {
|
||||
if (cells.h[e] >= 20 && cells.culture[e]) cells.religion[e] = r; // assign religion to cell
|
||||
cost[e] = totalCost;
|
||||
queue.queue({e, p: totalCost, r});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function checkCenters() {
|
||||
const {cells, religions} = pack;
|
||||
|
||||
const codes = religions.map(r => r.code);
|
||||
religions.forEach(r => {
|
||||
if (!r.i) return;
|
||||
r.code = abbreviate(r.name, codes);
|
||||
|
||||
// move religion center if it's not within religion area after expansion
|
||||
if (cells.religion[r.center] === r.i) return; // in area
|
||||
const religCells = cells.i.filter(i => cells.religion[i] === r.i);
|
||||
if (!religCells.length) return; // extinct religion
|
||||
r.center = religCells.sort((a, b) => cells.pop[b] - cells.pop[a])[0];
|
||||
});
|
||||
}
|
||||
|
||||
function updateCultures() {
|
||||
TIME && console.time("updateCulturesForReligions");
|
||||
pack.religions = pack.religions.map((religion, index) => {
|
||||
|
|
@ -764,5 +795,5 @@ window.Religions = (function () {
|
|||
return type() + " of the " + generateMeaning();
|
||||
}
|
||||
|
||||
return {generate, add, getDeityName, expandReligions, updateCultures};
|
||||
return {generate, add, getDeityName, updateCultures};
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -210,7 +210,7 @@ function showMapTooltip(point, e, i, g) {
|
|||
const r = pack.religions[religion];
|
||||
const type = r.type === "Cult" || r.type == "Heresy" ? r.type : r.type + " religion";
|
||||
tip(type + ": " + r.name);
|
||||
if (religionsEditor?.offsetParent) highlightEditorLine(religionsEditor, religion);
|
||||
if (byId("religionsEditor")?.offsetParent) highlightEditorLine(religionsEditor, religion);
|
||||
} else if (pack.cells.state[i] && (layerIsOn("toggleProvinces") || layerIsOn("toggleStates"))) {
|
||||
const state = pack.cells.state[i];
|
||||
const stateName = pack.states[state].fullName;
|
||||
|
|
|
|||
|
|
@ -1132,6 +1132,7 @@ function drawBorders() {
|
|||
const provToCell = cells.c[i].find(
|
||||
n => cells.state[n] === s && p > cells.province[n] && pUsed[p][n] !== cells.province[n]
|
||||
);
|
||||
|
||||
if (provToCell) {
|
||||
const provTo = cells.province[provToCell];
|
||||
pUsed[p][provToCell] = provTo;
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ function editProvinces() {
|
|||
else if (cl.contains("culturePopulation")) changePopulation(p);
|
||||
else if (cl.contains("icon-pin")) toggleFog(p, cl);
|
||||
else if (cl.contains("icon-trash-empty")) removeProvince(p);
|
||||
else if (cl.contains("icon-lock") || cl.contains("icon-lock-open")) updateLockStatus(p, cl);
|
||||
});
|
||||
|
||||
body.addEventListener("change", function (ev) {
|
||||
|
|
@ -163,6 +164,7 @@ function editProvinces() {
|
|||
class="icon-flag-empty ${separable ? "" : "placeholder"} hide"
|
||||
></span>
|
||||
<span data-tip="Toggle province focus" class="icon-pin ${focused ? "" : " inactive"} hide"></span>
|
||||
<span data-tip="Lock the province" class="icon-lock${p.lock ? '' : '-open'} hide"></span>
|
||||
<span data-tip="Remove the province" class="icon-trash-empty hide"></span>
|
||||
</div>`;
|
||||
}
|
||||
|
|
@ -1086,3 +1088,11 @@ function editProvinces() {
|
|||
if (customization === 12) exitAddProvinceMode();
|
||||
}
|
||||
}
|
||||
|
||||
function updateLockStatus(provinceId, classList) {
|
||||
const p = pack.provinces[provinceId];
|
||||
p.lock = !p.lock;
|
||||
|
||||
classList.toggle("icon-lock-open");
|
||||
classList.toggle("icon-lock");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
"use strict";
|
||||
|
||||
// module to control the Tools options (click to edit, to re-geenerate, tp add)
|
||||
|
||||
toolsContent.addEventListener("click", function (event) {
|
||||
|
|
@ -138,64 +139,136 @@ function recalculatePopulation() {
|
|||
}
|
||||
|
||||
function regenerateStates() {
|
||||
recreateStates();
|
||||
BurgsAndStates.expandStates();
|
||||
BurgsAndStates.normalizeStates();
|
||||
BurgsAndStates.collectStatistics();
|
||||
BurgsAndStates.assignColors();
|
||||
BurgsAndStates.generateCampaigns();
|
||||
BurgsAndStates.generateDiplomacy();
|
||||
BurgsAndStates.defineStateForms();
|
||||
BurgsAndStates.generateProvinces(true);
|
||||
|
||||
layerIsOn("toggleStates") ? drawStates() : toggleStates();
|
||||
layerIsOn("toggleBorders") ? drawBorders() : toggleBorders();
|
||||
if (layerIsOn("toggleProvinces")) drawProvinces();
|
||||
|
||||
BurgsAndStates.drawStateLabels();
|
||||
Military.generate();
|
||||
if (layerIsOn("toggleEmblems")) drawEmblems();
|
||||
|
||||
if (document.getElementById("burgsOverviewRefresh")?.offsetParent) burgsOverviewRefresh.click();
|
||||
if (document.getElementById("statesEditorRefresh")?.offsetParent) statesEditorRefresh.click();
|
||||
if (document.getElementById("militaryOverviewRefresh")?.offsetParent) militaryOverviewRefresh.click();
|
||||
}
|
||||
|
||||
function recreateStates() {
|
||||
const localSeed = generateSeed();
|
||||
Math.random = aleaPRNG(localSeed);
|
||||
|
||||
const statesCount = +regionsOutput.value;
|
||||
const burgs = pack.burgs.filter(b => b.i && !b.removed);
|
||||
if (!burgs.length) return tip("There are no any burgs to generate states. Please create burgs first", false, "error");
|
||||
if (burgs.length < statesCount)
|
||||
tip(`Not enough burgs to generate ${statesCount} states. Will generate only ${burgs.length} states`, false, "warn");
|
||||
const validBurgs = pack.burgs.filter(b => b.i && !b.removed);
|
||||
|
||||
// turn all old capitals into towns
|
||||
burgs
|
||||
.filter(b => b.capital)
|
||||
.forEach(b => {
|
||||
moveBurgToGroup(b.i, "towns");
|
||||
b.capital = 0;
|
||||
});
|
||||
if (!validBurgs.length)
|
||||
return tip("There are no any burgs to generate states. Please create burgs first", false, "error");
|
||||
if (validBurgs.length < statesCount)
|
||||
tip(
|
||||
`Not enough burgs to generate ${statesCount} states. Will generate only ${validBurgs.length} states`,
|
||||
false,
|
||||
"warn"
|
||||
);
|
||||
|
||||
// remove emblems
|
||||
document.querySelectorAll("[id^=stateCOA]").forEach(el => el.remove());
|
||||
document.querySelectorAll("[id^=provinceCOA]").forEach(el => el.remove());
|
||||
emblems.selectAll("use").remove();
|
||||
const lockedStates = pack.states.filter(s => s.i && !s.removed && s.lock);
|
||||
const lockedStatesIds = lockedStates.map(s => s.i);
|
||||
const lockedStatesCapitals = lockedStates.map(s => s.capital);
|
||||
|
||||
// turn all old capitals into towns, except for the capitals of locked states
|
||||
for (const burg of validBurgs) {
|
||||
if (!burg.capital) continue;
|
||||
if (lockedStatesCapitals.includes(burg.i)) continue;
|
||||
|
||||
moveBurgToGroup(burg.i, "towns");
|
||||
burg.capital = 0;
|
||||
}
|
||||
|
||||
// remove labels and emblems for non-locked states
|
||||
for (const state of pack.states) {
|
||||
if (!state.i || state.removed || state.lock) continue;
|
||||
|
||||
// remove state labels
|
||||
byId(`stateLabel${state.i}`)?.remove();
|
||||
byId(`textPath_stateLabel${state.i}`)?.remove();
|
||||
|
||||
// remove state emblems
|
||||
byId(`stateCOA${state.i}`)?.remove();
|
||||
document.querySelector(`#stateEmblems > use[data-i="${state.i}"]`)?.remove();
|
||||
|
||||
// remove province data and emblems
|
||||
for (const provinceId of state.provinces) {
|
||||
byId(`provinceCOA${provinceId}`)?.remove();
|
||||
document.querySelector(`#provinceEmblems > use[data-i="${provinceId}"]`)?.remove();
|
||||
pack.provinces[provinceId].removed = true;
|
||||
}
|
||||
}
|
||||
|
||||
unfog();
|
||||
|
||||
if (!statesCount) {
|
||||
tip(`Cannot generate zero states. Please check the <i>States Number</i> option`, false, "warn");
|
||||
pack.states = pack.states.slice(0, 1); // remove all except of neutrals
|
||||
pack.states[0].diplomacy = []; // clear diplomacy
|
||||
pack.provinces = [0]; // remove all provinces
|
||||
pack.cells.state = new Uint16Array(pack.cells.i.length); // reset cells data
|
||||
borders.selectAll("path").remove(); // remove borders
|
||||
regions.selectAll("path").remove(); // remove states fill
|
||||
labels.select("#states").selectAll("text"); // remove state labels
|
||||
defs.select("#textPaths").selectAll("path[id*='stateLabel']").remove(); // remove state labels paths
|
||||
|
||||
if (document.getElementById("burgsOverviewRefresh").offsetParent) burgsOverviewRefresh.click();
|
||||
if (document.getElementById("statesEditorRefresh").offsetParent) statesEditorRefresh.click();
|
||||
return;
|
||||
}
|
||||
|
||||
// burg local ids sorted by a bit randomized population:
|
||||
const sortedBurgs = burgs
|
||||
.map((b, i) => [b, b.population * Math.random()])
|
||||
// burg local ids sorted by a bit randomized population. Also ignore burgs of a locked state
|
||||
const sortedBurgs = validBurgs
|
||||
.filter(b => !lockedStatesIds.includes(b.state))
|
||||
.map(b => [b, b.population * Math.random()])
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(b => b[0]);
|
||||
const capitalsTree = d3.quadtree();
|
||||
|
||||
const neutral = pack.states[0].name; // neutrals name
|
||||
const count = Math.min(statesCount, burgs.length) + 1; // +1 for neutral
|
||||
const count = Math.min(statesCount, validBurgs.length) + 1; // +1 for neutral
|
||||
let spacing = (graphWidth + graphHeight) / 2 / count; // min distance between capitals
|
||||
|
||||
pack.states = d3.range(count).map(i => {
|
||||
if (!i) return {i, name: neutral};
|
||||
const capitalsTree = d3.quadtree();
|
||||
const isTooClose = (x, y, spacing) => Boolean(capitalsTree.find(x, y, spacing));
|
||||
|
||||
const newStates = [{i: 0, name: pack.states[0].name}];
|
||||
|
||||
// restore locked states
|
||||
lockedStates.forEach(state => {
|
||||
const newId = newStates.length;
|
||||
const {x, y} = validBurgs[state.capital];
|
||||
capitalsTree.add([x, y]);
|
||||
|
||||
// update label id reference
|
||||
labels
|
||||
.select("#states")
|
||||
.select(`#stateLabel${state.i}`)
|
||||
.attr("id", `stateLabel${newId}`)
|
||||
.select("textPath")
|
||||
.attr("xlink:href", `#textPath_stateLabel${newId}`);
|
||||
defs.select("#textPaths").select(`#textPath_stateLabel${state.i}`).attr("id", `textPath_stateLabel${newId}`);
|
||||
|
||||
// update emblem id reference
|
||||
byId(`stateCOA${state.i}`)?.setAttribute("id", `stateCOA${newId}`);
|
||||
document.querySelector(`#stateEmblems > use[data-i="${state.i}"]`)?.setAttribute("data-i", newId);
|
||||
|
||||
state.provinces.forEach(provinceId => {
|
||||
if (!pack.provinces[provinceId]) return;
|
||||
pack.provinces[provinceId].state = newId;
|
||||
});
|
||||
|
||||
state.i = newId;
|
||||
newStates.push(state);
|
||||
});
|
||||
|
||||
for (const i of pack.cells.i) {
|
||||
const stateId = pack.cells.state[i];
|
||||
const lockedStateIndex = lockedStatesIds.indexOf(stateId) + 1;
|
||||
// lockedStateIndex is an index of locked state or 0 if state is not locked
|
||||
pack.cells.state[i] = lockedStateIndex;
|
||||
}
|
||||
|
||||
for (let i = newStates.length; i < count; i++) {
|
||||
let capital = null;
|
||||
|
||||
for (const burg of sortedBurgs) {
|
||||
const {x, y} = burg;
|
||||
if (capitalsTree.find(x, y, spacing) === undefined) {
|
||||
if (!isTooClose(x, y, spacing)) {
|
||||
burg.capital = 1;
|
||||
capital = burg;
|
||||
capitalsTree.add([x, y]);
|
||||
|
|
@ -206,6 +279,10 @@ function regenerateStates() {
|
|||
spacing = Math.max(spacing - 1, 1);
|
||||
}
|
||||
|
||||
// all burgs are too close, should not happen in normal conditions
|
||||
if (!capital) break;
|
||||
|
||||
// create new state
|
||||
const culture = capital.culture;
|
||||
const basename =
|
||||
capital.name.length < 9 && capital.cell % 5 === 0 ? capital.name : Names.getCulture(culture, 3, 6, "", 0);
|
||||
|
|
@ -222,34 +299,18 @@ function regenerateStates() {
|
|||
const coa = COA.generate(capital.coa, 0.3, null, cultureType);
|
||||
coa.shield = capital.coa.shield;
|
||||
|
||||
return {i, name, type, capital: capital.i, center: capital.cell, culture, expansionism, coa};
|
||||
});
|
||||
newStates.push({i, name, type, capital: capital.i, center: capital.cell, culture, expansionism, coa});
|
||||
}
|
||||
|
||||
BurgsAndStates.expandStates();
|
||||
BurgsAndStates.normalizeStates();
|
||||
BurgsAndStates.collectStatistics();
|
||||
BurgsAndStates.assignColors();
|
||||
BurgsAndStates.generateCampaigns();
|
||||
BurgsAndStates.generateDiplomacy();
|
||||
BurgsAndStates.defineStateForms();
|
||||
BurgsAndStates.generateProvinces(true);
|
||||
if (!layerIsOn("toggleStates")) toggleStates();
|
||||
else drawStates();
|
||||
if (!layerIsOn("toggleBorders")) toggleBorders();
|
||||
else drawBorders();
|
||||
BurgsAndStates.drawStateLabels();
|
||||
Military.generate();
|
||||
if (layerIsOn("toggleEmblems")) drawEmblems(); // redrawEmblems
|
||||
if (!statesCount) tip(`<i>States Number</i> option is set to zero. No counties are generated`, false, "warn");
|
||||
|
||||
if (document.getElementById("burgsOverviewRefresh")?.offsetParent) burgsOverviewRefresh.click();
|
||||
if (document.getElementById("statesEditorRefresh")?.offsetParent) statesEditorRefresh.click();
|
||||
if (document.getElementById("militaryOverviewRefresh")?.offsetParent) militaryOverviewRefresh.click();
|
||||
pack.states = newStates;
|
||||
}
|
||||
|
||||
function regenerateProvinces() {
|
||||
unfog();
|
||||
|
||||
BurgsAndStates.generateProvinces(true);
|
||||
BurgsAndStates.generateProvinces(true, true);
|
||||
drawBorders();
|
||||
if (layerIsOn("toggleProvinces")) drawProvinces();
|
||||
|
||||
|
|
@ -257,6 +318,7 @@ function regenerateProvinces() {
|
|||
document.querySelectorAll("[id^=provinceCOA]").forEach(el => el.remove());
|
||||
emblems.selectAll("use").remove();
|
||||
if (layerIsOn("toggleEmblems")) drawEmblems();
|
||||
refreshAllEditors();
|
||||
}
|
||||
|
||||
function regenerateBurgs() {
|
||||
|
|
@ -403,6 +465,7 @@ function regenerateReligions() {
|
|||
Religions.generate();
|
||||
if (!layerIsOn("toggleReligions")) toggleReligions();
|
||||
else drawReligions();
|
||||
refreshAllEditors();
|
||||
}
|
||||
|
||||
function regenerateCultures() {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"use strict";
|
||||
|
||||
// version and caching control
|
||||
const version = "1.88.06"; // generator version, update each time
|
||||
const version = "1.89.00"; // generator version, update each time
|
||||
|
||||
{
|
||||
document.title += " v" + version;
|
||||
|
|
@ -28,6 +28,7 @@ const version = "1.88.06"; // generator version, update each time
|
|||
|
||||
<ul>
|
||||
<strong>Latest changes:</strong>
|
||||
<li>Lock states, provinces, cultures, and religions from regeneration</li>
|
||||
<li>Heightmap brushes: linear edit option</li>
|
||||
<li>Data Charts screen</li>
|
||||
<li>Сultures and religions can have multiple parents in hierarchy tree</li>
|
||||
|
|
@ -35,9 +36,6 @@ const version = "1.88.06"; // generator version, update each time
|
|||
<li>Dialogs optimization for mobile</li>
|
||||
<li>New heightmap template: Fractious</li>
|
||||
<li>Template Editor: mask and invert tools</li>
|
||||
<li>Ability to install the App</li>
|
||||
<li>14 new default fonts</li>
|
||||
<li>Caching for faster startup</li>
|
||||
</ul>
|
||||
|
||||
<p>Join our <a href="${discord}" target="_blank">Discord server</a> and <a href="${reddit}" target="_blank">Reddit community</a> to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.</p>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue